diff --git a/assets/js/api-search/index.js b/assets/js/api-search/index.js
index 87d610e013..77f6d3a801 100644
--- a/assets/js/api-search/index.js
+++ b/assets/js/api-search/index.js
@@ -12,6 +12,7 @@ import {
WPElement,
} from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
+import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies.
@@ -316,7 +317,18 @@ export const ApiSearchProvider = ({
return;
}
- setResults(response);
+ /**
+ * Filter the search results response.
+ *
+ * @filter ep.InstantResults.response
+ * @since 5.3.0
+ *
+ * @param {object} response The search results response.
+ * @returns {object} Filtered search results response.
+ */
+ const filteredResponse = applyFilters('ep.InstantResults.response', response);
+
+ setResults(filteredResponse || response);
} catch (e) {
const errorMessage = sprintf(
/* translators: Error message */
diff --git a/assets/js/instant-results/components/facets/facet.js b/assets/js/instant-results/components/facets/facet.js
index 8c3a40f395..43299e1876 100644
--- a/assets/js/instant-results/components/facets/facet.js
+++ b/assets/js/instant-results/components/facets/facet.js
@@ -2,6 +2,7 @@
* WordPress dependencies.
*/
import { WPElement } from '@wordpress/element';
+import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies.
@@ -24,16 +25,36 @@ import TaxonomyTermsFacet from './taxonomy-terms-facet';
export default ({ index, label, name, postTypes, type }) => {
const defaultIsOpen = index < 2;
+ /**
+ * Filter the facet label.
+ *
+ * @filter ep.InstantResults.filter.label
+ * @since 5.3.0
+ *
+ * @param {string} label Facet label.
+ * @param {string} name Facet name.
+ * @param {string} type Facet type.
+ * @param {string[]} postTypes Facet post types.
+ * @returns {string} Filtered facet label.
+ */
+ const filteredLabel = applyFilters(
+ 'ep.InstantResults.filter.label',
+ label,
+ name,
+ type,
+ postTypes,
+ );
+
switch (type) {
case 'post_type':
- return ;
+ return ;
case 'price_range':
- return ;
+ return ;
case 'taxonomy':
return (
diff --git a/assets/js/instant-results/components/facets/post-type-facet.js b/assets/js/instant-results/components/facets/post-type-facet.js
index a04f714120..978659da52 100644
--- a/assets/js/instant-results/components/facets/post-type-facet.js
+++ b/assets/js/instant-results/components/facets/post-type-facet.js
@@ -3,6 +3,7 @@
*/
import { useCallback, useMemo, WPElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
+import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies.
@@ -61,7 +62,20 @@ export default ({ defaultIsOpen, label }) => {
/**
* Reduce buckets to options.
*/
- const options = useMemo(() => buckets.reduce(reduceOptions, []), [buckets, reduceOptions]);
+ const options = useMemo(() => {
+ const reducedOptions = buckets.reduce(reduceOptions, []);
+
+ /**
+ * Filter the post type filter options.
+ *
+ * @filter ep.InstantResults.filter.postType.options
+ * @since 5.3.0
+ *
+ * @param {object[]} options Post type filter options.
+ * @returns {object[]} Filtered post type filter options.
+ */
+ return applyFilters('ep.InstantResults.filter.postType.options', reducedOptions);
+ }, [buckets, reduceOptions]);
/**
* Handle checkbox change event.
diff --git a/assets/js/instant-results/components/facets/price-range-facet.js b/assets/js/instant-results/components/facets/price-range-facet.js
index aed619c2fd..dd9efb5306 100644
--- a/assets/js/instant-results/components/facets/price-range-facet.js
+++ b/assets/js/instant-results/components/facets/price-range-facet.js
@@ -3,6 +3,7 @@
*/
import { useLayoutEffect, useState, WPElement } from '@wordpress/element';
import { _x, sprintf } from '@wordpress/i18n';
+import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies.
@@ -38,8 +39,23 @@ export default ({ defaultIsOpen, label }) => {
/**
* Minimum and maximum possible values.
*/
- const max = Math.ceil(maxAgg);
- const min = Math.floor(minAgg);
+ let max = Math.ceil(maxAgg);
+ let min = Math.floor(minAgg);
+
+ /**
+ * Filter the price range values.
+ *
+ * @filter ep.InstantResults.filter.priceRange.options
+ * @since 5.3.0
+ *
+ * @param {object} range Price range values.
+ * @param {number} range.min Minimum price.
+ * @param {number} range.max Maximum price.
+ * @returns {object} Filtered price range values.
+ */
+ const range = applyFilters('ep.InstantResults.filter.priceRange.options', { min, max });
+
+ ({ min, max } = range);
/**
* Current minimum and maximum values.
diff --git a/assets/js/instant-results/components/results/did-you-mean.js b/assets/js/instant-results/components/results/did-you-mean.js
index 4197af7fe1..ff15185752 100644
--- a/assets/js/instant-results/components/results/did-you-mean.js
+++ b/assets/js/instant-results/components/results/did-you-mean.js
@@ -3,6 +3,7 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import { WPElement, createInterpolateElement } from '@wordpress/element';
+import { applyFilters } from '@wordpress/hooks';
import { showSuggestions, suggestionsBehavior } from '../../config';
@@ -22,7 +23,7 @@ export default (props) => {
// Get other terms by excluding the first term from suggestedTerms
const otherTerms = suggestedTerms.slice(1);
- return (
+ const didYouMean = (
<>
{showSuggestions && suggestedTerms && suggestedTerms?.[0]?.text && (
@@ -67,4 +68,16 @@ export default (props) => {
)}
>
);
+
+ /**
+ * Filter the did you mean component.
+ *
+ * @filter ep.InstantResults.component.didYouMean
+ * @since 5.3.0
+ *
+ * @param {WPElement} didYouMean Did you mean component.
+ * @param {object} props Props.
+ * @returns {WPElement} Did you mean component.
+ */
+ return applyFilters('ep.InstantResults.component.didYouMean', didYouMean, props);
};
diff --git a/assets/js/instant-results/components/tools/sort.js b/assets/js/instant-results/components/tools/sort.js
index 22d1add8ee..a455d9e3a5 100644
--- a/assets/js/instant-results/components/tools/sort.js
+++ b/assets/js/instant-results/components/tools/sort.js
@@ -3,6 +3,7 @@
*/
import { useMemo, WPElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
+import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies.
@@ -41,7 +42,7 @@ export default () => {
search({ orderby, order });
};
- return (
+ const sort = (
);
+
+ /**
+ * Filter the sort component.
+ *
+ * @filter ep.InstantResults.component.sort
+ * @since 5.3.0
+ *
+ * @param {WPElement} sort Sort component.
+ * @param {object} context Sort context.
+ * @param {string} context.orderby Current orderby value.
+ * @param {string} context.order Current order value.
+ * @returns {WPElement} Sort component.
+ */
+ return applyFilters('ep.InstantResults.component.sort', sort, { orderby, order });
};
diff --git a/includes/classes/Feature/InstantResults/InstantResults.php b/includes/classes/Feature/InstantResults/InstantResults.php
index ebf5985964..177ee18621 100644
--- a/includes/classes/Feature/InstantResults/InstantResults.php
+++ b/includes/classes/Feature/InstantResults/InstantResults.php
@@ -224,27 +224,39 @@ public function enqueue_frontend_assets() {
*/
$api_endpoint = apply_filters( 'ep_instant_results_search_endpoint', "api/v1/search/posts/{$index}", $index );
+ $ep_instant_results_config = array(
+ 'apiEndpoint' => $api_endpoint,
+ 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? esc_url_raw( $this->host ) : '',
+ 'argsSchema' => $this->get_args_schema(),
+ 'currencyCode' => $this->is_woocommerce ? get_woocommerce_currency() : false,
+ 'facets' => $this->get_facets_for_frontend(),
+ 'highlightTag' => $this->settings['highlight_tag'],
+ 'isWooCommerce' => $this->is_woocommerce,
+ 'locale' => str_replace( '_', '-', get_locale() ),
+ 'matchType' => $this->settings['match_type'],
+ 'paramPrefix' => 'ep-',
+ 'postTypeLabels' => $this->get_post_type_labels(),
+ 'termCount' => $this->settings['term_count'],
+ 'numberedPagination' => $this->settings['numbered_pagination'],
+ 'requestIdBase' => Utils\get_request_id_base(),
+ 'showSuggestions' => \ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->is_active(),
+ 'suggestionsBehavior' => $this->settings['search_behavior'],
+ );
+
+ /**
+ * Filter the Instant Results configuration passed to the front end.
+ *
+ * @since 5.3.0
+ * @hook ep_instant_results_config
+ * @param {array} $ep_instant_results_config Instant Results configuration.
+ * @returns {array} Filtered Instant Results configuration.
+ */
+ $ep_instant_results_config = apply_filters( 'ep_instant_results_config', $ep_instant_results_config );
+
wp_localize_script(
'elasticpress-instant-results',
'epInstantResults',
- array(
- 'apiEndpoint' => $api_endpoint,
- 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? esc_url_raw( $this->host ) : '',
- 'argsSchema' => $this->get_args_schema(),
- 'currencyCode' => $this->is_woocommerce ? get_woocommerce_currency() : false,
- 'facets' => $this->get_facets_for_frontend(),
- 'highlightTag' => $this->settings['highlight_tag'],
- 'isWooCommerce' => $this->is_woocommerce,
- 'locale' => str_replace( '_', '-', get_locale() ),
- 'matchType' => $this->settings['match_type'],
- 'paramPrefix' => 'ep-',
- 'postTypeLabels' => $this->get_post_type_labels(),
- 'termCount' => $this->settings['term_count'],
- 'numberedPagination' => $this->settings['numbered_pagination'],
- 'requestIdBase' => Utils\get_request_id_base(),
- 'showSuggestions' => \ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->is_active(),
- 'suggestionsBehavior' => $this->settings['search_behavior'],
- )
+ $ep_instant_results_config
);
}
diff --git a/tests/e2e/src/specs/instant-results.spec.ts b/tests/e2e/src/specs/instant-results.spec.ts
index 6a40945cbc..5799dcd05e 100644
--- a/tests/e2e/src/specs/instant-results.spec.ts
+++ b/tests/e2e/src/specs/instant-results.spec.ts
@@ -120,7 +120,7 @@ test.describe('Instant Results Feature', { tag: '@group1' }, () => {
test.beforeEach(async ({ loggedInPage }) => {
await deactivatePlugin(
loggedInPage,
- 'custom-instant-results-template open-instant-results-with-buttons filter-instant-results-per-page filter-instant-results-args-schema',
+ 'custom-instant-results-template open-instant-results-with-buttons filter-instant-results-per-page filter-instant-results-args-schema filter-instant-results-label filter-instant-results-post-type-options filter-instant-results-price-range filter-instant-results-components filter-instant-results-response filter-instant-results-config filter-instant-results-category-terms',
'wpCli',
);
});
@@ -598,6 +598,149 @@ test.describe('Instant Results Feature', { tag: '@group1' }, () => {
'wpCli',
);
});
+
+ test('Can filter facet labels', async ({ loggedInPage }) => {
+ await maybeEnableFeature('instant-results');
+ await activatePlugin(loggedInPage, 'filter-instant-results-label', 'wpCli');
+
+ await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress');
+ const apiResponsePromise = loggedInPage.waitForResponse(
+ '**/wp-json/elasticpress/v1/features*',
+ );
+
+ await loggedInPage.getByRole('button', { name: 'Live Search' }).click();
+ await loggedInPage.getByRole('button', { name: 'Instant Results' }).click();
+ await addInstantResultFilter(loggedInPage, 'post type');
+ await loggedInPage.getByRole('button', { name: 'Save changes' }).click();
+
+ await apiResponsePromise;
+
+ const responsePromise = instantResultRequestPromise(loggedInPage, 'search=new');
+ await loggedInPage.goto('/');
+ await searchFor(loggedInPage, 'new');
+ await responsePromise;
+
+ const postTypeFacet = loggedInPage.locator('.ep-search-panel').filter({
+ has: loggedInPage
+ .locator('.ep-search-panel__button')
+ .filter({ hasText: 'Filtered Type Label' }),
+ });
+ await expect(postTypeFacet).toBeVisible();
+ });
+
+ test('Can filter post type options', async ({ loggedInPage }) => {
+ await maybeEnableFeature('instant-results');
+ await activatePlugin(
+ loggedInPage,
+ 'filter-instant-results-post-type-options',
+ 'wpCli',
+ );
+
+ await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress');
+ const apiResponsePromise = loggedInPage.waitForResponse(
+ '**/wp-json/elasticpress/v1/features*',
+ );
+
+ await loggedInPage.getByRole('button', { name: 'Live Search' }).click();
+ await loggedInPage.getByRole('button', { name: 'Instant Results' }).click();
+ await addInstantResultFilter(loggedInPage, 'post type');
+ await loggedInPage.getByRole('button', { name: 'Save changes' }).click();
+
+ await apiResponsePromise;
+
+ const responsePromise = instantResultRequestPromise(loggedInPage, 'search=new');
+ await loggedInPage.goto('/');
+ await searchFor(loggedInPage, 'new');
+ await responsePromise;
+
+ const postTypeFacet = loggedInPage.locator('.ep-search-panel').filter({
+ has: loggedInPage
+ .locator('.ep-search-panel__button')
+ .filter({ hasText: 'Type' }),
+ });
+ await expect(postTypeFacet).toContainText('(filtered)');
+ });
+
+ test('Can filter price range options', async ({ loggedInPage }) => {
+ await maybeEnableFeature('instant-results');
+ await maybeEnableFeature('woocommerce');
+ await activatePlugin(loggedInPage, 'filter-instant-results-price-range', 'wpCli');
+ await wpCli('wp elasticpress sync');
+
+ await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress');
+ const apiResponsePromise = loggedInPage.waitForResponse(
+ '**/wp-json/elasticpress/v1/features*',
+ );
+
+ await loggedInPage.getByRole('button', { name: 'Live Search' }).click();
+ await loggedInPage.getByRole('button', { name: 'Instant Results' }).click();
+ await addInstantResultFilter(loggedInPage, 'price');
+ await loggedInPage.getByRole('button', { name: 'Save changes' }).click();
+
+ await apiResponsePromise;
+
+ const responsePromise = instantResultRequestPromise(
+ loggedInPage,
+ 'search=ergonomic',
+ );
+ await loggedInPage.goto('/');
+ await searchFor(loggedInPage, 'ergonomic');
+ await responsePromise;
+
+ const priceValues = await loggedInPage
+ .locator('.ep-search-price-facet__values')
+ .textContent();
+ expect(priceValues).toContain('$0');
+ expect(priceValues).toContain('$999');
+ });
+
+ test('Can filter components (did-you-mean and sort)', async ({ loggedInPage }) => {
+ await maybeEnableFeature('instant-results');
+ await maybeEnableFeature('did-you-mean');
+ await activatePlugin(loggedInPage, 'filter-instant-results-components', 'wpCli');
+
+ await wpCli('wp elasticpress sync --setup --yes');
+
+ const responsePromise = instantResultRequestPromise(
+ loggedInPage,
+ 'search=wordpless',
+ );
+ await loggedInPage.goto('/');
+ await searchFor(loggedInPage, 'wordpless');
+ await expect(loggedInPage.locator('.ep-search-modal')).toBeVisible();
+ await responsePromise;
+
+ await expect(loggedInPage.locator('.my-custom-did-you-mean')).toBeVisible();
+ await expect(loggedInPage.locator('.my-custom-sort')).toBeVisible();
+ });
+
+ test('Can filter the search response', async ({ loggedInPage }) => {
+ await maybeEnableFeature('instant-results');
+ await activatePlugin(loggedInPage, 'filter-instant-results-response', 'wpCli');
+
+ const responsePromise = instantResultRequestPromise(loggedInPage, 'search=new');
+ await loggedInPage.goto('/');
+ await searchFor(loggedInPage, 'new');
+ await expect(loggedInPage.locator('.ep-search-modal')).toBeVisible();
+ await responsePromise;
+
+ await expect(loggedInPage.locator('.ep-search-result')).not.toBeVisible();
+ await expect(loggedInPage.locator('.ep-search-results__title')).toContainText('0');
+ });
+
+ test('Can filter the front-end config', async ({ loggedInPage }) => {
+ await maybeEnableFeature('instant-results');
+ await activatePlugin(loggedInPage, 'filter-instant-results-config', 'wpCli');
+
+ await loggedInPage.goto('/');
+
+ const config = await loggedInPage.evaluate(() => {
+ return (window as any).epInstantResults;
+ });
+
+ expect(config.highlightTag).toBe('mark');
+ expect(config.epTestFilteredConfig).toBe(true);
+ });
});
test('Is possible to filter the arguments schema', async ({ loggedInPage }) => {
diff --git a/tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-components.php b/tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-components.php
new file mode 100644
index 0000000000..7ea9f5ecd5
--- /dev/null
+++ b/tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-components.php
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+