Verified Commit a2bcba1d authored by godotislate's avatar godotislate
Browse files

refactor: #3035340 Deprecate core/modules/views_ui/admin.inc

By: lendude
By: yogeshmpawar
By: andypost
By: shaktik
By: claudiu.cristea
By: kim.pepper
By: nicxvan
By: berdir
By: godotislate
By: xjm
parent f245a8f0
Loading
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -21,12 +21,14 @@
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\PluginBase;
use Drupal\views\Views;
use Drupal\views\ViewsFormHelperTrait;

/**
 * Base class for views display plugins.
 */
abstract class DisplayPluginBase extends PluginBase implements DisplayPluginInterface, DependentPluginInterface {
  use PluginDependencyTrait;
  use ViewsFormHelperTrait;

  /**
   * The top object of a view.
@@ -1384,7 +1386,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    parent::buildOptionsForm($form, $form_state);
    $section = $form_state->get('section');
    if ($this->defaultableSections($section)) {
      views_ui_standard_display_dropdown($form, $form_state, $section);
      $this->standardDisplayDropdown($form, $form_state, $section);
    }
    $form['#title'] = $this->display['display_title'] . ': ';

+3 −1
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@
use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Drupal\views\ViewsFormHelperTrait;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
@@ -42,6 +43,7 @@ class EntityField extends FieldPluginBase implements CacheableDependencyInterfac

  use FieldAPIHandlerTrait;
  use PluginDependencyTrait;
  use ViewsFormHelperTrait;

  /**
   * An array to store field renderable arrays for use by renderItems().
@@ -506,7 +508,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
      '#options' => $formatters,
      '#default_value' => $this->options['type'],
      '#ajax' => [
        'url' => views_ui_build_form_url($form_state),
        'url' => $this->buildFormUrl($form_state),
      ],
      '#submit' => [[$this, 'submitTemporaryForm']],
      '#executes_submit_callback' => TRUE,
+7 −7
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@
use Drupal\Core\Url;
use Drupal\views\Entity\View;
use Drupal\views\Views;
use Drupal\views\ViewsFormAjaxHelperTrait;
use Drupal\views_ui\ViewUI;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\PluginBase;
@@ -36,6 +37,8 @@
 */
abstract class WizardPluginBase extends PluginBase implements WizardInterface {

  use ViewsFormAjaxHelperTrait;

  /**
   * The base table connected with the wizard.
   *
@@ -285,7 +288,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    $style_form['style_plugin']['#default_value'] = static::getSelected($form_state, ['page', 'style', 'style_plugin'], 'default', $style_form['style_plugin']);
    // Changing this dropdown updates $form['displays']['page']['options'] via
    // AJAX.
    views_ui_add_ajax_trigger($style_form, 'style_plugin', ['displays', 'page', 'options']);
    $this->addAjaxTrigger($style_form, 'style_plugin', ['displays', 'page', 'options']);

    $this->buildFormStyle($form, $form_state, 'page');
    $form['displays']['page']['options']['items_per_page'] = [
@@ -419,7 +422,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
      ], 'default', $style_form['style_plugin']);
      // Changing this dropdown updates $form['displays']['block']['options']
      // via AJAX.
      views_ui_add_ajax_trigger($style_form, 'style_plugin', ['displays', 'block', 'options']);
      $this->addAjaxTrigger($style_form, 'style_plugin', ['displays', 'block', 'options']);

      $this->buildFormStyle($form, $form_state, 'block');
      $form['displays']['block']['options']['items_per_page'] = [
@@ -589,7 +592,7 @@ protected function buildFormStyle(array &$form, FormStateInterface $form_state,
      $default_value = $block_with_linked_titles_available ? 'titles_linked' : key($options);
      $style_form['row_plugin']['#default_value'] = static::getSelected($form_state, [$type, 'style', 'row_plugin'], $default_value, $style_form['row_plugin']);
      // Changing this dropdown updates the individual row options via AJAX.
      views_ui_add_ajax_trigger($style_form, 'row_plugin', ['displays', $type, 'options', 'style', 'row_options']);
      $this->addAjaxTrigger($style_form, 'row_plugin', ['displays', $type, 'options', 'style', 'row_options']);

      // This is the region that can be updated by AJAX. The base class doesn't
      // add anything here, but child classes can.
@@ -621,8 +624,6 @@ protected function rowStyleOptions() {
   * available).
   */
  protected function buildFilters(&$form, FormStateInterface $form_state) {
    \Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin');

    $bundles = isset($this->entityTypeId) ? $this->bundleInfoService->getBundleInfo($this->entityTypeId) : [];
    // If the current base table support bundles and has more than one (like
    // user).
@@ -642,7 +643,7 @@ protected function buildFilters(&$form, FormStateInterface $form_state) {
      // Changing this dropdown updates the entire content of $form['displays']
      // via AJAX, since each bundle might have entirely different fields
      // attached to it, etc.
      views_ui_add_ajax_trigger($form['displays']['show'], 'type', ['displays']);
      $this->addAjaxTrigger($form['displays']['show'], 'type', ['displays']);
    }
  }

@@ -927,7 +928,6 @@ protected function defaultDisplayFiltersUser(array $form, FormStateInterface $fo
      // Figure out the table where $bundle_key lives. It may not be the same as
      // the base table for the view; the taxonomy vocabulary machine_name, for
      // example, is stored in taxonomy_vocabulary, not taxonomy_term_data.
      \Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin');
      $fields = Views::viewsDataHelper()->fetchFields($this->base_table, 'filter');
      $table = FALSE;
      if (isset($fields[$this->base_table . '.' . $bundle_key])) {
+255 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\views;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Provides reusable code to be shared by Views Ajax forms.
 */
trait ViewsFormAjaxHelperTrait {

  use StringTranslationTrait;

  /**
   * Memory cache for processed buttons.
   *
   * @var array<string, int>
   */
  protected static array $ajaxTriggerButtons = [];

  /**
   * Converts a form element in the new view wizard to be AJAX-enabled.
   *
   * This function takes a form element and adds AJAX behaviors to it such that
   * changing it triggers another part of the form to update automatically. It
   * also adds a submit-button to the form that appears next to the triggering
   * element, and that duplicates its functionality for users who do not have
   * JavaScript enabled (the button is automatically hidden for users who do
   * have JavaScript).
   *
   * To use this function, call it directly from your form builder function
   * immediately after you have defined the form element that will serve as the
   * JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may
   * mean that the non-JavaScript fallback button does not appear in the correct
   * place in the form.
   *
   * @param array $wrappingElement
   *   The element whose child will server as the AJAX trigger. For example, if
   *   $form['some_wrapper']['triggering_element'] represents the element which
   *   will trigger the AJAX behavior, you would pass $form['some_wrapper'] for
   *   this parameter.
   * @param string $triggerKey
   *   The key within the wrapping element that identifies which of its children
   *   serves as the AJAX trigger. In the above example, you would pass
   *   'triggering_element' for this parameter.
   * @param string[] $refreshParents
   *   An array of parent keys that point to the part of the form that AJAX will
   *   refresh. For example, if triggering the AJAX behavior should cause
   *   $form['dynamic_content']['section'] to be refreshed, you would pass
   *   ['dynamic_content', 'section'] for this parameter.
   */
  protected function addAjaxTrigger(array &$wrappingElement, string $triggerKey, array $refreshParents): void {
    // Add the AJAX behavior to the triggering element.
    $triggeringElement = &$wrappingElement[$triggerKey];
    $triggeringElement['#ajax']['callback'] = static::class . ':ajaxUpdateForm';

    // We do not use \Drupal\Component\Utility\Html::getUniqueId() to get an ID
    // for the AJAX wrapper. It remembers IDs across AJAX requests (and won't
    // reuse them). But in our case we need to use the same ID from request to
    // request so that the wrapper can be recognized by the AJAX system and its
    // content can be dynamically updated. So instead, we will keep track of
    // duplicate IDs (within a single request) on our own, later in this method.
    $triggeringElement['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refreshParents) . '-wrapper';

    // Add a submit-button for users who do not have JavaScript enabled. It
    // should be displayed next to the triggering element on the form.
    $buttonKey = $triggerKey . '_trigger_update';
    $wrappingElement[$buttonKey] = [
      '#type' => 'submit',
      // Hide this button when JavaScript is enabled.
      '#attributes' => ['class' => ['js-hide']],
      '#submit' => [[static::class, 'noJsSubmit']],
      // Add a process function to limit this button's validation errors to the
      // triggering element only. We have to do this in #process since until the
      // form API has added the #parents property to the triggering element for
      // us, we don't have any (easy) way to find out where its submitted values
      // will eventually appear in FormStateInterface->getValues().
      '#process' => [
        [static::class, 'addLimitedValidation'],
        ...$this->getElementInfo()->getInfoProperty('submit', '#process', []),
      ],
      // Add an after-build function that inserts a wrapper around the region of
      // the form that needs to be refreshed by AJAX (so that the AJAX system
      // can detect and dynamically update it). This is done in #after_build
      // because it's a convenient place where we have automatic access to the
      // complete form array, but also to minimize the chance that the HTML we
      // add will get clobbered by code that runs after we have added it.
      '#after_build' => [
        ...$this->getElementInfo()->getInfoProperty('submit', '#after_build', []),
        [static::class, 'addAjaxWrapper'],
      ],
    ];
    // Copy #weight and #access from the triggering element to the button so
    // that the two elements will be displayed together.
    foreach (['#weight', '#access'] as $property) {
      if (isset($triggeringElement[$property])) {
        $wrappingElement[$buttonKey][$property] = $triggeringElement[$property];
      }
    }
    // For the easiest integration with the form API and the testing framework,
    // we always give the button a unique #value, rather than playing around
    // with #name. We also cast the #title to string as we will use it as an
    // array key, and it may be a TranslatableMarkup.
    $buttonTitle = !empty($triggeringElement['#title']) ? (string) $triggeringElement['#title'] : $triggerKey;
    if (empty(static::$ajaxTriggerButtons[$buttonTitle])) {
      $wrappingElement[$buttonKey]['#value'] = $this->t('Update "@title" choice', [
        '@title' => $buttonTitle,
      ]);
      static::$ajaxTriggerButtons[$buttonTitle] = 1;
    }
    else {
      $wrappingElement[$buttonKey]['#value'] = $this->t('Update "@title" choice (@number)', [
        '@title' => $buttonTitle,
        '@number' => ++static::$ajaxTriggerButtons[$buttonTitle],
      ]);
    }

    // Attach custom data to the triggering element and submit button, so we can
    // use it in both the process function and AJAX callback.
    $ajaxData = [
      'wrapper' => $triggeringElement['#ajax']['wrapper'],
      'trigger_key' => $triggerKey,
      'refresh_parents' => $refreshParents,
    ];
    $triggeringElement['#views_ui_ajax_data'] = $ajaxData;
    $wrappingElement[$buttonKey]['#views_ui_ajax_data'] = $ajaxData;
  }

  /**
   * Limits validation errors for a non-JavaScript fallback submit button.
   *
   * @param array $element
   *   The form element render array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The current state of the form.
   *
   * @return array
   *   Render array.
   */
  public static function addLimitedValidation(array $element, FormStateInterface $formState): array {
    // Retrieve the AJAX triggering element so we can determine its parents. (We
    // know it's at the same level of the complete form array as the
    // submit-button, so all we have to do to find it is swap out the
    // submit-button's last array parent.)
    $arrayParents = $element['#array_parents'];
    array_pop($arrayParents);
    $arrayParents[] = $element['#views_ui_ajax_data']['trigger_key'];
    $ajaxTriggeringElement = NestedArray::getValue($formState->getCompleteForm(), $arrayParents);

    // Limit this button's validation to the AJAX triggering element, so it can
    // update the form for that change without requiring that the rest of the
    // form be already filled out properly.
    $element['#limit_validation_errors'] = [$ajaxTriggeringElement['#parents']];

    // If we are in the process of a form submission and this is the button that
    // was clicked, the form API workflow in
    // \Drupal::formBuilder()->doBuildForm() will have already copied it to
    // $formState->getTriggeringElement() before our #process function is run.
    // So we need to make the same modifications in $formState as we did to the
    // element itself to ensure that #limit_validation_errors will actually be
    // set in the correct place.
    $clickedButton = &$formState->getTriggeringElement();
    if ($clickedButton && $clickedButton['#name'] == $element['#name'] && $clickedButton['#value'] == $element['#value']) {
      $clickedButton['#limit_validation_errors'] = $element['#limit_validation_errors'];
    }

    return $element;
  }

  /**
   * Adds a wrapper to a form region (for AJAX refreshes) after the build.
   *
   * This function inserts a wrapper around the region of the form that needs to
   * be refreshed by AJAX, based on information stored in the corresponding
   * submit-button form element.
   *
   * @param array $element
   *   The form element render array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The current state of the form.
   *
   * @return array
   *   Render array.
   */
  public static function addAjaxWrapper(array $element, FormStateInterface $formState): array {
    // Find the region of the complete form that needs to be refreshed by AJAX.
    // This was earlier stored in a property on the element.
    $completeForm = &$formState->getCompleteForm();
    $refreshParents = $element['#views_ui_ajax_data']['refresh_parents'];
    $refreshElement = NestedArray::getValue($completeForm, $refreshParents);

    // The HTML ID that AJAX expects was also stored in a property on the
    // element, so use that information to insert the wrapper <div> here.
    $id = $element['#views_ui_ajax_data']['wrapper'];
    $refreshElement += [
      '#prefix' => '',
      '#suffix' => '',
    ];
    $refreshElement['#prefix'] = '<div id="' . $id . '" class="views-ui-ajax-wrapper">' . $refreshElement['#prefix'];
    $refreshElement['#suffix'] .= '</div>';

    // Copy the element that needs to be refreshed back into the form, with our
    // modifications to it.
    NestedArray::setValue($completeForm, $refreshParents, $refreshElement);

    return $element;
  }

  /**
   * Provides a triggering element Ajax callback.
   *
   * @param array $form
   *   The form render array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The current state of the form.
   *
   * @return array
   *   Render array.
   */
  public function ajaxUpdateForm(array $form, FormStateInterface $formState): array {
    // The region that needs to be updated was stored in a property of the
    // triggering element by self::addAjaxTrigger(), so all we have to do is
    // retrieve that here.
    // @see \Drupal\views\ViewsFormAjaxHelperTrait::addAjaxTrigger()
    return NestedArray::getValue($form, $formState->getTriggeringElement()['#views_ui_ajax_data']['refresh_parents']);
  }

  /**
   * Provides a callback for non-JavaScript submit.
   *
   * @param array $form
   *   The form render array.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The current state of the form.
   */
  public static function noJsSubmit(array $form, FormStateInterface $formState): void {
    $formState->setRebuild();
  }

  /**
   * Returns the element info plugin manager.
   *
   * @return \Drupal\Core\Render\ElementInfoManagerInterface
   *   The element info plugin manager.
   */
  protected function getElementInfo(): ElementInfoManagerInterface {
    return \Drupal::service('plugin.manager.element_info');
  }

}
+167 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\views;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;

/**
 * Provides reusable code to be shared by Views forms.
 */
trait ViewsFormHelperTrait {

  use StringTranslationTrait;

  /**
   * Adds an element to select either the default or the current display.
   *
   * @param array $form
   *   The form render array to be altered.
   * @param \Drupal\Core\Form\FormStateInterface $formState
   *   The current state of the form.
   * @param string $section
   *   The section to which the display dropdown belongs.
   */
  protected function standardDisplayDropdown(array &$form, FormStateInterface $formState, string $section): void {
    $view = $formState->get('view');
    $displayId = $formState->get('display_id');
    $executable = $view->getExecutable();
    $displays = $executable->displayHandlers;
    $currentDisplay = $executable->display_handler;

    // @todo Move this to a separate function if it's needed on any forms that
    // don't have the display dropdown.
    $form['override'] = [
      '#prefix' => '<div class="views-override clearfix form--inline views-offset-top" data-drupal-views-offset="top">',
      '#suffix' => '</div>',
      '#weight' => -1000,
      '#tree' => TRUE,
    ];

    // Add the "2 of 3" progress indicator.
    if ($formProgress = $view->getFormProgress()) {
      $arguments = $form['#title']->getArguments() + [
        '@current' => $formProgress['current'],
        '@total' => $formProgress['total'],
      ];
      $form['#title'] = $this->t('Configure @type @current of @total: @item', $arguments);
    }

    // The dropdown should not be added when any of the following is true:
    // - this is the default display.
    // - there is no default shown and just one additional display (mostly
    //   page), and the current display is defaulted.
    if (
      $currentDisplay->isDefaultDisplay()
      || (
        $currentDisplay->isDefaulted($section)
        && !$this->getConfigFactory()->get('views.settings')->get('ui.show.default_display')
        && count($displays) <= 2
      )
    ) {
      return;
    }

    // Determine whether any other displays have overrides for this section.
    $sectionOverrides = FALSE;
    $sectionDefaulted = $currentDisplay->isDefaulted($section);
    foreach ($displays as $id => $display) {
      if ($id === 'default' || $id === $displayId) {
        continue;
      }
      if ($display && !$display->isDefaulted($section)) {
        $sectionOverrides = TRUE;
      }
    }

    $displayDropdown['default'] = ($sectionOverrides ? $this->t('All displays (except overridden)') : $this->t('All displays'));
    $displayDropdown[$displayId] = $this->t('This @display_type (override)', ['@display_type' => $currentDisplay->getPluginId()]);
    // Only display the revert option if we are in an overridden section.
    if (!$sectionDefaulted) {
      $displayDropdown['default_revert'] = $this->t('Revert to default');
    }

    $form['override']['dropdown'] = [
      '#type' => 'select',
      // @todo Translators may need more context than this.
      '#title' => $this->t('For'),
      '#options' => $displayDropdown,
    ];
    if ($currentDisplay->isDefaulted($section)) {
      $form['override']['dropdown']['#default_value'] = 'defaults';
    }
    else {
      $form['override']['dropdown']['#default_value'] = $displayId;
    }
  }

  /**
   * Creates the menu path for a standard AJAX form given the form state.
   *
   * @return \Drupal\Core\Url
   *   The URL object pointing to the form URL.
   */
  protected function buildFormUrl(FormStateInterface $formState): Url {
    $ajax = !$formState->get('ajax') ? 'nojs' : 'ajax';
    $name = $formState->get('view')->id();
    $formKey = $formState->get('form_key');
    $displayId = $formState->get('display_id');

    $formKey = str_replace('-', '_', $formKey);
    $routeName = "views_ui.form_$formKey";
    $routeParameters = [
      'js' => $ajax,
      'view' => $name,
      'display_id' => $displayId,
    ];
    $url = Url::fromRoute($routeName, $routeParameters);
    if ($type = $formState->get('type')) {
      $url->setRouteParameter('type', $type);
    }
    if ($id = $formState->get('id')) {
      $url->setRouteParameter('id', $id);
    }
    return $url;
  }

  /**
   * The #process callback for a button.
   *
   * Determines if a button is the form's triggering element.
   *
   * The Form API has logic to determine the form's triggering element based on
   * the data in POST. However, it only checks buttons based on a single #value
   * per button. This function may be added to a button's #process callbacks to
   * extend button click detection to support multiple #values per button. If
   * the data in POST matches any value in the button's #values array, then the
   * button is detected as having been clicked. This can be used when the value
   * (label) of the same logical button may be different based on context (e.g.,
   * "Apply" vs. "Apply and continue").
   *
   * @see \Drupal\Core\Form\FormBuilder::handleInputElement()
   * @see \Drupal\Core\Form\FormBuilder::buttonWasClicked()
   */
  public static function formButtonWasClicked(array $element, FormStateInterface $formState): array {
    $userInput = $formState->getUserInput();
    $processInput = empty($element['#disabled']) && ($formState->isProgrammed() || ($formState->isProcessingInput() && (!isset($element['#access']) || $element['#access'])));
    if ($processInput && !$formState->getTriggeringElement() && !empty($element['#is_button']) && isset($userInput[$element['#name']]) && isset($element['#values']) && in_array($userInput[$element['#name']], array_map('strval', $element['#values']), TRUE)) {
      $formState->setTriggeringElement($element);
    }
    return $element;
  }

  /**
   * Returns the config factory service.
   *
   * @return \Drupal\Core\Config\ConfigFactoryInterface
   *   The config factory service.
   */
  protected function getConfigFactory(): ConfigFactoryInterface {
    return \Drupal::service('config.factory');
  }

}
Loading