Commit c3cbdd8e authored by Tim Plunkett's avatar Tim Plunkett
Browse files

Issue #3299029 by narendraR, tim.plunkett, fjgarlin, bnjmnm: Control order of enabled plugins

parent d6d1e77c
Loading
Loading
Loading
Loading
+145 −0
Original line number Diff line number Diff line
/**
 * @file
 * Tabledrag behaviors.
 *
 * This code is borrowed from block's JavaScript.
 *
 * @see core/modules/block/js/block.es6.js
 */

(function ($, window, Drupal, once) {
  /**
   * Enable/Disable a Plugin in the table via select list.
   *
   * This behavior is dependent on the tableDrag behavior, since it uses the
   * objects initialized in that behavior to update the row.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Attaches the tableDrag behavior plugin settings form.
   */
  Drupal.behaviors.projectBrowserPluginSourceDrag = {
    attach(context, settings) {
      // Only proceed if tableDrag is present and we are on the settings page.
      if (!Drupal.tableDrag  || !Drupal.tableDrag.project_browser) {
        return;
      }

      /**
       * Function to update the last placed row with the correct classes.
       *
       * @param {jQuery} table
       *   The jQuery object representing the table to inspect.
       * @param {jQuery} rowObject
       *   The jQuery object representing the table row.
       */
      function updateLastPlaced(table, rowObject) {
        const $rowObject = $(rowObject);
        if (!$rowObject.is('.drag-previous')) {
          table.find('.drag-previous').removeClass('drag-previous');
          $rowObject.addClass('drag-previous');
        }
      }

      /**
       * Update source plugin weights in the given region.
       *
       * @param {jQuery} table
       *   Table with draggable items.
       * @param {string} region
       *   Machine name of region containing source plugin to update.
       */
      function updateSourcePluginWeights(table, region) {
        // Calculate minimum weight.
        let weight = -Math.round(table.find('.draggable').length / 2);
        // Update the source plugin weights.
        table
          .find(`.status-title-${region}`)
          .nextUntil('.status-title')
          .find('select.source-weight')
          .val(
            // Increment the weight before assigning it to prevent using the
            // absolute minimum available weight. This way we always have an
            // unused upper and lower bound, which makes manually setting the
            // weights easier for users who prefer to do it that way.
            () => ++weight,
          );
      }

      const table = $('#project_browser');
      // Get the tableDrag object.
      const tableDrag = Drupal.tableDrag.project_browser;
      // Add a handler for when a row is swapped.
      tableDrag.row.prototype.onSwap = function (swappedRow) {
        updateLastPlaced(table, this);
      };

      // Add a handler so when a row is dropped, update fields dropped into
      // new regions.
      tableDrag.onDrop = function () {
        const dragObject = this;
        const $rowElement = $(dragObject.rowObject.element);
        const regionRow = $rowElement.prevAll('tr.status-title').get(0);
        const regionName = regionRow.classList[1].replace('status-title-', '');
        const regionField = $rowElement.find('select.source-status-select');
        // Update region and weight fields if the region has been changed.
        if (!regionField.is(`.source-status-${regionName}`)) {
          const weightField = $rowElement.find('select.source-weight');
          const oldRegionName = weightField[0].className.replace(
            /([^ ]+[ ]+)*source-weight-([^ ]+)([ ]+[^ ]+)*/,
            '$2',
          );
          regionField
            .removeClass(`source-status-${oldRegionName}`)
            .addClass(`source-status-${regionName}`);
          weightField
            .removeClass(`source-weight-${oldRegionName}`)
            .addClass(`source-weight-${regionName}`);
          regionField.val(regionName);
        }

        updateSourcePluginWeights(table, regionName);
      };

      // Add the behavior to each region select list.
      $(once('source-status-select', 'select.source-status-select', context)).on(
        'change',
        function (event) {
          // Make our new row and select field.
          const row = $(this).closest('tr');
          const select = $(this);
          // Find the correct region and insert the row as the last in the
          // region.
          tableDrag.rowObject = new tableDrag.row(row[0]);
          const regionMessage = table.find(
            `.status-title-${select[0].value}`,
          );
          const regionItems = regionMessage.nextUntil(
            '.status-title',
          );
          if (regionItems.length) {
            regionItems.last().after(row);
          }
          // We found that regionMessage is the last row.
          else {
            regionMessage.after(row);
          }
          updateSourcePluginWeights(table, select[0].value);
          // Update last placed source plugin indication.
          updateLastPlaced(table, row);
          // Show unsaved changes warning.
          if (!tableDrag.changed) {
            $(Drupal.theme('tableDragChangedWarning'))
              .insertBefore(tableDrag.table)
              .hide()
              .fadeIn('slow');
            tableDrag.changed = true;
          }
          // Remove focus from selectbox.
          select.trigger('blur');
        },
      );
    },
  };
})(jQuery, window, Drupal, once);
+8 −0
Original line number Diff line number Diff line
@@ -12,3 +12,11 @@ svelte:
    - core/drupal
    - core/drupal.debounce
    - core/drupal.dialog

tabledrag:
  js:
    js/project_browser.admin.js: {}
  dependencies:
    - core/jquery
    - core/drupal
    - core/once
+120 −12
Original line number Diff line number Diff line
@@ -69,6 +69,20 @@ class SettingsForm extends ConfigFormBase {
    return ['project_browser.admin_settings'];
  }

  /**
   * Returns an array containing the table headers.
   *
   * @return array
   *   The table header.
   */
  protected function getTableHeader() {
    return [
      $this->t('Source'),
      $this->t('Status'),
      $this->t('Weight'),
    ];
  }

  /**
   * {@inheritdoc}
   */
@@ -82,27 +96,121 @@ class SettingsForm extends ConfigFormBase {
      ];
    }

    $types = [];
    foreach ($source_plugins as $source) {
      $types[$source['id']] = (string) $source['label'];
    $enabled_sources = $config->get('enabled_sources');
    // Sort the source plugins by the order they're stored in config.
    $sorted_arr = array_merge(array_flip($enabled_sources), $source_plugins);

    $source_plugins = array_merge($sorted_arr, $source_plugins);

    $weight_delta = round(count($source_plugins) / 2);
    $table = [
      '#type' => 'table',
      '#header' => $this->getTableHeader(),
      '#attributes' => [
        'id' => 'project_browser',
      ],
    ];
    $options = [
      'enabled' => $this->t('Enabled'),
      'disabled' => $this->t('Disabled'),
    ];
    foreach ($options as $status => $title) {
      $table['#tabledrag'][] = [
        'action' => 'match',
        'relationship' => 'sibling',
        'group' => 'source-status-select',
        'subgroup' => 'source-status-' . $status,
        'hidden' => FALSE,
      ];
      $table['#tabledrag'][] = [
        'action' => 'order',
        'relationship' => 'sibling',
        'group' => 'source-weight',
        'subgroup' => 'source-weight-' . $status,
      ];
      $table['status-' . $status] = [
        '#attributes' => [
          'class' => ['status-title', 'status-title-' . $status],
          'no_striping' => TRUE,
        ],
      ];
      $table['status-' . $status]['title'] = [
        '#plain_text' => $title,
        '#wrapper_attributes' => [
          'colspan' => 3,
        ],
      ];

      // Plugin rows.
      foreach ($source_plugins as $plugin_name => $plugin_definition) {
        // Only include plugins in their respective section.
        if (($status === 'enabled') !== in_array($plugin_name, $enabled_sources, TRUE)) {
          continue;
        }
    $form['enabled_sources'] = [
      '#type' => 'checkboxes',
      '#title' => $this->t('Enabled source plugins for Project Browser'),
      '#default_value' => $config->get('enabled_sources') ?: [],
      '#options' => $types,
      '#description' => $this->t('Check to select source plugin for Project Browser. At least one checkbox needs to be checked.'),

        $label = (string) $plugin_definition['label'];
        $plugin_key_exists = array_search($plugin_name, $enabled_sources);
        $table[$plugin_name] = [
          '#attributes' => [
            'class' => [
              'draggable',
            ],
          ],
        ];
        $table[$plugin_name]['source'] = [
          '#plain_text' => $label,
          '#wrapper_attributes' => [
            'id' => 'source--' . $plugin_name,
          ],
        ];

        $table[$plugin_name]['status'] = [
          '#type' => 'select',
          '#default_value' => $status,
          '#required' => TRUE,
          '#title' => $this->t('Status for @source source', ['@source' => $label]),
          '#title_display' => 'invisible',
          '#options' => $options,
          '#attributes' => [
            'class' => ['source-status-select', 'source-status-' . $status],
          ],
        ];
        $table[$plugin_name]['weight'] = [
          '#type' => 'weight',
          '#default_value' => ($plugin_key_exists === FALSE) ? 0 : $plugin_key_exists,
          '#delta' => $weight_delta,
          '#title' => $this->t('Weight for @source source', ['@source' => $label]),
          '#title_display' => 'invisible',
          '#attributes' => [
            'class' => ['source-weight', 'source-weight-' . $status],
          ],
        ];
      }
    }

    $form['enabled_sources'] = $table;
    $form['#attached']['library'][] = 'project_browser/tabledrag';
    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    $all_plugins = $form_state->getValue('enabled_sources');
    if (!array_key_exists('enabled', array_count_values(array_column($all_plugins, 'status')))) {
      $form_state->setErrorByName('enabled_sources', $this->t('At least one source plugin must be enabled.'));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $all_plugins = $form_state->getValue('enabled_sources');
    $enabled_plugins = array_filter($all_plugins, fn($source) => $source['status'] === 'enabled');
    $this->config('project_browser.admin_settings')
      ->set('enabled_sources', array_filter(array_values($form_state->getValue('enabled_sources'))))
      ->set('enabled_sources', array_keys($enabled_plugins))
      ->save();
    $this->cacheBin->deleteAll();
    parent::submitForm($form, $form_state);
+2 −6
Original line number Diff line number Diff line
@@ -35,12 +35,8 @@ class ProjectBrowserPluginTest extends WebDriverTestBase {
      'administer modules',
      'administer site configuration',
    ]));
    // Update configuration, disable drupalorg_mockapi source.
    $this->drupalGet('admin/config/development/project_browser');
    $edit = [
      'enabled_sources[drupalorg_mockapi]' => FALSE,
    ];
    $this->submitForm($edit, 'Save configuration');
    // Update configuration, enable only random_data source.
    $this->config('project_browser.admin_settings')->set('enabled_sources', ['random_data'])->save(TRUE);
  }

  /**
+60 −6
Original line number Diff line number Diff line
@@ -633,7 +633,6 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
   * Tests recommended filters.
   */
  public function testRecommendedFilter(): void {
    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();
    // Clear filters.
    $this->drupalGet('admin/modules/browse');
@@ -659,13 +658,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    $assert_session->pageTextContains('At least two source plugins are required to configure this feature.');
    // Enable module for extra source plugin.
    $this->container->get('module_installer')->install(['project_browser_devel'], TRUE);
    // Update configuration, enable multiple sources.
    // Configuration updated when Project Browser Devel module enabled above.
    $this->drupalGet('admin/config/development/project_browser');
    $assert_session->pageTextNotContains('At least two source plugins are required to configure this feature.');
    $edit = [
      'enabled_sources[drupalorg_mockapi]' => TRUE,
    ];
    $this->submitForm($edit, 'Save configuration');
    // Test categories with multiple plugin enabled.
    $this->drupalGet('admin/modules/browse');
    $this->svelteInitHelper('css', '.pb-categories input[type="checkbox"]');
@@ -684,4 +679,63 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
    $this->assertNotEquals('9 Results Sorted by Project Usage', $this->getElementText('.grid--1 output'));
  }

  /**
   * Tests tabledrag on configuration page.
   */
  public function testTabledrag(): void {
    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();
    $this->container->get('module_installer')->install(['project_browser_devel'], TRUE);

    $this->drupalGet('admin/modules/browse');
    $assert_session->waitForElementVisible('css', '#project-browser .project');
    // Count tabs.
    $tab_count = $page->findAll('css', '.plugin-tabs button');
    $this->assertCount(2, $tab_count);

    // Verify that Drupal.org mockapi is first tab.
    $first_tab = $page->find('css', '.plugin-tabs button:nth-child(1)');
    $this->assertEquals('drupalorg_mockapi', $first_tab->getValue());

    // Re-order plugins.
    $this->drupalGet('admin/config/development/project_browser');
    $first_plugin = $page->find('css', '#source--drupalorg_mockapi');
    $second_plugin = $page->find('css', '#source--random_data');
    $first_plugin->find('css', '.handle')->dragTo($second_plugin);
    $assert_session->assertWaitOnAjaxRequest();
    $this->submitForm([], 'Save');

    // Verify that Random data is first tab.
    $this->drupalGet('admin/modules/browse');
    $assert_session->waitForElementVisible('css', '#project-browser .project');
    $first_tab = $page->find('css', '.plugin-tabs button:nth-child(1)');
    $this->assertEquals('random_data', $first_tab->getValue());

    // Disable Drupal.org mockapi plugin.
    $this->drupalGet('admin/config/development/project_browser');
    $enabled_row = $page->find('css', '#source--drupalorg_mockapi');
    $disabled_region_row = $page->find('css', '.status-title-disabled');
    $enabled_row->find('css', '.handle')->dragTo($disabled_region_row);
    $assert_session->assertWaitOnAjaxRequest();
    $this->submitForm([], 'Save');
    $assert_session->pageTextContains('The configuration options have been saved.');

    // Verify that only Random data plugin is enabled.
    $this->drupalGet('admin/modules/browse');
    $this->svelteInitHelper('css', '.pb-categories input[type="checkbox"]');
    $assert_session->elementsCount('css', '.pb-categories input[type="checkbox"]', 20);

    // Enable only Drupal.org mockapi plugin through config update.
    // It is done this way because dragging was not working reliably for enabling Drupal.org mockapi plugin.
    $this->config('project_browser.admin_settings')->set('enabled_sources', ['drupalorg_mockapi'])->save(TRUE);
    $this->drupalGet('admin/config/development/project_browser');
    $this->assertTrue($assert_session->optionExists('edit-enabled-sources-drupalorg-mockapi-status', 'enabled')->isSelected());
    $this->assertTrue($assert_session->optionExists('edit-enabled-sources-random-data-status', 'disabled')->isSelected());

    // Verify that only Drupal.org mockapi plugin is enabled.
    $this->drupalGet('admin/modules/browse');
    $this->svelteInitHelper('css', '.pb-categories input[type="checkbox"]');
    $assert_session->elementsCount('css', '.pb-categories input[type="checkbox"]', 54);
  }

}