diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f218a8774a1f8a3a79df086a0d05d70028dc3485..52eb20a850166478498f7399f36de3ca1a6ac5d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,6 +62,8 @@ default: URL: 'git@git.drupal.org:project/drupal_cms_page.git' - DIR: recipes/drupal_cms_privacy_basic URL: 'git@git.drupal.org:project/drupal_cms_privacy_basic.git' + - DIR: recipes/drupal_cms_search + URL: 'git@git.drupal.org:project/drupal_cms_search.git' - DIR: recipes/drupal_cms_seo_basic URL: 'git@git.drupal.org:project/drupal_cms_seo_basic.git' - DIR: recipes/drupal_cms_seo_tools diff --git a/components.composer.json b/components.composer.json index d719d09b09c2c85acc096d3f56f3ab5a37781cd2..a2ac5605293cc2c80a1e9ae232e03f51523b7267 100644 --- a/components.composer.json +++ b/components.composer.json @@ -64,6 +64,10 @@ "type": "path", "url": "recipes/drupal_cms_privacy_basic" }, + "search": { + "type": "path", + "url": "recipes/drupal_cms_search" + }, "seo_basic": { "type": "path", "url": "recipes/drupal_cms_seo_basic" diff --git a/recipes/drupal_cms_search/composer.json b/recipes/drupal_cms_search/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..054bdebdc86aaf98dfdbd72cb833b7406ee89db1 --- /dev/null +++ b/recipes/drupal_cms_search/composer.json @@ -0,0 +1,23 @@ +{ + "name": "drupal/drupal_cms_search", + "type": "drupal-recipe", + "description": "Provides a simple database-powered search, with autocomplete support.", + "require": { + "drupal/core": ">=10.4", + "drupal/search_api": "1.35", + "drupal/search_api_autocomplete": "^1.9", + "drupal/search_api_exclude": "^2.0", + "drupal/simple_search_form" : "^1.5" + }, + "extra": { + "patches": { + "drupal/search_api": { + "#3483366: Allow to set the same view mode for all entity bundles in Search API plugins": "https://www.drupal.org/files/issues/2024-11-15/3483366-global-view-modes_0.patch" + }, + "drupal/core": { + "#3483353: EntityDisplayBase::createCopy() naïvely assumes that the duplicate doesn't already exist": "https://www.drupal.org/files/issues/2024-11-15/3483353-create-copy-entity-display-config-action.patch" + } + } + }, + "version": "dev-main" +} diff --git a/recipes/drupal_cms_search/config/search_api.index.content.yml b/recipes/drupal_cms_search/config/search_api.index.content.yml new file mode 100644 index 0000000000000000000000000000000000000000..96ec576fbac18d65d8907d675660a5847b38a8da --- /dev/null +++ b/recipes/drupal_cms_search/config/search_api.index.content.yml @@ -0,0 +1,127 @@ +status: true +dependencies: + config: + - search_api.server.database + - core.entity_view_mode.node.search_index + module: + - node + - search_api + - search_api_exclude +id: content +name: 'Content' +description: 'Can search within all content types.' +read_only: false +field_settings: + rendered_item: + label: 'Rendered HTML output' + property_path: rendered_item + type: text + configuration: + roles: + - anonymous + view_mode: + 'entity:node': + ':default': search_index + title: + label: Title + datasource_id: 'entity:node' + property_path: title + type: text + boost: 3.0 + dependencies: + module: + - node + type: + label: 'Content type' + datasource_id: 'entity:node' + property_path: type + type: string + dependencies: + module: + - node +datasource_settings: + 'entity:node': + bundles: + default: true + selected: { } + languages: + default: true + selected: { } +processor_settings: + add_url: { } + aggregated_field: { } + custom_value: { } + entity_status: { } + entity_type: { } + highlight: + weights: + postprocess_query: 0 + prefix: '<strong>' + suffix: '</strong>' + excerpt: true + excerpt_always: false + excerpt_length: 256 + exclude_fields: { } + highlight: always + highlight_partial: true + html_filter: + weights: + preprocess_index: -15 + preprocess_query: -15 + all_fields: false + fields: + - rendered_item + - title + - type + title: true + alt: true + tags: + b: 2 + h1: 5 + h2: 3 + h3: 2 + strong: 2 + ignorecase: + weights: + preprocess_index: -20 + preprocess_query: -20 + all_fields: false + fields: + - rendered_item + - title + - type + language_with_fallback: { } + node_exclude: { } + rendered_item: { } + reverse_entity_references: { } + stemmer: + weights: + preprocess_index: 0 + preprocess_query: 0 + all_fields: false + fields: + - rendered_item + - title + exceptions: + mexican: mexic + texan: texa + tokenizer: + weights: + preprocess_index: -6 + preprocess_query: -6 + all_fields: false + fields: + - rendered_item + - title + spaces: '' + ignored: ._- + overlap_cjk: 1 + minimum_word_size: '3' +tracker_settings: + default: + indexing_order: lifo +options: + cron_limit: 10 + index_directly: true + track_changes_in_references: true +server: database diff --git a/recipes/drupal_cms_search/config/search_api.server.database.yml b/recipes/drupal_cms_search/config/search_api.server.database.yml new file mode 100644 index 0000000000000000000000000000000000000000..487de597e9ce44cc95ab86c5f2ad379c2eabdbf0 --- /dev/null +++ b/recipes/drupal_cms_search/config/search_api.server.database.yml @@ -0,0 +1,13 @@ +status: true +dependencies: + module: + - search_api_db +id: database +name: 'Database' +description: 'Uses database tables for indexing content.' +backend: search_api_db +backend_config: + database: 'default:default' + min_chars: 3 + matching: partial + phrase: bigram diff --git a/recipes/drupal_cms_search/config/search_api_autocomplete.search.content_autocomplete.yml b/recipes/drupal_cms_search/config/search_api_autocomplete.search.content_autocomplete.yml new file mode 100644 index 0000000000000000000000000000000000000000..14fada9a7dc00edd9cca1c1c4c2eebc3db094e38 --- /dev/null +++ b/recipes/drupal_cms_search/config/search_api_autocomplete.search.content_autocomplete.yml @@ -0,0 +1,34 @@ +status: true +dependencies: + config: + - search_api.index.content + - views.view.search + module: + - views + - search_api_autocomplete +id: content_autocomplete +label: Content Autocomplete +index_id: content +suggester_settings: + live_results: + fields: { } + highlight: + enabled: false + field: '' + suggest_keys: false + view_modes: { } +suggester_weights: + live_results: 0 +suggester_limits: { } +search_settings: + 'views:search': + displays: + default: true + selected: { } +options: + limit: 10 + min_length: 1 + show_count: false + delay: null + submit_button_selector: ':submit' + autosubmit: true diff --git a/recipes/drupal_cms_search/config/views.view.search.yml b/recipes/drupal_cms_search/config/views.view.search.yml new file mode 100644 index 0000000000000000000000000000000000000000..34191dd11c44d3b6102e2e4f0b1d1fa2ac5b63f2 --- /dev/null +++ b/recipes/drupal_cms_search/config/views.view.search.yml @@ -0,0 +1,228 @@ +status: true +dependencies: + config: + - search_api.index.content + module: + - search_api + - user +id: search +label: Search +module: views +description: 'Displays search results.' +tag: '' +base_table: search_api_index_content +base_field: search_api_id +display: + default: + id: default + display_title: Default + display_plugin: default + position: 0 + display_options: + title: Search + fields: + rendered_item: + id: rendered_item + table: search_api_index_content + field: rendered_item + relationship: none + group_type: group + admin_label: '' + entity_type: null + entity_field: null + plugin_id: search_api + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + link_to_item: false + use_highlighting: false + multi_type: separator + multi_separator: ', ' + pager: + type: mini + options: + offset: 0 + pagination_heading_level: h4 + items_per_page: 10 + total_pages: null + id: 0 + tags: + next: ›› + previous: ‹‹ + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + exposed_form: + type: basic + options: + submit_button: Find + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: perm + options: + perm: 'access content' + cache: + type: search_api_tag + options: { } + empty: { } + sorts: + search_api_relevance: + id: search_api_relevance + table: search_api_index_content + field: search_api_relevance + relationship: none + group_type: group + admin_label: '' + plugin_id: search_api + order: DESC + expose: + label: '' + field_identifier: '' + exposed: false + arguments: { } + filters: + search_api_fulltext: + id: search_api_fulltext + table: search_api_index_content + field: search_api_fulltext + relationship: none + group_type: group + admin_label: '' + plugin_id: search_api_fulltext + operator: or + value: '' + group: 1 + exposed: true + expose: + operator_id: search_api_fulltext_op + label: '' + description: '' + use_operator: false + operator: search_api_fulltext_op + operator_limit_selection: false + operator_list: { } + identifier: text + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + content_editor: '0' + administrator: '0' + expose_fields: false + placeholder: 'Enter search keywords' + searched_fields_id: search_api_fulltext_searched_fields + value_maxlength: 128 + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + parse_mode: terms + min_length: null + fields: { } + style: + type: default + row: + type: search_api + options: + view_modes: + 'entity:node': + ':default': search_result + query: + type: search_api_query + options: + bypass_access: false + skip_access: false + preserve_facet_query_args: false + query_tags: { } + relationships: { } + header: { } + footer: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: + - 'config:search_api.index.content' + - 'search_api_list:content' + page_1: + id: page_1 + display_title: Page + display_plugin: page + position: 1 + display_options: + display_extenders: { } + path: search + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: + - 'config:search_api.index.content' + - 'search_api_list:content' diff --git a/recipes/drupal_cms_search/recipe.yml b/recipes/drupal_cms_search/recipe.yml new file mode 100644 index 0000000000000000000000000000000000000000..4a1993b69b9109a3d535efef7d7b4f153edf7368 --- /dev/null +++ b/recipes/drupal_cms_search/recipe.yml @@ -0,0 +1,86 @@ +name: Search +type: Drupal CMS +description: Provides a simple database-powered search, with autocomplete support. + +install: + - block + # Basic search is for node entity type only at the moment. + - node + - search_api + - search_api_autocomplete + - search_api_db + - search_api_exclude + - simple_search_form + - views + +config: + strict: false + import: + # Use core node view modes for indexing and displaying results. + node: + - core.entity_view_mode.node.search_index + - core.entity_view_mode.node.search_result + actions: + # Enable Search API Exclude for all content types. + node.type.*: + setThirdPartySetting: + module: search_api_exclude + key: enabled + value: 1 + createForEachIfNotExists: + # Create view display "Search result highlighting input" for all available content types. + core.entity_view_display.node.%bundle.search_result: + status: true + targetEntityType: node + mode: search_result + bundle: '%bundle' + # Create view display "Search Index" for all available content types. + core.entity_view_display.node.*.default: + # Clone the "Default" view display by default. + createCopy: + mode: search_index + use_existing: true + # Make sure view display "Search result highlighting input" contains field "Search result excerpt". + core.entity_view_display.node.*.search_result: + setComponent: + name: search_api_excerpt + options: + weight: 0 + region: content + # Add the search form to the header of the page. + block.block.search_form: + placeBlockInDefaultTheme: + id: search_form + # The region accepts an array keyed by theme name. + region: + olivero: primary_menu + drupal_cms_olivero: primary_menu + # A fallback used if no match found in the region array. + default_region: content + # Place the block before any blocks already in the region. + position: first + plugin: simple_search_form_block + settings: + label: 'Search form' + label_display: '0' + provider: simple_search_form + action_path: /search + get_parameter: text + input_type: search_api_autocomplete + search_api_autocomplete: + search_id: content_autocomplete + display: page_1 + arguments: '' + filter: text + input_label: Search + input_label_display: invisible + input_placeholder: 'Enter search keywords' + input_css_classes: '' + submit_display: true + submit_label: Find + input_keep_value: false + preserve_url_query_parameters: { } + user.role.anonymous: + grantPermission: 'use search_api_autocomplete for content_autocomplete' + user.role.authenticated: + grantPermission: 'use search_api_autocomplete for content_autocomplete' diff --git a/recipes/drupal_cms_search/tests/src/Functional/ComponentValidationTest.php b/recipes/drupal_cms_search/tests/src/Functional/ComponentValidationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..865cceea8d7a2c4579d165578afc4f3f393135f9 --- /dev/null +++ b/recipes/drupal_cms_search/tests/src/Functional/ComponentValidationTest.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\drupal_cms_search\Functional; + +use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait; +use Drupal\Tests\BrowserTestBase; + +/** + * @group drupal_cms_search + */ +class ComponentValidationTest extends BrowserTestBase { + + use RecipeTestTrait; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected static $modules = ['block', 'node']; + + public function test(): void { + // The PlaceBlock config action has a core bug, where it doesn't account + // for the possibility of there being no blocks in a region. As a + // workaround, prevent that from happening by placing a useless block into + // the content region. + $this->drupalPlaceBlock('system_powered_by_block'); + + $dir = realpath(__DIR__ . '/../../..'); + + // The recipe should apply cleanly. + $this->applyRecipe($dir); + // Apply it again to prove that it is idempotent. + $this->applyRecipe($dir); + } + +} diff --git a/recipes/drupal_cms_starter/composer.json b/recipes/drupal_cms_starter/composer.json index 1bd8c8094dde9d7be09307930608ec761269a6e5..cb571598d3ab5f63c122e2471da3beb546bb4014 100644 --- a/recipes/drupal_cms_starter/composer.json +++ b/recipes/drupal_cms_starter/composer.json @@ -14,6 +14,7 @@ "drupal/drupal_cms_media_tools": "*", "drupal/drupal_cms_olivero": "*", "drupal/drupal_cms_privacy_basic": "*", + "drupal/drupal_cms_search": "*", "drupal/drupal_cms_seo_basic": "*", "drupal/drupal_cms_dashboard": "*", "drupal/easy_email_express": "1.0.1",