3 namespace Drupal\views_ui;
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;
14 use Drupal\user\SharedTempStoreFactory;
15 use Drupal\views\Views;
16 use Symfony\Component\DependencyInjection\ContainerInterface;
17 use Symfony\Component\HttpFoundation\RequestStack;
20 * Form controller for the Views edit form.
22 class ViewEditForm extends ViewFormBase {
25 * The views temp store.
27 * @var \Drupal\user\SharedTempStore
34 * @var \Symfony\Component\HttpFoundation\RequestStack
36 protected $requestStack;
39 * The date formatter service.
41 * @var \Drupal\Core\Datetime\DateFormatterInterface
43 protected $dateFormatter;
46 * The element info manager.
48 * @var \Drupal\Core\Render\ElementInfoManagerInterface
50 protected $elementInfo;
53 * Constructs a new ViewEditForm object.
55 * @param \Drupal\user\SharedTempStoreFactory $temp_store_factory
56 * The factory for the temp store object.
57 * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
58 * The request stack object.
59 * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
60 * The date Formatter service.
61 * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
62 * The element info manager.
64 public function __construct(SharedTempStoreFactory $temp_store_factory, RequestStack $requestStack, DateFormatterInterface $date_formatter, ElementInfoManagerInterface $element_info) {
65 $this->tempStore = $temp_store_factory->get('views');
66 $this->requestStack = $requestStack;
67 $this->dateFormatter = $date_formatter;
68 $this->elementInfo = $element_info;
74 public static function create(ContainerInterface $container) {
76 $container->get('user.shared_tempstore'),
77 $container->get('request_stack'),
78 $container->get('date.formatter'),
79 $container->get('element_info')
86 public function form(array $form, FormStateInterface $form_state) {
87 $view = $this->entity;
88 $display_id = $this->displayID;
89 // Do not allow the form to be cached, because $form_state->get('view') can become
90 // stale between page requests.
91 // See views_ui_ajax_get_form() for how this affects #ajax.
92 // @todo To remove this and allow the form to be cacheable:
93 // - Change $form_state->get('view') to $form_state->getTemporary()['view'].
94 // - Add a #process function to initialize $form_state->getTemporary()['view']
95 // on cached form submissions.
96 // - Use \Drupal\Core\Form\FormStateInterface::loadInclude().
97 $form_state->disableCache();
100 if (!$view->getExecutable()->setDisplay($display_id)) {
101 $form['#markup'] = $this->t('Invalid display id @display', ['@display' => $display_id]);
106 $form['#tree'] = TRUE;
108 $form['#attached']['library'][] = 'core/jquery.ui.tabs';
109 $form['#attached']['library'][] = 'core/jquery.ui.dialog';
110 $form['#attached']['library'][] = 'core/drupal.states';
111 $form['#attached']['library'][] = 'core/drupal.tabledrag';
112 $form['#attached']['library'][] = 'views_ui/views_ui.admin';
113 $form['#attached']['library'][] = 'views_ui/admin.styling';
120 $view_status = $view->status() ? 'enabled' : 'disabled';
121 $form['#prefix'] .= '<div class="views-edit-view views-admin ' . $view_status . ' clearfix">';
122 $form['#suffix'] = '</div>' . $form['#suffix'];
124 $form['#attributes']['class'] = ['form-edit'];
126 if ($view->isLocked()) {
128 '#theme' => 'username',
129 '#account' => $this->entityManager->getStorage('user')->load($view->lock->owner),
131 $lock_message_substitutions = [
132 '@user' => \Drupal::service('renderer')->render($username),
133 '@age' => $this->dateFormatter->formatTimeDiffSince($view->lock->updated),
134 ':url' => $view->url('break-lock-form'),
137 '#type' => 'container',
138 '#attributes' => ['class' => ['view-locked', 'messages', 'messages--warning']],
139 '#children' => $this->t('This view is being edited by user @user, and is therefore locked from editing by others. This lock is @age old. Click here to <a href=":url">break this lock</a>.', $lock_message_substitutions),
145 '#type' => 'container',
146 '#attributes' => ['class' => ['view-changed', 'messages', 'messages--warning']],
147 '#children' => $this->t('You have unsaved changes.'),
150 if (empty($view->changed)) {
151 $form['changed']['#attributes']['class'][] = 'js-hide';
155 $form['displays'] = [
156 '#prefix' => '<h1 class="unit-title clearfix">' . $this->t('Displays') . '</h1>',
157 '#type' => 'container',
165 $form['displays']['top'] = $this->renderDisplayTop($view);
167 // The rest requires a display to be selected.
169 $form_state->set('display_id', $display_id);
171 // The part of the page where editing will take place.
172 $form['displays']['settings'] = [
173 '#type' => 'container',
174 '#id' => 'edit-display-settings',
176 'class' => ['edit-display-settings'],
180 // Add a text that the display is disabled.
181 if ($view->getExecutable()->displayHandlers->has($display_id)) {
182 if (!$view->getExecutable()->displayHandlers->get($display_id)->isEnabled()) {
183 $form['displays']['settings']['disabled']['#markup'] = $this->t('This display is disabled.');
187 // Add the edit display content
188 $tab_content = $this->getDisplayTab($view);
189 $tab_content['#theme_wrappers'] = ['container'];
190 $tab_content['#attributes'] = ['class' => ['views-display-tab']];
191 $tab_content['#id'] = 'views-tab-' . $display_id;
192 // Mark deleted displays as such.
193 $display = $view->get('display');
194 if (!empty($display[$display_id]['deleted'])) {
195 $tab_content['#attributes']['class'][] = 'views-display-deleted';
197 // Mark disabled displays as such.
199 if ($view->getExecutable()->displayHandlers->has($display_id) && !$view->getExecutable()->displayHandlers->get($display_id)->isEnabled()) {
200 $tab_content['#attributes']['class'][] = 'views-display-disabled';
202 $form['displays']['settings']['settings_content'] = [
203 '#type' => 'container',
204 'tab_content' => $tab_content,
214 protected function actions(array $form, FormStateInterface $form_state) {
215 $actions = parent::actions($form, $form_state);
216 unset($actions['delete']);
218 $actions['cancel'] = [
220 '#value' => $this->t('Cancel'),
221 '#submit' => ['::cancel'],
222 '#limit_validation_errors' => [],
224 if ($this->entity->isLocked()) {
225 $actions['submit']['#access'] = FALSE;
226 $actions['cancel']['#access'] = FALSE;
234 public function validateForm(array &$form, FormStateInterface $form_state) {
235 parent::validateForm($form, $form_state);
237 $view = $this->entity;
238 if ($view->isLocked()) {
239 $form_state->setErrorByName('', $this->t('Changes cannot be made to a locked view.'));
241 foreach ($view->getExecutable()->validate() as $display_errors) {
242 foreach ($display_errors as $error) {
243 $form_state->setErrorByName('', $error);
251 public function save(array $form, FormStateInterface $form_state) {
252 $view = $this->entity;
253 $executable = $view->getExecutable();
254 $executable->initDisplay();
256 // Go through and remove displayed scheduled for removal.
257 $displays = $view->get('display');
258 foreach ($displays as $id => $display) {
259 if (!empty($display['deleted'])) {
260 // Remove view display from view attachment under the attachments
262 $display_handler = $executable->displayHandlers->get($id);
263 if ($attachments = $display_handler->getAttachedDisplays()) {
264 foreach ($attachments as $attachment) {
265 $attached_options = $executable->displayHandlers->get($attachment)->getOption('displays');
266 unset($attached_options[$id]);
267 $executable->displayHandlers->get($attachment)->setOption('displays', $attached_options);
270 $executable->displayHandlers->remove($id);
271 unset($displays[$id]);
275 // Rename display ids if needed.
276 foreach ($executable->displayHandlers as $id => $display) {
277 if (!empty($display->display['new_id']) && $display->display['new_id'] !== $display->display['id'] && empty($display->display['deleted'])) {
278 $new_id = $display->display['new_id'];
279 $display->display['id'] = $new_id;
280 unset($display->display['new_id']);
281 $executable->displayHandlers->set($new_id, $display);
283 $displays[$new_id] = $displays[$id];
284 unset($displays[$id]);
286 // Redirect the user to the renamed display to be sure that the page itself exists and doesn't throw errors.
287 $form_state->setRedirect('entity.view.edit_display_form', [
288 'view' => $view->id(),
289 'display_id' => $new_id,
292 elseif (isset($display->display['new_id'])) {
293 unset($display->display['new_id']);
296 $view->set('display', $displays);
298 // @todo: Revisit this when https://www.drupal.org/node/1668866 is in.
299 $query = $this->requestStack->getCurrentRequest()->query;
300 $destination = $query->get('destination');
302 if (!empty($destination)) {
303 // Find out the first display which has a changed path and redirect to this url.
304 $old_view = Views::getView($view->id());
305 $old_view->initDisplay();
306 foreach ($old_view->displayHandlers as $id => $display) {
307 // Only check for displays with a path.
308 $old_path = $display->getOption('path');
309 if (empty($old_path)) {
313 if (($display->getPluginId() == 'page') && ($old_path == $destination) && ($old_path != $view->getExecutable()->displayHandlers->get($id)->getOption('path'))) {
314 $destination = $view->getExecutable()->displayHandlers->get($id)->getOption('path');
315 $query->remove('destination');
318 // @todo Use Url::fromPath() once https://www.drupal.org/node/2351379 is
320 $form_state->setRedirectUrl(Url::fromUri("base:$destination"));
325 drupal_set_message($this->t('The view %name has been saved.', ['%name' => $view->label()]));
327 // Remove this view from cache so we can edit it properly.
328 $this->tempStore->delete($view->id());
332 * Form submission handler for the 'cancel' action.
335 * An associative array containing the structure of the form.
336 * @param \Drupal\Core\Form\FormStateInterface $form_state
337 * The current state of the form.
339 public function cancel(array $form, FormStateInterface $form_state) {
340 // Remove this view from cache so edits will be lost.
341 $view = $this->entity;
342 $this->tempStore->delete($view->id());
343 $form_state->setRedirectUrl($this->entity->urlInfo('collection'));
347 * Returns a renderable array representing the edit page for one display.
349 public function getDisplayTab($view) {
351 $display_id = $this->displayID;
352 $display = $view->getExecutable()->displayHandlers->get($display_id);
353 // If the plugin doesn't exist, display an error message instead of an edit
355 if (empty($display)) {
356 // @TODO: Improved UX for the case where a plugin is missing.
357 $build['#markup'] = $this->t("Error: Display @display refers to a plugin named '@plugin', but that plugin is not available.", ['@display' => $display->display['id'], '@plugin' => $display->display['display_plugin']]);
359 // Build the content of the edit page.
361 $build['details'] = $this->getDisplayDetails($view, $display->display);
363 // In AJAX context, ViewUI::rebuildCurrentTab() returns this outside of form
364 // context, so hook_form_views_ui_edit_form_alter() is insufficient.
365 \Drupal::moduleHandler()->alter('views_ui_display_tab', $build, $view, $display_id);
370 * Helper function to get the display details section of the edit UI.
375 * A renderable page build array.
377 public function getDisplayDetails($view, $display) {
378 $display_title = $this->getDisplayLabel($view, $display['id'], FALSE);
380 '#theme_wrappers' => ['container'],
381 '#attributes' => ['id' => 'edit-display-settings-details'],
384 $is_display_deleted = !empty($display['deleted']);
385 // The master display cannot be duplicated.
386 $is_default = $display['id'] == 'default';
387 // @todo: Figure out why getOption doesn't work here.
388 $is_enabled = $view->getExecutable()->displayHandlers->get($display['id'])->isEnabled();
390 if ($display['id'] != 'default') {
391 $build['top']['#theme_wrappers'] = ['container'];
392 $build['top']['#attributes']['id'] = 'edit-display-settings-top';
393 $build['top']['#attributes']['class'] = ['views-ui-display-tab-actions', 'edit-display-settings-top', 'views-ui-display-tab-bucket', 'clearfix'];
395 // The Delete, Duplicate and Undo Delete buttons.
396 $build['top']['actions'] = [
397 '#theme_wrappers' => ['dropbutton_wrapper'],
400 // Because some of the 'links' are actually submit buttons, we have to
401 // manually wrap each item in <li> and the whole list in <ul>.
402 $build['top']['actions']['prefix']['#markup'] = '<ul class="dropbutton">';
404 if (!$is_display_deleted) {
406 $build['top']['actions']['enable'] = [
408 '#value' => $this->t('Enable @display_title', ['@display_title' => $display_title]),
409 '#limit_validation_errors' => [],
410 '#submit' => ['::submitDisplayEnable', '::submitDelayDestination'],
411 '#prefix' => '<li class="enable">',
412 "#suffix" => '</li>',
415 // Add a link to view the page unless the view is disabled or has no
417 elseif ($view->status() && $view->getExecutable()->displayHandlers->get($display['id'])->hasPath()) {
418 $path = $view->getExecutable()->displayHandlers->get($display['id'])->getPath();
419 if ($path && (strpos($path, '%') === FALSE)) {
420 if (!parse_url($path, PHP_URL_SCHEME)) {
421 // @todo Views should expect and store a leading /. See:
422 // https://www.drupal.org/node/2423913
423 $url = Url::fromUserInput('/' . ltrim($path, '/'));
426 $url = Url::fromUri("base:$path");
428 $build['top']['actions']['path'] = [
430 '#title' => $this->t('View @display_title', ['@display_title' => $display_title]),
431 '#options' => ['alt' => [$this->t("Go to the real page for this display")]],
433 '#prefix' => '<li class="view">',
434 "#suffix" => '</li>',
439 $build['top']['actions']['duplicate'] = [
441 '#value' => $this->t('Duplicate @display_title', ['@display_title' => $display_title]),
442 '#limit_validation_errors' => [],
443 '#submit' => ['::submitDisplayDuplicate', '::submitDelayDestination'],
444 '#prefix' => '<li class="duplicate">',
445 "#suffix" => '</li>',
448 // Always allow a display to be deleted.
449 $build['top']['actions']['delete'] = [
451 '#value' => $this->t('Delete @display_title', ['@display_title' => $display_title]),
452 '#limit_validation_errors' => [],
453 '#submit' => ['::submitDisplayDelete', '::submitDelayDestination'],
454 '#prefix' => '<li class="delete">',
455 "#suffix" => '</li>',
458 foreach (Views::fetchPluginNames('display', NULL, [$view->get('storage')->get('base_table')]) as $type => $label) {
459 if ($type == $display['display_plugin']) {
463 $build['top']['actions']['duplicate_as'][$type] = [
465 '#value' => $this->t('Duplicate as @type', ['@type' => $label]),
466 '#limit_validation_errors' => [],
467 '#submit' => ['::submitDuplicateDisplayAsType', '::submitDelayDestination'],
468 '#prefix' => '<li class="duplicate">',
469 '#suffix' => '</li>',
474 $build['top']['actions']['undo_delete'] = [
476 '#value' => $this->t('Undo delete of @display_title', ['@display_title' => $display_title]),
477 '#limit_validation_errors' => [],
478 '#submit' => ['::submitDisplayUndoDelete', '::submitDelayDestination'],
479 '#prefix' => '<li class="undo-delete">',
480 "#suffix" => '</li>',
484 $build['top']['actions']['disable'] = [
486 '#value' => $this->t('Disable @display_title', ['@display_title' => $display_title]),
487 '#limit_validation_errors' => [],
488 '#submit' => ['::submitDisplayDisable', '::submitDelayDestination'],
489 '#prefix' => '<li class="disable">',
490 "#suffix" => '</li>',
493 $build['top']['actions']['suffix']['#markup'] = '</ul>';
495 // The area above the three columns.
496 $build['top']['display_title'] = [
497 '#theme' => 'views_ui_display_tab_setting',
498 '#description' => $this->t('Display name'),
499 '#link' => $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($display_title, 'display_title'),
503 $build['columns'] = [];
504 $build['columns']['#theme_wrappers'] = ['container'];
505 $build['columns']['#attributes'] = ['id' => 'edit-display-settings-main', 'class' => ['clearfix', 'views-display-columns']];
507 $build['columns']['first']['#theme_wrappers'] = ['container'];
508 $build['columns']['first']['#attributes'] = ['class' => ['views-display-column', 'first']];
510 $build['columns']['second']['#theme_wrappers'] = ['container'];
511 $build['columns']['second']['#attributes'] = ['class' => ['views-display-column', 'second']];
513 $build['columns']['second']['settings'] = [];
514 $build['columns']['second']['header'] = [];
515 $build['columns']['second']['footer'] = [];
516 $build['columns']['second']['empty'] = [];
517 $build['columns']['second']['pager'] = [];
519 // The third column buckets are wrapped in details.
520 $build['columns']['third'] = [
521 '#type' => 'details',
522 '#title' => $this->t('Advanced'),
523 '#theme_wrappers' => ['details'],
526 'views-display-column',
531 // Collapse the details by default.
532 $build['columns']['third']['#open'] = \Drupal::config('views.settings')->get('ui.show.advanced_column');
534 // Each option (e.g. title, access, display as grid/table/list) fits into one
535 // of several "buckets," or boxes (Format, Fields, Sort, and so on).
538 // Fetch options from the display plugin, with a list of buckets they go into.
540 $view->getExecutable()->displayHandlers->get($display['id'])->optionsSummary($buckets, $options);
542 // Place each option into its bucket.
543 foreach ($options as $id => $option) {
544 // Each option self-identifies as belonging in a particular bucket.
545 $buckets[$option['category']]['build'][$id] = $this->buildOptionForm($view, $id, $option, $display);
548 // Place each bucket into the proper column.
549 foreach ($buckets as $id => $bucket) {
550 // Let buckets identify themselves as belonging in a column.
551 if (isset($bucket['column']) && isset($build['columns'][$bucket['column']])) {
552 $column = $bucket['column'];
554 // If a bucket doesn't pick one of our predefined columns to belong to, put
555 // it in the last one.
559 if (isset($bucket['build']) && is_array($bucket['build'])) {
560 $build['columns'][$column][$id] = $bucket['build'];
561 $build['columns'][$column][$id]['#theme_wrappers'][] = 'views_ui_display_tab_bucket';
562 $build['columns'][$column][$id]['#title'] = !empty($bucket['title']) ? $bucket['title'] : '';
563 $build['columns'][$column][$id]['#name'] = $id;
567 $build['columns']['first']['fields'] = $this->getFormBucket($view, 'field', $display);
568 $build['columns']['first']['filters'] = $this->getFormBucket($view, 'filter', $display);
569 $build['columns']['first']['sorts'] = $this->getFormBucket($view, 'sort', $display);
570 $build['columns']['second']['header'] = $this->getFormBucket($view, 'header', $display);
571 $build['columns']['second']['footer'] = $this->getFormBucket($view, 'footer', $display);
572 $build['columns']['second']['empty'] = $this->getFormBucket($view, 'empty', $display);
573 $build['columns']['third']['arguments'] = $this->getFormBucket($view, 'argument', $display);
574 $build['columns']['third']['relationships'] = $this->getFormBucket($view, 'relationship', $display);
580 * Submit handler to add a restore a removed display to a view.
582 public function submitDisplayUndoDelete($form, FormStateInterface $form_state) {
583 $view = $this->entity;
584 // Create the new display
585 $id = $form_state->get('display_id');
586 $displays = $view->get('display');
587 $displays[$id]['deleted'] = FALSE;
588 $view->set('display', $displays);
593 // Redirect to the top-level edit page.
594 $form_state->setRedirect('entity.view.edit_display_form', [
595 'view' => $view->id(),
601 * Submit handler to enable a disabled display.
603 public function submitDisplayEnable($form, FormStateInterface $form_state) {
604 $view = $this->entity;
605 $id = $form_state->get('display_id');
606 // setOption doesn't work because this would might affect upper displays
607 $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', TRUE);
612 // Redirect to the top-level edit page.
613 $form_state->setRedirect('entity.view.edit_display_form', [
614 'view' => $view->id(),
620 * Submit handler to disable display.
622 public function submitDisplayDisable($form, FormStateInterface $form_state) {
623 $view = $this->entity;
624 $id = $form_state->get('display_id');
625 $view->getExecutable()->displayHandlers->get($id)->setOption('enabled', FALSE);
630 // Redirect to the top-level edit page.
631 $form_state->setRedirect('entity.view.edit_display_form', [
632 'view' => $view->id(),
638 * Submit handler to delete a display from a view.
640 public function submitDisplayDelete($form, FormStateInterface $form_state) {
641 $view = $this->entity;
642 $display_id = $form_state->get('display_id');
644 // Mark the display for deletion.
645 $displays = $view->get('display');
646 $displays[$display_id]['deleted'] = TRUE;
647 $view->set('display', $displays);
650 // Redirect to the top-level edit page. The first remaining display will
651 // become the active display.
652 $form_state->setRedirectUrl($view->urlInfo('edit-form'));
656 * Regenerate the current tab for AJAX updates.
658 * @param \Drupal\views_ui\ViewUI $view
659 * The view to regenerate its tab.
660 * @param \Drupal\Core\Ajax\AjaxResponse $response
661 * The response object to add new commands to.
662 * @param string $display_id
663 * The display ID of the tab to regenerate.
665 public function rebuildCurrentTab(ViewUI $view, AjaxResponse $response, $display_id) {
666 $this->displayID = $display_id;
667 if (!$view->getExecutable()->setDisplay('default')) {
671 // Regenerate the main display area.
672 $build = $this->getDisplayTab($view);
673 $response->addCommand(new HtmlCommand('#views-tab-' . $display_id, $build));
675 // Regenerate the top area so changes to display names and order will appear.
676 $build = $this->renderDisplayTop($view);
677 $response->addCommand(new ReplaceCommand('#views-display-top', $build));
681 * Render the top of the display so it can be updated during ajax operations.
683 public function renderDisplayTop(ViewUI $view) {
684 $display_id = $this->displayID;
685 $element['#theme_wrappers'][] = 'views_ui_container';
686 $element['#attributes']['class'] = ['views-display-top', 'clearfix'];
687 $element['#attributes']['id'] = ['views-display-top'];
689 // Extra actions for the display
690 $element['extra_actions'] = [
691 '#type' => 'dropbutton',
693 'id' => 'views-display-extra-actions',
697 'title' => $this->t('Edit view name/description'),
698 'url' => Url::fromRoute('views_ui.form_edit_details', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
699 'attributes' => ['class' => ['views-ajax-link']],
702 'title' => $this->t('Analyze view'),
703 'url' => Url::fromRoute('views_ui.form_analyze', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
704 'attributes' => ['class' => ['views-ajax-link']],
707 'title' => $this->t('Duplicate view'),
708 'url' => $view->urlInfo('duplicate-form'),
711 'title' => $this->t('Reorder displays'),
712 'url' => Url::fromRoute('views_ui.form_reorder_displays', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display_id]),
713 'attributes' => ['class' => ['views-ajax-link']],
718 if ($view->access('delete')) {
719 $element['extra_actions']['#links']['delete'] = [
720 'title' => $this->t('Delete view'),
721 'url' => $view->urlInfo('delete-form'),
725 // Let other modules add additional links here.
726 \Drupal::moduleHandler()->alter('views_ui_display_top_links', $element['extra_actions']['#links'], $view, $display_id);
728 if (isset($view->type) && $view->type != $this->t('Default')) {
729 if ($view->type == $this->t('Overridden')) {
730 $element['extra_actions']['#links']['revert'] = [
731 'title' => $this->t('Revert view'),
732 'href' => "admin/structure/views/view/{$view->id()}/revert",
733 'query' => ['destination' => $view->url('edit-form')],
737 $element['extra_actions']['#links']['delete'] = [
738 'title' => $this->t('Delete view'),
739 'url' => $view->urlInfo('delete-form'),
744 // Determine the displays available for editing.
745 if ($tabs = $this->getDisplayTabs($view)) {
747 $tabs[$display_id]['#active'] = TRUE;
749 $tabs['#prefix'] = '<h2 class="visually-hidden">' . $this->t('Secondary tabs') . '</h2><ul id = "views-display-menu-tabs" class="tabs secondary">';
750 $tabs['#suffix'] = '</ul>';
751 $element['tabs'] = $tabs;
754 // Buttons for adding a new display.
755 foreach (Views::fetchPluginNames('display', NULL, [$view->get('base_table')]) as $type => $label) {
756 $element['add_display'][$type] = [
758 '#value' => $this->t('Add @display', ['@display' => $label]),
759 '#limit_validation_errors' => [],
760 '#submit' => ['::submitDisplayAdd', '::submitDelayDestination'],
761 '#attributes' => ['class' => ['add-display']],
762 // Allow JavaScript to remove the 'Add ' prefix from the button label when
763 // placing the button in a "Add" dropdown menu.
764 '#process' => array_merge(['views_ui_form_button_was_clicked'], $this->elementInfo->getInfoProperty('submit', '#process', [])),
765 '#values' => [$this->t('Add @display', ['@display' => $label]), $label],
773 * Submit handler for form buttons that do not complete a form workflow.
775 * The Edit View form is a multistep form workflow, but with state managed by
776 * the SharedTempStore rather than $form_state->setRebuild(). Without this
777 * submit handler, buttons that add or remove displays would redirect to the
778 * destination parameter (e.g., when the Edit View form is linked to from a
779 * contextual link). This handler can be added to buttons whose form submission
780 * should not yet redirect to the destination.
782 public function submitDelayDestination($form, FormStateInterface $form_state) {
783 $request = $this->requestStack->getCurrentRequest();
784 $destination = $request->query->get('destination');
786 $redirect = $form_state->getRedirect();
787 // If there is a destination, and redirects are not explicitly disabled, add
788 // the destination as a query string to the redirect and suppress it for the
790 if (isset($destination) && $redirect !== FALSE) {
791 // Create a valid redirect if one does not exist already.
792 if (!($redirect instanceof Url)) {
793 $redirect = Url::createFromRequest($request);
796 // Add the current destination to the redirect unless one exists already.
797 $options = $redirect->getOptions();
798 if (!isset($options['query']['destination'])) {
799 $options['query']['destination'] = $destination;
800 $redirect->setOptions($options);
803 $form_state->setRedirectUrl($redirect);
804 $request->query->remove('destination');
809 * Submit handler to duplicate a display for a view.
811 public function submitDisplayDuplicate($form, FormStateInterface $form_state) {
812 $view = $this->entity;
813 $display_id = $this->displayID;
815 // Create the new display.
816 $displays = $view->get('display');
817 $display = $view->getExecutable()->newDisplay($displays[$display_id]['display_plugin']);
818 $new_display_id = $display->display['id'];
819 $displays[$new_display_id] = $displays[$display_id];
820 $displays[$new_display_id]['id'] = $new_display_id;
821 $view->set('display', $displays);
823 // By setting the current display the changed marker will appear on the new
825 $view->getExecutable()->current_display = $new_display_id;
828 // Redirect to the new display's edit page.
829 $form_state->setRedirect('entity.view.edit_display_form', [
830 'view' => $view->id(),
831 'display_id' => $new_display_id,
836 * Submit handler to add a display to a view.
838 public function submitDisplayAdd($form, FormStateInterface $form_state) {
839 $view = $this->entity;
840 // Create the new display.
841 $parents = $form_state->getTriggeringElement()['#parents'];
842 $display_type = array_pop($parents);
843 $display = $view->getExecutable()->newDisplay($display_type);
844 $display_id = $display->display['id'];
845 // A new display got added so the asterisks symbol should appear on the new
847 $view->getExecutable()->current_display = $display_id;
850 // Redirect to the new display's edit page.
851 $form_state->setRedirect('entity.view.edit_display_form', [
852 'view' => $view->id(),
853 'display_id' => $display_id,
858 * Submit handler to Duplicate a display as another display type.
860 public function submitDuplicateDisplayAsType($form, FormStateInterface $form_state) {
861 /** @var \Drupal\views\ViewEntityInterface $view */
862 $view = $this->entity;
863 $display_id = $this->displayID;
865 // Create the new display.
866 $parents = $form_state->getTriggeringElement()['#parents'];
867 $display_type = array_pop($parents);
869 $new_display_id = $view->duplicateDisplayAsType($display_id, $display_type);
871 // By setting the current display the changed marker will appear on the new
873 $view->getExecutable()->current_display = $new_display_id;
876 // Redirect to the new display's edit page.
877 $form_state->setRedirect('entity.view.edit_display_form', [
878 'view' => $view->id(),
879 'display_id' => $new_display_id,
884 * Build a renderable array representing one option on the edit form.
886 * This function might be more logical as a method on an object, if a suitable
887 * object emerges out of refactoring.
889 public function buildOptionForm(ViewUI $view, $id, $option, $display) {
891 $option_build['#theme'] = 'views_ui_display_tab_setting';
893 $option_build['#description'] = $option['title'];
895 $option_build['#link'] = $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($option['value'], $id, '', empty($option['desc']) ? '' : $option['desc']);
897 $option_build['#links'] = [];
898 if (!empty($option['links']) && is_array($option['links'])) {
899 foreach ($option['links'] as $link_id => $link_value) {
900 $option_build['#settings_links'][] = $view->getExecutable()->displayHandlers->get($display['id'])->optionLink($option['setting'], $link_id, 'views-button-configure', $link_value);
904 if (!empty($view->getExecutable()->displayHandlers->get($display['id'])->options['defaults'][$id])) {
905 $display_id = 'default';
906 $option_build['#defaulted'] = TRUE;
909 $display_id = $display['id'];
910 if (!$view->getExecutable()->displayHandlers->get($display['id'])->isDefaultDisplay()) {
911 if ($view->getExecutable()->displayHandlers->get($display['id'])->defaultableSections($id)) {
912 $option_build['#overridden'] = TRUE;
916 $option_build['#attributes']['class'][] = Html::cleanCssIdentifier($display_id . '-' . $id);
917 return $option_build;
921 * Add information about a section to a display.
923 public function getFormBucket(ViewUI $view, $type, $display) {
924 $executable = $view->getExecutable();
925 $executable->setDisplay($display['id']);
926 $executable->initStyle();
928 $types = $executable->getHandlerTypes();
931 '#theme_wrappers' => ['views_ui_display_tab_bucket'],
934 $build['#overridden'] = FALSE;
935 $build['#defaulted'] = FALSE;
937 $build['#name'] = $type;
938 $build['#title'] = $types[$type]['title'];
940 $rearrange_url = Url::fromRoute('views_ui.form_rearrange', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]);
941 $class = 'icon compact rearrange';
943 // Different types now have different rearrange forms, so we use this switch
944 // to get the right one.
947 // The rearrange form for filters contains the and/or UI, so override
949 $rearrange_url = Url::fromRoute('views_ui.form_rearrange_filter', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id']]);
950 // TODO: Add another class to have another symbol for filter rearrange.
951 $class = 'icon compact rearrange';
954 // Fetch the style plugin info so we know whether to list fields or not.
955 $style_plugin = $executable->style_plugin;
956 $uses_fields = $style_plugin && $style_plugin->usesFields();
958 $build['fields'][] = [
959 '#markup' => $this->t('The selected style or row format does not use fields.'),
960 '#theme_wrappers' => ['views_ui_container'],
961 '#attributes' => ['class' => ['views-display-setting']],
969 if (!$executable->display_handler->usesAreas()) {
971 '#markup' => $this->t('The selected display type does not use @type plugins', ['@type' => $type]),
972 '#theme_wrappers' => ['views_ui_container'],
973 '#attributes' => ['class' => ['views-display-setting']],
980 // Create an array of actions to pass to links template.
982 $count_handlers = count($executable->display_handler->getHandlers($type));
984 // Create the add text variable for the add action.
985 $add_text = $this->t('Add <span class="visually-hidden">@type</span>', ['@type' => $types[$type]['ltitle']]);
988 'title' => $add_text,
989 'url' => Url::fromRoute('views_ui.form_add_handler', ['js' => 'nojs', 'view' => $view->id(), 'display_id' => $display['id'], 'type' => $type]),
990 'attributes' => ['class' => ['icon compact add', 'views-ajax-link'], 'id' => 'views-add-' . $type],
992 if ($count_handlers > 0) {
993 // Create the rearrange text variable for the rearrange action.
994 $rearrange_text = $type == 'filter' ? $this->t('And/Or Rearrange <span class="visually-hidden">filter criteria</span>') : $this->t('Rearrange <span class="visually-hidden">@type</span>', ['@type' => $types[$type]['ltitle']]);
996 $actions['rearrange'] = [
997 'title' => $rearrange_text,
998 'url' => $rearrange_url,
999 'attributes' => ['class' => [$class, 'views-ajax-link'], 'id' => 'views-rearrange-' . $type],
1003 // Render the array of links
1004 $build['#actions'] = [
1005 '#type' => 'dropbutton',
1006 '#links' => $actions,
1008 'class' => ['views-ui-settings-bucket-operations'],
1012 if (!$executable->display_handler->isDefaultDisplay()) {
1013 if (!$executable->display_handler->isDefaulted($types[$type]['plural'])) {
1014 $build['#overridden'] = TRUE;
1017 $build['#defaulted'] = TRUE;
1021 static $relationships = NULL;
1022 if (!isset($relationships)) {
1023 // Get relationship labels.
1024 $relationships = [];
1025 foreach ($executable->display_handler->getHandlers('relationship') as $id => $handler) {
1026 $relationships[$id] = $handler->adminLabel();
1030 // Filters can now be grouped so we do a little bit extra:
1033 if ($type == 'filter') {
1034 $group_info = $executable->display_handler->getOption('filter_groups');
1035 // If there is only one group but it is using the "OR" filter, we still
1036 // treat it as a group for display purposes, since we want to display the
1037 // "OR" label next to items within the group.
1038 if (!empty($group_info['groups']) && (count($group_info['groups']) > 1 || current($group_info['groups']) == 'OR')) {
1040 $groups = [0 => []];
1044 $build['fields'] = [];
1046 foreach ($executable->display_handler->getOption($types[$type]['plural']) as $id => $field) {
1047 // Build the option link for this handler ("Node: ID = article").
1048 $build['fields'][$id] = [];
1049 $build['fields'][$id]['#theme'] = 'views_ui_display_tab_setting';
1051 $handler = $executable->display_handler->getHandler($type, $id);
1052 if ($handler->broken()) {
1053 $build['fields'][$id]['#class'][] = 'broken';
1054 $field_name = $handler->adminLabel();
1055 $build['fields'][$id]['#link'] = $this->l($field_name, new Url('views_ui.form_handler', [
1057 'view' => $view->id(),
1058 'display_id' => $display['id'],
1061 ], ['attributes' => ['class' => ['views-ajax-link']]]));
1065 $field_name = $handler->adminLabel(TRUE);
1066 if (!empty($field['relationship']) && !empty($relationships[$field['relationship']])) {
1067 $field_name = '(' . $relationships[$field['relationship']] . ') ' . $field_name;
1070 $description = $handler->adminSummary();
1071 $link_text = $field_name . (empty($description) ? '' : " ($description)");
1072 $link_attributes = ['class' => ['views-ajax-link']];
1073 if (!empty($field['exclude'])) {
1074 $link_attributes['class'][] = 'views-field-excluded';
1075 // Add a [hidden] marker, if the field is excluded.
1076 $link_text .= ' [' . $this->t('hidden') . ']';
1078 $build['fields'][$id]['#link'] = $this->l($link_text, new Url('views_ui.form_handler', [
1080 'view' => $view->id(),
1081 'display_id' => $display['id'],
1084 ], ['attributes' => $link_attributes]));
1085 $build['fields'][$id]['#class'][] = Html::cleanCssIdentifier($display['id'] . '-' . $type . '-' . $id);
1087 if ($executable->display_handler->useGroupBy() && $handler->usesGroupBy()) {
1088 $build['fields'][$id]['#settings_links'][] = $this->l(SafeMarkup::format('<span class="label">@text</span>', ['@text' => $this->t('Aggregation settings')]), new Url('views_ui.form_handler_group', [
1090 'view' => $view->id(),
1091 'display_id' => $display['id'],
1094 ], ['attributes' => ['class' => ['views-button-configure', 'views-ajax-link'], 'title' => $this->t('Aggregation settings')]]));
1097 if ($handler->hasExtraOptions()) {
1098 $build['fields'][$id]['#settings_links'][] = $this->l(SafeMarkup::format('<span class="label">@text</span>', ['@text' => $this->t('Settings')]), new Url('views_ui.form_handler_extra', [
1100 'view' => $view->id(),
1101 'display_id' => $display['id'],
1104 ], ['attributes' => ['class' => ['views-button-configure', 'views-ajax-link'], 'title' => $this->t('Settings')]]));
1108 $gid = $handler->options['group'];
1110 // Show in default group if the group does not exist.
1111 if (empty($group_info['groups'][$gid])) {
1114 $groups[$gid][] = $id;
1118 // If using grouping, re-order fields so that they show up properly in the list.
1119 if ($type == 'filter' && $grouping) {
1120 $store = $build['fields'];
1121 $build['fields'] = [];
1122 foreach ($groups as $gid => $contents) {
1123 // Display an operator between each group.
1124 if (!empty($build['fields'])) {
1125 $build['fields'][] = [
1126 '#theme' => 'views_ui_display_tab_setting',
1127 '#class' => ['views-group-text'],
1128 '#link' => ($group_info['operator'] == 'OR' ? $this->t('OR') : $this->t('AND')),
1131 // Display an operator between each pair of filters within the group.
1132 $keys = array_keys($contents);
1134 foreach ($contents as $key => $pid) {
1135 if ($key != $last) {
1136 $operator = $group_info['groups'][$gid] == 'OR' ? $this->t('OR') : $this->t('AND');
1137 $store[$pid]['#link'] = SafeMarkup::format('@link <span>@operator</span>', ['@link' => $store[$pid]['#link'], '@operator' => $operator]);
1139 $build['fields'][$pid] = $store[$pid];