diff --git a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php index 2ca6469923dbf063003eb13ad7c9a1099d2165fc..3a29c7fb5e153bed92e33db361c07c0b0cf6a7bd 100644 --- a/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php +++ b/modules/project_browser_devel/src/Plugin/ProjectBrowserSource/RandomDataPlugin.php @@ -9,7 +9,6 @@ use Drupal\Core\Url; use Drupal\project_browser\Attribute\ProjectBrowserSource; use Drupal\project_browser\Plugin\ProjectBrowserSourceBase; use Drupal\project_browser\ProjectBrowser\Filter\BooleanFilter; -use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -119,14 +118,7 @@ final class RandomDataPlugin extends ProjectBrowserSourceBase { * {@inheritdoc} */ public function getFilterDefinitions(): array { - $filters = []; - - $categories = $this->getCategories(); - $choices = array_combine( - array_column($categories, 'id'), - array_column($categories, 'name'), - ); - $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL); + $filters = parent::getFilterDefinitions(); $filters['security_advisory_coverage'] = new BooleanFilter( TRUE, diff --git a/src/Plugin/DrupalDotOrgSourceBase.php b/src/Plugin/DrupalDotOrgSourceBase.php index 74a362459e5d4ae58fa0a5a6ace6cecde3a8d496..359ec51dc7cbcd08237fb3011ce977140e4f6839 100644 --- a/src/Plugin/DrupalDotOrgSourceBase.php +++ b/src/Plugin/DrupalDotOrgSourceBase.php @@ -13,7 +13,6 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\project_browser\ProjectBrowser\Filter\BooleanFilter; -use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter; use Drupal\project_browser\ProjectBrowser\Project; use GuzzleHttp\ClientInterface; use Psr\Log\LoggerInterface; @@ -209,14 +208,7 @@ abstract class DrupalDotOrgSourceBase extends ProjectBrowserSourceBase implement * {@inheritdoc} */ public function getFilterDefinitions(): array { - $filters = []; - - $categories = $this->getCategories(); - $choices = array_combine( - array_column($categories, 'id'), - array_column($categories, 'name'), - ); - $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL); + $filters = parent::getFilterDefinitions(); $filters['security_advisory_coverage'] = new BooleanFilter( TRUE, diff --git a/src/Plugin/ProjectBrowserSource/Recipes.php b/src/Plugin/ProjectBrowserSource/Recipes.php index f8283a8a3c561209407a58e1edf71c7b6f9ac327..1c0819bb9534adc5aafce7b6e021e903a6ea3736 100644 --- a/src/Plugin/ProjectBrowserSource/Recipes.php +++ b/src/Plugin/ProjectBrowserSource/Recipes.php @@ -17,6 +17,7 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\project_browser\Attribute\ProjectBrowserSource; use Drupal\project_browser\Plugin\ProjectBrowserSourceBase; +use Drupal\project_browser\ProjectBrowser\Filter\TextFilter; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; use Drupal\project_browser\ProjectType; @@ -62,6 +63,15 @@ final class Recipes extends ProjectBrowserSourceBase { ); } + /** + * {@inheritdoc} + */ + public function getFilterDefinitions(): array { + return [ + 'search' => new TextFilter('', $this->t('Search'), NULL), + ]; + } + /** * {@inheritdoc} */ diff --git a/src/Plugin/ProjectBrowserSourceBase.php b/src/Plugin/ProjectBrowserSourceBase.php index 92a9d3b6186617697933859c835a38d71ed5f8b8..c51c6c1fd8b780307192316bf21ff20c9523cebf 100644 --- a/src/Plugin/ProjectBrowserSourceBase.php +++ b/src/Plugin/ProjectBrowserSourceBase.php @@ -6,6 +6,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter; +use Drupal\project_browser\ProjectBrowser\Filter\TextFilter; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; /** @@ -23,7 +24,9 @@ abstract class ProjectBrowserSourceBase extends PluginBase implements ProjectBro * {@inheritdoc} */ public function getFilterDefinitions(): array { - $filters = []; + $filters = [ + 'search' => new TextFilter('', $this->t('Search'), NULL), + ]; $categories = $this->getCategories(); if (is_array($categories)) { diff --git a/src/ProjectBrowser/Filter/FilterBase.php b/src/ProjectBrowser/Filter/FilterBase.php index a84a7ede9ca5397e1369ee463dfb513291411f85..c5a81b4f33fe26ae0092cdd0cc477caf57400533 100644 --- a/src/ProjectBrowser/Filter/FilterBase.php +++ b/src/ProjectBrowser/Filter/FilterBase.php @@ -22,7 +22,8 @@ abstract class FilterBase implements \JsonSerializable { '_type' => match (static::class) { BooleanFilter::class => 'boolean', MultipleChoiceFilter::class => 'multiple_choice', - default => throw new \UnhandledMatchError('Unexpected class ' . static::class), + TextFilter::class => 'text', + default => throw new \UnhandledMatchError("Unknown filter type."), }, ] + get_object_vars($this); diff --git a/src/ProjectBrowser/Filter/TextFilter.php b/src/ProjectBrowser/Filter/TextFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..77b8827954a01ba81a19b82321dcdec581578d8a --- /dev/null +++ b/src/ProjectBrowser/Filter/TextFilter.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser\ProjectBrowser\Filter; + +/** + * Defines a filter that matches some text. + */ +final class TextFilter extends FilterBase { + + public function __construct(public string $value, mixed ...$arguments) { + parent::__construct(...$arguments); + } + +} diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 1f08f0e940735a257cf0e693dc30fba071e420a7..6ab303721ff78d20ecbe605d0f4438ff3090dafb 100644 Binary files a/sveltejs/public/build/bundle.js and b/sveltejs/public/build/bundle.js differ diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index a55a93570d0b0daefc94f1720feb89420cbc00b6..bc7ad9184da97ca23d5fe3bd230bd982a766b9bb 100644 Binary files a/sveltejs/public/build/bundle.js.map and b/sveltejs/public/build/bundle.js.map differ diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte index 46ff0b192918f5a45f7281f3dc6d8ac552d90bfd..77f62ce984900b3df81b8992e528bbc91c62c656 100644 --- a/sveltejs/src/ProjectBrowser.svelte +++ b/sveltejs/src/ProjectBrowser.svelte @@ -145,10 +145,6 @@ await load(); } - async function onSearch(event) { - await onFilterChange(event); - } - async function onToggle(val) { if (val !== toggleView) toggleView = val; preferredView.set(val); @@ -198,7 +194,7 @@ } }); - window.onload = { onSearch }; + window.onload = { onFilterChange }; // Removes initial loader if it exists. const initialLoader = document.getElementById('initial-loader'); if (initialLoader) { @@ -211,7 +207,6 @@ <div slot="head"> <Search bind:this={searchComponent} - on:search={onSearch} on:sort={onSort} on:FilterChange={onFilterChange} {refreshLiveRegion} diff --git a/sveltejs/src/Search/Search.svelte b/sveltejs/src/Search/Search.svelte index e3084894223763878be6a548bb3012cf993e74f4..d233e05addec117b04e8ac86908c3c714cc7183a 100644 --- a/sveltejs/src/Search/Search.svelte +++ b/sveltejs/src/Search/Search.svelte @@ -4,7 +4,7 @@ import BooleanFilter from './BooleanFilter.svelte'; import MultipleChoiceFilter from '../MultipleChoiceFilter.svelte'; import SearchSort from './SearchSort.svelte'; - import { FULL_MODULE_PATH, DARK_COLOR_SCHEME } from '../constants'; + import TextFilter from './TextFilter.svelte'; const { Drupal } = window; const dispatch = createEventDispatcher(); @@ -12,12 +12,6 @@ const filters = getContext('filters'); export let refreshLiveRegion; - export const filter = (row, text) => - Object.values(row).filter( - (item) => - item && item.toString().toLowerCase().indexOf(text.toLowerCase()) > 1, - ).length > 0; - export let index = -1; export let filterDefinitions; export let sorts; @@ -29,14 +23,6 @@ let sortText = sorts[$sort]; let filterComponent; - export async function onSearch() { - dispatch('search', { - filter, - filters: $filters, - index, - }); - refreshLiveRegion(); - } function onFilterChange(event) { // This function might have been called directly when clearing or resetting // the filters, so we can't rely on the presence of an event. @@ -62,29 +48,20 @@ refreshLiveRegion(); } - function clearText() { - $filters.search = ''; - onSearch(); - document.getElementById('pb-text').focus(); - } - /** * Sets all filters to a falsy value. - * - * After this is called, hasFilterValues() will return false. */ function clearFilters() { - $filters.search = ''; Object.entries(filterDefinitions).forEach(([name, definition]) => { const { _type } = definition; - if (_type === 'boolean') { - $filters[name] = false; - } else if (_type === 'multiple_choice') { - $filters[name] = []; - } else { - $filters[name] = null; - } + const falsyValuesByType = { + boolean: false, + multiple_choice: [], + text: '', + }; + $filters[name] = + _type in falsyValuesByType ? falsyValuesByType[_type] : null; }); onFilterChange(); } @@ -102,65 +79,8 @@ } </script> -<form class="search__form-container"> - <div - class="search__bar-container search__form-item js-form-item form-item js-form-type-textfield form-type--textfield" - role="search" - > - <label for="pb-text" class="form-item__label">{Drupal.t('Search')}</label> - <div class="search__search-bar"> - <input - class="search__search_term form-text form-element form-element--type-text" - type="search" - id="pb-text" - name="text" - bind:value={$filters.search} - on:keydown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - onSearch(e); - } - if (e.key === 'Escape') { - e.preventDefault(); - clearText(); - } - }} - /> - {#if $filters.search} - <button - class="search__search-clear" - id="clear-text" - type="button" - on:click={clearText} - aria-label={Drupal.t('Clear search text')} - tabindex="-1" - > - <img - src="{FULL_MODULE_PATH}/images/cross{DARK_COLOR_SCHEME - ? '--dark-color-scheme' - : ''}.svg" - alt="" - /> - </button> - {/if} - <button - class="search__search-submit" - type="button" - on:click={onSearch} - aria-label={Drupal.t('Search')} - > - <img - class="search__search-icon" - id="search-icon" - src="{FULL_MODULE_PATH}/images/search-icon{DARK_COLOR_SCHEME - ? '--dark-color-scheme' - : ''}.svg" - alt="" - /> - </button> - </div> - </div> - {#if Object.keys(filterDefinitions).length !== 0} +{#if Object.keys(filterDefinitions).length !== 0} + <form class="search__form-container"> <div class="search__form-filters-container"> <div class="search__form-filters"> {#each Object.entries(filterDefinitions) as [name, filter]} @@ -178,6 +98,12 @@ on:FilterChange={onFilterChange} bind:this={filterComponent} /> + {:else if filter._type === 'text'} + <TextFilter + {name} + refresh={refreshLiveRegion} + on:FilterChange={onFilterChange} + /> {/if} {/each} </div> @@ -224,5 +150,5 @@ <SearchSort on:sort bind:sortText refresh={refreshLiveRegion} {sorts} /> </div> </div> - {/if} -</form> + </form> +{/if} diff --git a/sveltejs/src/Search/TextFilter.svelte b/sveltejs/src/Search/TextFilter.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9ed9ee52f2e111c98d920e847706223ed40953a2 --- /dev/null +++ b/sveltejs/src/Search/TextFilter.svelte @@ -0,0 +1,80 @@ +<script> + import { createEventDispatcher, getContext } from 'svelte'; + import { DARK_COLOR_SCHEME, FULL_MODULE_PATH } from '../constants'; + + const { Drupal } = window; + const filters = getContext('filters'); + const dispatch = createEventDispatcher(); + export let name; + export let refresh; + function onClick() { + dispatch('FilterChange', { + filters: $filters, + }); + refresh(); + } + + function clearText() { + $filters.search = ''; + onClick(); + document.getElementById('pb-text').focus(); + } +</script> + +<div + class="search__bar-container search__form-item js-form-item form-item js-form-type-textfield form-type--textfield" + role="search" +> + <label for="pb-text" class="form-item__label">{Drupal.t('Search')}</label> + <div class="search__search-bar"> + <input + class="search__search_term form-text form-element form-element--type-text" + type="search" + id="pb-text" + {name} + bind:value={$filters[name]} + on:keydown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + onClick(); + } + if (e.key === 'Escape') { + e.preventDefault(); + clearText(); + } + }} + /> + {#if $filters[name]} + <button + class="search__search-clear" + id="clear-text" + type="button" + on:click={clearText} + aria-label={Drupal.t('Clear search text')} + tabindex="-1" + > + <img + src="{FULL_MODULE_PATH}/images/cross{DARK_COLOR_SCHEME + ? '--dark-color-scheme' + : ''}.svg" + alt="" + /> + </button> + {/if} + <button + class="search__search-submit" + type="button" + on:click={onClick} + aria-label={Drupal.t('Search')} + > + <img + class="search__search-icon" + id="search-icon" + src="{FULL_MODULE_PATH}/images/search-icon{DARK_COLOR_SCHEME + ? '--dark-color-scheme' + : ''}.svg" + alt="" + /> + </button> + </div> +</div> 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 72a77815f651a38b256679d8b02e08601bd1009d..7e167c9dde957db8fdabdc1de8f4f87a49ebbd30 100644 --- a/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php +++ b/tests/modules/project_browser_test/src/Plugin/ProjectBrowserSource/ProjectBrowserTestMock.php @@ -14,7 +14,6 @@ use Drupal\Core\Url; use Drupal\project_browser\Attribute\ProjectBrowserSource; use Drupal\project_browser\Plugin\ProjectBrowserSourceBase; use Drupal\project_browser\ProjectBrowser\Filter\BooleanFilter; -use Drupal\project_browser\ProjectBrowser\Filter\MultipleChoiceFilter; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; use Psr\Log\LoggerInterface; @@ -262,14 +261,7 @@ final class ProjectBrowserTestMock extends ProjectBrowserSourceBase { * {@inheritdoc} */ public function getFilterDefinitions(): array { - $filters = []; - - $categories = $this->getCategories(); - $choices = array_combine( - array_column($categories, 'id'), - array_column($categories, 'name'), - ); - $filters['categories'] = new MultipleChoiceFilter($choices, [], $this->t('Categories'), NULL); + $filters = parent::getFilterDefinitions(); $filters['security_advisory_coverage'] = new BooleanFilter( TRUE, diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php index 8ecae7289e0c8cf20e58c4bded3bda2ae2ff075e..a953c0442dd9cb2f5a97689fa7a5b4915e3c0ba6 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php @@ -337,19 +337,21 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { // Enable module for extra source plugin. $this->container->get('module_installer')->install(['project_browser_devel']); $this->config('project_browser.admin_settings') - ->set('enabled_sources', ['recipes', 'project_browser_test_mock']) + ->set('enabled_sources', ['project_browser_test_mock']) ->save(); - $this->drupalGet('admin/modules/browse/recipes'); - // Recipes doesn't define any filters so no filters are displayed. + // Make the mock source show no filters, and ensure that we never see any. + \Drupal::state()->set('filters_to_define', []); + $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->assertNull($assert_session->waitForElementVisible('css', '.search__form-filters-container')); // 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 = ['maintenance_status', 'security_advisory_coverage']; - \Drupal::state()->set('filters_to_define', $filters_to_define); - - $this->drupalGet('admin/modules/browse/project_browser_test_mock'); + \Drupal::state()->set('filters_to_define', [ + 'maintenance_status', + 'security_advisory_coverage', + ]); + $this->getSession()->reload(); // Drupal.org test mock defines only two filters (actively maintained filter // and security coverage filter). $this->assertElementIsVisible('css', '.search__form-filters-container');