From 217d4d7391ff4570df2a0cd8fba9da1917cd8d3b Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Sun, 21 Jun 2026 10:41:41 +0600 Subject: [PATCH] feat(autosuggest): enable autosuggest in the WordPress admin - Add enable_admin_autosuggest setting to the Autosuggest feature. - Enqueue existing autosuggest assets on admin_enqueue_scripts when the setting is enabled and the user has edit_posts (filterable via ep_admin_autosuggest_capability). - Generate an admin-specific query template using the post statuses that are actually indexed. - Force ElasticPress integration for the template-generation query only by filtering ep_is_integrated_request for search and autosuggest contexts, mirroring the pattern used by Instant Results and WooCommerce Orders Autosuggest. - Refactor shared enqueue/option logic into helper methods so both frontend and admin use the same localization structure. - Add PHPUnit tests for the new setting, admin enqueue gating, capability check, and admin query template. Fixes #3886 --- .../Feature/Autosuggest/Autosuggest.php | 202 ++++++++++++++---- tests/php/features/TestAutosuggest.php | 161 +++++++++++++- 2 files changed, 321 insertions(+), 42 deletions(-) diff --git a/includes/classes/Feature/Autosuggest/Autosuggest.php b/includes/classes/Feature/Autosuggest/Autosuggest.php index 044f11593..5a7ecf336 100644 --- a/includes/classes/Feature/Autosuggest/Autosuggest.php +++ b/includes/classes/Feature/Autosuggest/Autosuggest.php @@ -45,9 +45,10 @@ public function __construct() { $this->requires_install_reindex = true; $this->default_settings = [ - 'endpoint_url' => '', - 'autosuggest_selector' => '', - 'trigger_ga_event' => '0', + 'endpoint_url' => '', + 'autosuggest_selector' => '', + 'trigger_ga_event' => '0', + 'enable_admin_autosuggest' => '0', ]; $this->available_during_installation = true; @@ -80,6 +81,7 @@ public function set_i18n_strings(): void { */ public function setup() { add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] ); add_filter( 'ep_post_mapping', [ $this, 'mapping' ] ); add_filter( 'ep_post_sync_args', [ $this, 'filter_term_suggest' ], 10 ); add_filter( 'ep_post_fuzziness_arg', [ $this, 'set_fuzziness' ], 10, 3 ); @@ -293,21 +295,74 @@ public function enqueue_scripts() { return; } - $host = Utils\get_host(); + $settings = $this->get_settings(); + $endpoint_url = $this->get_autosuggest_endpoint_url(); + + if ( empty( $endpoint_url ) ) { + return; + } + + $this->enqueue_autosuggest_assets(); + + $query = $this->generate_search_query(); + + wp_localize_script( + 'elasticpress-autosuggest', + 'epas', + apply_filters( + 'ep_autosuggest_options', + $this->get_epas_options( $query, $endpoint_url, $settings ) + ) + ); + } + + /** + * Enqueue autosuggest assets for the WordPress admin. + * + * @since 5.4.0 + */ + public function enqueue_admin_scripts() { + if ( Utils\is_indexing() ) { + return; + } + $settings = $this->get_settings(); - if ( defined( 'EP_AUTOSUGGEST_ENDPOINT' ) && EP_AUTOSUGGEST_ENDPOINT ) { - $endpoint_url = EP_AUTOSUGGEST_ENDPOINT; - } elseif ( Utils\is_epio() ) { - $endpoint_url = trailingslashit( $host ) . Indexables::factory()->get( 'post' )->get_index_name() . '/autosuggest'; - } else { - $endpoint_url = $settings['endpoint_url']; + if ( empty( $settings['enable_admin_autosuggest'] ) ) { + return; } + $capability = apply_filters( 'ep_admin_autosuggest_capability', 'edit_posts' ); + if ( ! current_user_can( $capability ) ) { + return; + } + + $endpoint_url = $this->get_autosuggest_endpoint_url(); + if ( empty( $endpoint_url ) ) { return; } + $this->enqueue_autosuggest_assets(); + + $query = $this->generate_admin_search_query(); + + wp_localize_script( + 'elasticpress-autosuggest', + 'epas', + apply_filters( + 'ep_autosuggest_options', + $this->get_epas_options( $query, $endpoint_url, $settings ) + ) + ); + } + + /** + * Enqueue the shared autosuggest script and stylesheet. + * + * @since 5.4.0 + */ + protected function enqueue_autosuggest_assets() { wp_enqueue_script( 'elasticpress-autosuggest', EP_URL . 'dist/js/autosuggest-script.js', @@ -324,15 +379,43 @@ public function enqueue_scripts() { Utils\get_asset_info( 'autosuggest-styles', 'dependencies' ), Utils\get_asset_info( 'autosuggest-styles', 'version' ) ); + } + + /** + * Get the configured autosuggest endpoint URL. + * + * @since 5.4.0 + * @return string + */ + protected function get_autosuggest_endpoint_url() { + if ( defined( 'EP_AUTOSUGGEST_ENDPOINT' ) && EP_AUTOSUGGEST_ENDPOINT ) { + return EP_AUTOSUGGEST_ENDPOINT; + } + + if ( Utils\is_epio() ) { + return trailingslashit( Utils\get_host() ) . Indexables::factory()->get( 'post' )->get_index_name() . '/autosuggest'; + } + + $settings = $this->get_settings(); + return $settings['endpoint_url']; + } + /** + * Build the autosuggest options array to localize to JavaScript. + * + * @since 5.4.0 + * @param array $query Generated query array. + * @param string $endpoint_url Endpoint URL. + * @param array $settings Feature settings. + * @return array + */ + protected function get_epas_options( $query, $endpoint_url, $settings ) { /** Features Class @var Features $features */ $features = Features::factory(); /** Search Feature @var Feature\Search\Search $search */ $search = $features->get_registered_feature( 'search' ); - $query = $this->generate_search_query(); - $epas_options = [ 'query' => $query['body'], 'placeholder' => $query['placeholder'], @@ -380,37 +463,17 @@ public function enqueue_scripts() { $epas_options['highlightingClass'] = apply_filters( 'ep_highlighting_class', 'ep-highlight' ); } - /** - * Output variables to use in Javascript - * index: the Elasticsearch index name - * endpointUrl: the Elasticsearch autosuggest endpoint url - * postType: which post types to use for suggestions - * action: the action to take when selecting an item. Possible values are "search" and "navigate". - */ - wp_localize_script( - 'elasticpress-autosuggest', - 'epas', - /** - * Filter autosuggest JavaScript options - * - * @hook ep_autosuggest_options - * @param {array} $options Autosuggest options to be localized - * @return {array} New options - */ - apply_filters( - 'ep_autosuggest_options', - $epas_options - ) - ); + return $epas_options; } /** * Build a default search request to pass to the autosuggest javascript. * The request will include a placeholder that can then be replaced. * + * @param bool $is_admin Whether the query is being generated for the admin. * @return array Generated ElasticSearch request array( 'placeholder'=> placeholderstring, 'body' => request body ) */ - public function generate_search_query() { + public function generate_search_query( $is_admin = false ) { /** * Filter autosuggest query placeholder @@ -435,12 +498,16 @@ public function generate_search_query() { */ $post_type = apply_filters( 'ep_term_suggest_post_type', array_values( $post_type ) ); - $post_status = get_post_stati( - [ - 'public' => true, - 'exclude_from_search' => false, - ] - ); + if ( $is_admin ) { + $post_status = Indexables::factory()->get( 'post' )->get_indexable_post_status(); + } else { + $post_status = get_post_stati( + [ + 'public' => true, + 'exclude_from_search' => false, + ] + ); + } /** * Filter post statuses available to autosuggest @@ -503,6 +570,52 @@ public function generate_search_query() { ]; } + /** + * Generate an autosuggest query template for the WordPress admin. + * + * Forces ElasticPress integration for the duration of the query so the + * template can be captured from an admin context. + * + * @since 5.4.0 + * @return array Generated query array. + */ + public function generate_admin_search_query() { + $original_user_id = get_current_user_id(); + wp_set_current_user( 0 ); + + add_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request_for_admin_template' ], 10, 2 ); + + $query = $this->generate_search_query( true ); + + remove_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request_for_admin_template' ], 10 ); + + wp_set_current_user( $original_user_id ); + + return $query; + } + + /** + * Enable ElasticPress integration for the admin query template. + * + * Applied as a filter on Utils\is_integrated_request() so the query used to + * generate the admin autosuggest template is integrated regardless of the + * request type. + * + * @since 5.4.0 + * @param bool $is_integrated Whether queries for the request will be integrated. + * @param string $context Context for the original check. + * @return bool + */ + public function is_integrated_request_for_admin_template( $is_integrated, $context ) { + $supported_contexts = [ 'search', 'autosuggest' ]; + + if ( in_array( $context, $supported_contexts, true ) ) { + return true; + } + + return $is_integrated; + } + /** * Ensure we do not fallback to WPDB query for this request * @@ -880,6 +993,13 @@ protected function set_settings_schema() { 'label' => __( 'Trigger Google Analytics events', 'elasticpress' ), 'type' => 'checkbox', ], + [ + 'default' => '0', + 'key' => 'enable_admin_autosuggest', + 'help' => __( 'Adds autosuggest to admin search fields such as the post list table. Requires a publicly accessible autosuggest endpoint or ElasticPress.io.', 'elasticpress' ), + 'label' => __( 'Enable autosuggest in the WordPress admin', 'elasticpress' ), + 'type' => 'checkbox', + ], ]; $this->maybe_add_epio_settings_schema(); diff --git a/tests/php/features/TestAutosuggest.php b/tests/php/features/TestAutosuggest.php index a41e518c4..f82dd8f0f 100644 --- a/tests/php/features/TestAutosuggest.php +++ b/tests/php/features/TestAutosuggest.php @@ -284,11 +284,28 @@ public function test_get_settings_schema() { $settings_keys = wp_list_pluck( $settings_schema, 'key' ); $this->assertSame( - [ 'active', 'autosuggest_selector', 'trigger_ga_event', 'endpoint_url' ], + [ 'active', 'autosuggest_selector', 'trigger_ga_event', 'enable_admin_autosuggest', 'endpoint_url' ], $settings_keys ); } + /** + * Test admin autosuggest setting schema + * + * @since 5.4.0 + * @group autosuggest + */ + public function test_admin_autosuggest_setting_schema() { + $settings_schema = $this->get_feature()->get_settings_schema(); + + $admin_setting = wp_list_filter( $settings_schema, [ 'key' => 'enable_admin_autosuggest' ] ); + $admin_setting = array_values( $admin_setting ); + + $this->assertNotEmpty( $admin_setting ); + $this->assertSame( 'checkbox', $admin_setting[0]['type'] ); + $this->assertSame( '0', $admin_setting[0]['default'] ); + } + /** * Test whether autosuggest ngram fields apply to the search query when AJAX integration and weighting is enabled. * @@ -495,4 +512,146 @@ function ( $path, $index, $type, $query ) { public function test_intercept_remote_request_method_throws_warning() { $this->assertTrue( $this->get_feature()->intercept_remote_request() ); } + + /** + * Reset autosuggest script/style registrations between enqueue tests. + */ + protected function reset_autosuggest_assets() { + wp_dequeue_script( 'elasticpress-autosuggest' ); + wp_deregister_script( 'elasticpress-autosuggest' ); + wp_dequeue_style( 'elasticpress-autosuggest' ); + wp_deregister_style( 'elasticpress-autosuggest' ); + } + + /** + * Test admin autosuggest is not enqueued by default. + * + * @since 5.4.0 + * @group autosuggest + */ + public function test_admin_autosuggest_default_off() { + set_current_screen( 'edit.php' ); + $this->reset_autosuggest_assets(); + + $this->get_feature()->enqueue_admin_scripts(); + + $this->assertFalse( wp_script_is( 'elasticpress-autosuggest' ) ); + $this->assertFalse( wp_style_is( 'elasticpress-autosuggest' ) ); + } + + /** + * Test admin autosuggest enqueue respects the setting and endpoint. + * + * @since 5.4.0 + * @group autosuggest + */ + public function test_admin_autosuggest_enqueue_respects_setting() { + set_current_screen( 'edit.php' ); + $this->reset_autosuggest_assets(); + + $filter = function () { + return [ + 'autosuggest' => [ + 'endpoint_url' => 'http://example.com', + 'enable_admin_autosuggest' => '1', + ], + ]; + }; + + add_filter( 'pre_site_option_ep_feature_settings', $filter ); + add_filter( 'pre_option_ep_feature_settings', $filter ); + + $this->get_feature()->enqueue_admin_scripts(); + + $this->assertTrue( wp_script_is( 'elasticpress-autosuggest' ) ); + $this->assertTrue( wp_style_is( 'elasticpress-autosuggest' ) ); + } + + /** + * Test admin autosuggest enqueue without endpoint does not load assets. + * + * @since 5.4.0 + * @group autosuggest + */ + public function test_admin_autosuggest_enqueue_without_endpoint() { + set_current_screen( 'edit.php' ); + $this->reset_autosuggest_assets(); + + $filter = function () { + return [ + 'autosuggest' => [ + 'enable_admin_autosuggest' => '1', + ], + ]; + }; + + add_filter( 'pre_site_option_ep_feature_settings', $filter ); + add_filter( 'pre_option_ep_feature_settings', $filter ); + + $this->get_feature()->enqueue_admin_scripts(); + + $this->assertFalse( wp_script_is( 'elasticpress-autosuggest' ) ); + $this->assertFalse( wp_style_is( 'elasticpress-autosuggest' ) ); + } + + /** + * Test admin autosuggest enqueue respects capability. + * + * @since 5.4.0 + * @group autosuggest + */ + public function test_admin_autosuggest_enqueue_respects_capability() { + set_current_screen( 'edit.php' ); + $this->reset_autosuggest_assets(); + + $subscriber_id = $this->factory->user->create( [ 'role' => 'subscriber' ] ); + wp_set_current_user( $subscriber_id ); + + $filter = function () { + return [ + 'autosuggest' => [ + 'endpoint_url' => 'http://example.com', + 'enable_admin_autosuggest' => '1', + ], + ]; + }; + + add_filter( 'pre_site_option_ep_feature_settings', $filter ); + add_filter( 'pre_option_ep_feature_settings', $filter ); + + $this->get_feature()->enqueue_admin_scripts(); + + $this->assertFalse( wp_script_is( 'elasticpress-autosuggest' ) ); + $this->assertFalse( wp_style_is( 'elasticpress-autosuggest' ) ); + } + + /** + * Test the admin query template integration override. + * + * @since 5.4.0 + * @group autosuggest + */ + public function test_is_integrated_request_for_admin_template() { + $feature = $this->get_feature(); + + $this->assertTrue( $feature->is_integrated_request_for_admin_template( false, 'search' ) ); + $this->assertTrue( $feature->is_integrated_request_for_admin_template( false, 'autosuggest' ) ); + $this->assertFalse( $feature->is_integrated_request_for_admin_template( false, 'some_other_context' ) ); + $this->assertTrue( $feature->is_integrated_request_for_admin_template( true, 'some_other_context' ) ); + } + + /** + * Test the admin search query template. + * + * @since 5.4.0 + * @group autosuggest + */ + public function test_generate_admin_search_query() { + $feature = $this->get_feature(); + $query = $feature->generate_admin_search_query(); + + $this->assertArrayHasKey( 'body', $query ); + $this->assertArrayHasKey( 'placeholder', $query ); + $this->assertContains( 'ep_autosuggest_placeholder', $query ); + } }