Verified Commit b1eddcbb authored by Lauri Timmanee's avatar Lauri Timmanee
Browse files

Issue #229193 by dmitrig01, narendraR, katbailey, k4v, Gurpartap Singh,...

Issue #229193 by dmitrig01, narendraR, katbailey, k4v, Gurpartap Singh, anavarre, kkaefer, chx, Gábor Hojtsy, jrockowitz, Bojhan, catch, Kiphaas7, corey.aufang, webchick, Dries, Senpai, smustgrave, anders.fajerson, larowlan: Incremental filter for permissions page
parent 9b62b23a
Loading
Loading
Loading
Loading
+22 −1
Original line number Diff line number Diff line
@@ -142,6 +142,27 @@ public function buildForm(array $form, FormStateInterface $form_state) {
      '#type' => 'system_compact_link',
    ];

    $form['filters'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => ['table-filter', 'js-show'],
      ],
    ];

    $form['filters']['text'] = [
      '#type' => 'search',
      '#title' => $this->t('Filter permissions'),
      '#title_display' => 'invisible',
      '#size' => 30,
      '#placeholder' => $this->t('Filter by permission name'),
      '#description' => $this->t('Enter permission name'),
      '#attributes' => [
        'class' => ['table-filter-text'],
        'data-table' => '#permissions',
        'autocomplete' => 'off',
      ],
    ];

    $form['permissions'] = [
      '#type' => 'table',
      '#header' => [$this->t('Permission')],
@@ -177,7 +198,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
        ];
        $form['permissions'][$perm]['description'] = [
          '#type' => 'inline_template',
          '#template' => '<div class="permission"><span class="title">{{ title }}</span>{% if description or warning %}<div class="description">{% if warning %}<em class="permission-warning">{{ warning }}</em> {% endif %}{{ description }}</div>{% endif %}</div>',
          '#template' => '<div class="permission"><span class="title table-filter-text-source">{{ title }}</span>{% if description or warning %}<div class="description">{% if warning %}<em class="permission-warning">{{ warning }}</em> {% endif %}{{ description }}</div>{% endif %}</div>',
          '#context' => [
            'title' => $perm_item['title'],
          ],
+96 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\user\FunctionalJavascript;

use Drupal\FunctionalJavascriptTests\WebDriverTestBase;

/**
 * Tests the JavaScript functionality of the permission filter.
 *
 * @group user
 */
class PermissionFilterTest extends WebDriverTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = ['user', 'system'];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $admin_user = $this->drupalCreateUser([
      'administer permissions',
    ]);
    $this->drupalLogin($admin_user);
  }

  /**
   * Tests that filter results announcement has correct pluralization.
   */
  public function testPermissionFilter() {
    // Find the permission filter field.
    $this->drupalGet('admin/people/permissions');
    $assertSession = $this->assertSession();
    $session = $this->getSession();
    $page = $session->getPage();

    $filter = $page->findField('edit-text');

    // Get all permission rows, for assertions later.
    $permission_rows = $page->findAll('css', 'tbody tr td .permission');

    // Administer filter reduces the number of visible rows.
    $filter->setValue('Administer');
    $session->wait(1000, "jQuery('tr[data-drupal-selector=\"edit-permissions-access-content\"]').length == 0");
    $visible_rows = $this->filterVisibleElements($permission_rows);
    // Test Drupal.announce() message when multiple matches are expected.
    $expected_message = count($visible_rows) . ' permissions are available in the modified list.';
    $assertSession->elementTextContains('css', '#drupal-live-announce', $expected_message);
    self::assertGreaterThan(count($visible_rows), count($permission_rows));
    self::assertGreaterThan(1, count($visible_rows));

    // Test Drupal.announce() message when one match is expected.
    // Using a very specific permission name, we expect only one row.
    $filter->setValue('Administer site configuration');
    $session->wait(1000, "jQuery('tr[data-drupal-selector=\"edit-permissions-access-content\"]').length == 0");
    $visible_rows = $this->filterVisibleElements($permission_rows);
    self::assertEquals(1, count($visible_rows));
    $expected_message = '1 permission is available in the modified list.';
    $assertSession->elementTextContains('css', '#drupal-live-announce', $expected_message);

    // Test Drupal.announce() message when no matches are expected.
    $filter->setValue('Pan-Galactic Gargle Blaster');
    $session->wait(1000, "jQuery('tr[data-drupal-selector=\"edit-permissions-access-content\"]').length == 0");
    $visible_rows = $this->filterVisibleElements($permission_rows);
    self::assertEquals(0, count($visible_rows));

    $expected_message = '0 permissions are available in the modified list.';
    $assertSession->elementTextContains('css', '#drupal-live-announce', $expected_message);
  }

  /**
   * Removes any non-visible elements from the passed array.
   *
   * @param \Behat\Mink\Element\NodeElement[] $elements
   *   An array of node elements.
   *
   * @return \Behat\Mink\Element\NodeElement[]
   *   An array of node elements.
   */
  protected function filterVisibleElements(array $elements): array {
    $elements = array_filter($elements, function ($element) {
      return $element->isVisible();
    });
    return $elements;
  }

}
+2 −0
Original line number Diff line number Diff line
@@ -28,6 +28,8 @@ drupal.user.permissions:
    - core/drupalSettings
    - user/drupal.user.admin
    - core/drupal.checkbox
    - core/drupal.debounce
    - core/drupal.announce

drupal.user.icons:
  version: VERSION
+114 −2
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@
 * User permission page behaviors.
 */

(function ($, Drupal) {
(function ($, Drupal, debounce) {
  /**
   * Shows checked and disabled checkboxes for inherited permissions.
   *
@@ -89,4 +89,116 @@
      });
    },
  };
})(jQuery, Drupal);

  /**
   * Filters the permission list table by a text input search string.
   *
   * Text search input: input.table-filter-text
   * Target table:      input.table-filter-text[data-table]
   * Source text:       .table-filter-text-source
   *
   * @type {Drupal~behavior}
   */
  Drupal.behaviors.tableFilterByText = {
    attach(context, settings) {
      const [input] = once('table-filter-text', 'input.table-filter-text');
      if (!input) {
        return;
      }
      const tableSelector = input.getAttribute('data-table');
      const $table = $(tableSelector);
      const $rows = $table.find('tbody tr');

      function hideEmptyPermissionHeader(index, row) {
        const tdsWithModuleClass = row.querySelectorAll('td.module');
        // Function to check if an element is visible (`display: block`).
        function isVisible(element) {
          return getComputedStyle(element).display !== 'none';
        }
        if (tdsWithModuleClass.length > 0) {
          // Find the next visible sibling `<tr>`.
          let nextVisibleSibling = row.nextElementSibling;
          while (nextVisibleSibling && !isVisible(nextVisibleSibling)) {
            nextVisibleSibling = nextVisibleSibling.nextElementSibling;
          }

          // Check if the next visible sibling has the "module" class in any of
          // its `<td>` elements.
          let nextVisibleSiblingHasModuleClass = false;
          if (nextVisibleSibling) {
            const nextSiblingTdsWithModuleClass =
              nextVisibleSibling.querySelectorAll('td.module');
            nextVisibleSiblingHasModuleClass =
              nextSiblingTdsWithModuleClass.length > 0;
          }

          // Check if this is the last visible row with class "module".
          const isLastVisibleModuleRow =
            !nextVisibleSibling || !isVisible(nextVisibleSibling);

          // Hide the current row with class "module" if it meets the
          // conditions.
          $(row).toggle(
            !nextVisibleSiblingHasModuleClass && !isLastVisibleModuleRow,
          );
        }
      }

      function filterPermissionList(e) {
        const query = e.target.value;
        if (query.length === 0) {
          // Reset table when the textbox is cleared.
          $rows.show();
        }
        // Case insensitive expression to find query at the beginning of a word.
        const re = new RegExp(`\\b${query}`, 'i');

        function showPermissionRow(index, row) {
          const sources = row.querySelectorAll('.table-filter-text-source');
          if (sources.length > 0) {
            const textMatch = sources[0].textContent.search(re) !== -1;
            $(row).closest('tr').toggle(textMatch);
          }
        }
        // Search over all rows.
        $rows.show();

        // Filter if the length of the query is at least 2 characters.
        if (query.length >= 2) {
          $rows.each(showPermissionRow);

          // Hide the empty header if they don't have any visible rows.
          const visibleRows = $table.find('tbody tr:visible');
          visibleRows.each(hideEmptyPermissionHeader);
          const rowsWithoutEmptyModuleName = $table.find('tbody tr:visible');
          // Find elements with class "permission" within visible rows.
          const tdsWithModuleOrPermissionClass =
            rowsWithoutEmptyModuleName.find('.permission');

          Drupal.announce(
            Drupal.formatPlural(
              tdsWithModuleOrPermissionClass.length,
              '1 permission is available in the modified list.',
              '@count permissions are available in the modified list.',
            ),
          );
        }
      }

      function preventEnterKey(event) {
        if (event.which === 13) {
          event.preventDefault();
          event.stopPropagation();
        }
      }

      if ($table.length) {
        $(input).on({
          keyup: debounce(filterPermissionList, 200),
          click: debounce(filterPermissionList, 200),
          keydown: preventEnterKey,
        });
      }
    },
  };
})(jQuery, Drupal, Drupal.debounce);
+13 −0
Original line number Diff line number Diff line
@@ -1770,3 +1770,16 @@ function claro_system_module_invoked_theme_registry_alter(array &$theme_registry
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for the user_admin_permissions form.
 */
function claro_form_user_admin_permissions_alter(&$form, FormStateInterface $form_state) {
  if (isset($form['filters'])) {
    $form['filters']['#attributes']['class'][] = 'permissions-table-filter';
    if (isset($form['filters']['text'])) {
      unset($form['filters']['text']['#title_display']);
      $form['filters']['text']['#title'] = t('Filter');
    }
  }
}
Loading