From 8055d95fc140961cf5e636e86029fe3f8c71a384 Mon Sep 17 00:00:00 2001
From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org>
Date: Mon, 27 Jan 2025 21:55:10 +0000
Subject: [PATCH] Issue #3502612 by phenaproxima, lostcarpark: Normalize
 treatment of filters on the front end

---
 .../ProjectBrowserSource/RandomDataPlugin.php |   6 +-
 .../DrupalDotOrgJsonApi.php                   |   6 +-
 sveltejs/public/build/bundle.js               | Bin 298510 -> 297852 bytes
 sveltejs/public/build/bundle.js.map           | Bin 272414 -> 272084 bytes
 sveltejs/src/ProjectBrowser.svelte            |  32 +------
 sveltejs/src/Search/BooleanFilter.svelte      |  10 +-
 sveltejs/src/Search/Search.svelte             |  90 +++++++++++++-----
 .../ProjectBrowserTestMock.php                |   6 +-
 .../ProjectBrowserPluginTest.php              |  10 +-
 .../ProjectBrowserUiTest.php                  |   8 +-
 .../ProjectBrowserUiTestJsonApi.php           |  14 +--
 .../Nightwatch/Tests/consistentPagination.js  |  12 ++-
 12 files changed, 109 insertions(+), 85 deletions(-)

diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
index 7b4a9cc65..e673b6159 100644
--- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
+++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php
@@ -130,21 +130,21 @@ final class RandomDataPlugin extends ProjectBrowserSourceBase {
     );
     $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL);
 
-    $filters['securityCoverage'] = new BooleanFilter(
+    $filters['security_advisory_coverage'] = new BooleanFilter(
       TRUE,
       $this->t('Show projects covered by a security policy'),
       $this->t('Show all'),
       $this->t('Security advisory coverage'),
       NULL,
     );
-    $filters['maintenanceStatus'] = new BooleanFilter(
+    $filters['maintenance_status'] = new BooleanFilter(
       TRUE,
       $this->t('Show actively maintained projects'),
       $this->t('Show all'),
       $this->t('Maintenance status'),
       NULL,
     );
-    $filters['developmentStatus'] = new BooleanFilter(
+    $filters['development_status'] = new BooleanFilter(
       FALSE,
       $this->t('Show projects under active development'),
       $this->t('Show all'),
diff --git a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
index c08960d57..7db69d4d3 100644
--- a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
+++ b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php
@@ -245,21 +245,21 @@ final class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase {
     );
     $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL);
 
-    $filters['securityCoverage'] = new BooleanFilter(
+    $filters['security_advisory_coverage'] = new BooleanFilter(
       TRUE,
       $this->t('Show projects covered by a security policy'),
       $this->t('Show all'),
       $this->t('Security advisory coverage'),
       NULL,
     );
-    $filters['maintenanceStatus'] = new BooleanFilter(
+    $filters['maintenance_status'] = new BooleanFilter(
       TRUE,
       $this->t('Show actively maintained projects'),
       $this->t('Show all'),
       $this->t('Maintenance status'),
       NULL,
     );
-    $filters['developmentStatus'] = new BooleanFilter(
+    $filters['development_status'] = new BooleanFilter(
       FALSE,
       $this->t('Show projects under active development'),
       $this->t('Show all'),
diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index bc859d827ffe464d75d3ff368bc30e0908766fd2..5cfe970d1bd668fb2a2e64351ac7a2cc9a8df694 100644
GIT binary patch
delta 2635
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

delta 2863
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

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 7efeb3c7162edf6e7a6e9f71ca18a1465f4acdd6..d12b193f55d82ab6fae62b40b29726e517643db7 100644
GIT binary patch
delta 3030
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

delta 3532
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

diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index 0847dd26c..a7fa549c8 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -71,36 +71,14 @@
    */
   async function load(_page) {
     loading = true;
-    const searchParams = new URLSearchParams({
-      page: _page,
-      limit: $pageSize,
-      sort: $sort,
-      source: DEFAULT_SOURCE_ID,
-    });
+    const searchParams = searchComponent.toSearchParams();
+    searchParams.set('page', _page);
+    searchParams.set('limit', $pageSize);
+    searchParams.set('sort', $sort);
+    searchParams.set('source', DEFAULT_SOURCE_ID);
     if (searchText) {
       searchParams.set('search', searchText);
     }
-    if ($filters.categories && $filters.categories.length) {
-      searchParams.set('categories', $filters.categories);
-    }
-    if ('developmentStatus' in $filters) {
-      searchParams.set(
-        'development_status',
-        Number($filters.developmentStatus).toString(),
-      );
-    }
-    if ('maintenanceStatus' in $filters) {
-      searchParams.set(
-        'maintenance_status',
-        Number($filters.maintenanceStatus).toString(),
-      );
-    }
-    if ('securityCoverage' in $filters) {
-      searchParams.set(
-        'security_advisory_coverage',
-        Number($filters.securityCoverage).toString(),
-      );
-    }
 
     const url = `${BASE_URL}project-browser/data/project?${searchParams.toString()}`;
 
diff --git a/sveltejs/src/Search/BooleanFilter.svelte b/sveltejs/src/Search/BooleanFilter.svelte
index 8cd94aee4..15c2cb66c 100644
--- a/sveltejs/src/Search/BooleanFilter.svelte
+++ b/sveltejs/src/Search/BooleanFilter.svelte
@@ -3,18 +3,18 @@
 
   export let definition;
 
-  const { name, on_label: onLabel, off_label: offLabel } = definition;
+  const { name: title, on_label: onLabel, off_label: offLabel } = definition;
 
   export let changeHandler;
-  export let type;
+  export let name;
 </script>
 
 <div class="filter-group__filter-options form-item">
-  <label for={type} class="form-item__label">{name}</label>
+  <label for={name} class="form-item__label">{title}</label>
   <select
-    name={type}
+    {name}
     class="search__filter-select form-select form-element form-element--type-select"
-    bind:value={$filters[type]}
+    bind:value={$filters[name]}
     on:change={changeHandler}
   >
     <option value={true}>{onLabel}</option>
diff --git a/sveltejs/src/Search/Search.svelte b/sveltejs/src/Search/Search.svelte
index c94700997..b7f127b5d 100644
--- a/sveltejs/src/Search/Search.svelte
+++ b/sveltejs/src/Search/Search.svelte
@@ -33,6 +33,22 @@
 
   export let filterDefinitions;
 
+  export function toSearchParams() {
+    const params = new URLSearchParams();
+
+    Object.entries(filterDefinitions).forEach(([name, definition]) => {
+      const { _type } = definition;
+      let value = $filters[name];
+
+      if (_type === 'boolean') {
+        // Convert to '1' or '0'.
+        value = Number(value).toString();
+      }
+      params.set(name, value);
+    });
+    return params;
+  }
+
   export async function onSearch(event) {
     const state = stateContext.getState();
     const detail = {
@@ -107,32 +123,58 @@
     document.getElementById('pb-text').focus();
   }
 
+  function refresh() {
+    onAdvancedFilter();
+    onSelectCategory();
+  }
+
   /**
-   * Resets the filters to the initial values provided by the source.
+   * Detects if there are any filters applied at all.
+   */
+  function hasFilterValues() {
+    return Object.keys(filterDefinitions).some((name) => $filters[name]);
+  }
+
+  /**
+   * Sets all filters to a falsy value.
    *
-   * @param {boolean} clear
-   *   Whether to clear all filter values (i.e., not reset them to their defaults,
-   *   but actually negate them all).
+   * After this is called, hasFilterValues() will return false.
    */
-  const resetFilters = (clear) => {
-    $filters = {};
+  function clearFilters() {
     Object.entries(filterDefinitions).forEach(([name, definition]) => {
-      let value;
-      if (clear) {
-        if (definition._type === 'boolean') {
-          value = false;
-        } else if (definition._type === 'multiple_choice') {
-          value = [];
-        }
+      const { _type } = definition;
+
+      if (_type === 'boolean') {
+        $filters[name] = false;
+      } else if (_type === 'multiple_choice') {
+        $filters[name] = [];
       } else {
-        value = definition.value;
+        $filters[name] = null;
       }
-      $filters[name] = value;
     });
-    $filters.categories = [];
-    onAdvancedFilter();
-    onSelectCategory();
-  };
+    refresh();
+  }
+
+  /**
+   * Detects if the user changed any filter values from their defaults).
+   */
+  function hasUserAppliedFilters() {
+    return Object.entries(filterDefinitions).some(
+      ([name, definition]) => $filters[name] !== definition.value,
+    );
+  }
+
+  /**
+   * Resets the filters to the initial values provided by the source.
+   *
+   * After calling this, hasUserAppliedFilters() will return false.
+   */
+  function resetFilters() {
+    Object.entries(filterDefinitions).forEach(([name, definition]) => {
+      $filters[name] = definition.value;
+    });
+    refresh();
+  }
 </script>
 
 <form class="search__form-container">
@@ -196,11 +238,11 @@
   {#if Object.keys(filterDefinitions).length !== 0}
     <div class="search__form-filters-container">
       <div class="search__form-filters">
-        {#each Object.entries(filterDefinitions) as [filterType, filter]}
+        {#each Object.entries(filterDefinitions) as [name, filter]}
           {#if filter._type === 'boolean'}
             <BooleanFilter
               definition={filter}
-              type={filterType}
+              {name}
               changeHandler={onAdvancedFilter}
             />
           {:else if filter._type === 'multiple_choice'}
@@ -236,16 +278,16 @@
               {/each}
             {/if}
 
-            {#if $filters.securityCoverage || $filters.maintenanceStatus || $filters.developmentStatus || ('categories' in $filters && $filters.categories.length)}
+            {#if hasFilterValues()}
               <button
                 class="search__filter-button"
                 type="button"
-                on:click|preventDefault={() => resetFilters(true)}
+                on:click|preventDefault={() => clearFilters()}
               >
                 {Drupal.t('Clear filters')}
               </button>
             {/if}
-            {#if !($filters.maintenanceStatus && $filters.securityCoverage && !$filters.developmentStatus && 'categories' in $filters && $filters.categories.length === 0)}
+            {#if hasUserAppliedFilters()}
               <button
                 class="search__filter-button"
                 type="button"
diff --git a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
index 248149b42..5064cf694 100644
--- a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
+++ b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php
@@ -314,21 +314,21 @@ final class ProjectBrowserTestMock extends ProjectBrowserSourceBase {
     );
     $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL);
 
-    $filters['securityCoverage'] = new BooleanFilter(
+    $filters['security_advisory_coverage'] = new BooleanFilter(
       TRUE,
       $this->t('Show projects covered by a security policy'),
       $this->t('Show all'),
       $this->t('Security advisory coverage'),
       NULL,
     );
-    $filters['maintenanceStatus'] = new BooleanFilter(
+    $filters['maintenance_status'] = new BooleanFilter(
       TRUE,
       $this->t('Show actively maintained projects'),
       $this->t('Show all'),
       $this->t('Maintenance status'),
       NULL,
     );
-    $filters['developmentStatus'] = new BooleanFilter(
+    $filters['development_status'] = new BooleanFilter(
       FALSE,
       $this->t('Show projects under active development'),
       $this->t('Show all'),
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php
index fbbc5ebba..6976e05ff 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php
@@ -16,9 +16,9 @@ class ProjectBrowserPluginTest extends WebDriverTestBase {
   use ProjectBrowserUiTestTrait;
 
   // Could be moved into trait under PHP 8.3.
-  protected const SECURITY_OPTION_SELECTOR = 'select[name="securityCoverage"] ';
-  protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenanceStatus"] ';
-  protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="developmentStatus"] ';
+  protected const SECURITY_OPTION_SELECTOR = 'select[name="security_advisory_coverage"] ';
+  protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenance_status"] ';
+  protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="development_status"] ';
   protected const OPTION_CHECKED = 'option:checked';
   protected const OPTION_FIRST_CHILD = 'option:first-child';
   protected const OPTION_LAST_CHILD = 'option:last-child';
@@ -109,9 +109,9 @@ class ProjectBrowserPluginTest extends WebDriverTestBase {
 
     $page = $this->getSession()->getPage();
     // Clear the security covered filter.
-    $page->selectFieldOption('securityCoverage', 'Show all');
+    $page->selectFieldOption('security_advisory_coverage', 'Show all');
     // Set the development status filter.
-    $page->selectFieldOption('developmentStatus', 'Show projects under active development');
+    $page->selectFieldOption('development_status', 'Show projects under active development');
 
     // Clear all filters.
     $this->pressWithWait('Clear filters');
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
index c55a1c41e..d8b23783a 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
@@ -28,9 +28,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
   use ProjectBrowserUiTestTrait;
 
   // Could be moved into trait under PHP 8.3.
-  protected const SECURITY_OPTION_SELECTOR = 'select[name="securityCoverage"] ';
-  protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenanceStatus"] ';
-  protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="developmentStatus"] ';
+  protected const SECURITY_OPTION_SELECTOR = 'select[name="security_advisory_coverage"] ';
+  protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenance_status"] ';
+  protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="development_status"] ';
   protected const OPTION_CHECKED = 'option:checked';
   protected const OPTION_FIRST_CHILD = 'option:first-child';
   protected const OPTION_LAST_CHILD = 'option:last-child';
@@ -391,7 +391,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     }
 
     // Click the Active filter.
-    $page->selectFieldOption('developmentStatus', 'Show projects under active development');
+    $page->selectFieldOption('development_status', 'Show projects under active development');
 
     $this->assertProjectsVisible([
       'Jazz',
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
index 7154f5b6c..fea509ddd 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
@@ -19,9 +19,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase {
   use ProjectBrowserUiTestTrait;
 
   // Could be moved into trait under PHP 8.3.
-  protected const SECURITY_OPTION_SELECTOR = 'select[name="securityCoverage"] ';
-  protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenanceStatus"] ';
-  protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="developmentStatus"] ';
+  protected const SECURITY_OPTION_SELECTOR = 'select[name="security_advisory_coverage"] ';
+  protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenance_status"] ';
+  protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="development_status"] ';
   protected const OPTION_CHECKED = 'option:checked';
   protected const OPTION_FIRST_CHILD = 'option:first-child';
   protected const OPTION_LAST_CHILD = 'option:last-child';
@@ -241,7 +241,7 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase {
     $assert_session->pageTextNotContains(' 0 Results');
 
     // Make sure the second filter applied is the security covered filter.
-    $this->assertTrue($assert_session->optionExists('securityCoverage', 'Show projects covered by a security policy')->isSelected());
+    $this->assertTrue($assert_session->optionExists('security_advisory_coverage', 'Show projects covered by a security policy')->isSelected());
 
     // Clear the security covered filter.
     $this->clickWithWait(self::SECURITY_OPTION_SELECTOR . self::OPTION_LAST_CHILD);
@@ -320,9 +320,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase {
     $this->pressWithWait('Recommended filters');
 
     // Check that the actively maintained tag is present.
-    $this->assertTrue($assert_session->optionExists('maintenanceStatus', 'Show actively maintained projects')->isSelected());
+    $this->assertTrue($assert_session->optionExists('maintenance_status', 'Show actively maintained projects')->isSelected());
     // Make sure the second filter applied is the security covered filter.
-    $this->assertTrue($assert_session->optionExists('securityCoverage', 'Show projects covered by a security policy')->isSelected());
+    $this->assertTrue($assert_session->optionExists('security_advisory_coverage', 'Show projects covered by a security policy')->isSelected());
     $this->assertTrue($assert_session->waitForText(' Results'));
     $assert_session->pageTextNotContains(' 0 Results');
   }
@@ -347,7 +347,7 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase {
 
     // Set the names of filters which will be defined by the test mock.
     // @see \Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock::getFilterDefinitions()
-    $filters_to_define = ['maintenanceStatus', 'securityCoverage'];
+    $filters_to_define = ['maintenance_status', 'security_advisory_coverage'];
     \Drupal::state()->set('filters_to_define', $filters_to_define);
 
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
diff --git a/tests/src/Nightwatch/Tests/consistentPagination.js b/tests/src/Nightwatch/Tests/consistentPagination.js
index ae5b707f7..c4ac5de11 100644
--- a/tests/src/Nightwatch/Tests/consistentPagination.js
+++ b/tests/src/Nightwatch/Tests/consistentPagination.js
@@ -12,8 +12,10 @@ module.exports = {
         .drupalRelativeURL('/admin/modules/browse/project_browser_test_mock')
         .waitForElementVisible('h1', 100)
         .assert.textContains('h1', 'Browse projects')
-        .click('select[name="securityCoverage"] option[value="false"]')
-        .click('select[name="maintenanceStatus"] option[value="false"]')
+        .click(
+          'select[name="security_advisory_coverage"] option[value="false"]',
+        )
+        .click('select[name="maintenance_status"] option[value="false"]')
         .assert.visible('select.pagination__num-projects')
         .click('select.pagination__num-projects option[value="24"]');
 
@@ -22,8 +24,10 @@ module.exports = {
         .drupalRelativeURL('/admin/modules/browse/project_browser_test_mock')
         .waitForElementVisible('h1', 100)
         .assert.textContains('h1', 'Browse projects')
-        .click('select[name="securityCoverage"] option[value="false"]')
-        .click('select[name="maintenanceStatus"] option[value="false"]')
+        .click(
+          'select[name="security_advisory_coverage"] option[value="false"]',
+        )
+        .click('select[name="maintenance_status"] option[value="false"]')
         .assert.visible('select.pagination__num-projects')
         .getValue('select.pagination__num-projects', function (result) {
           this.assert.strictEqual(
-- 
GitLab