From fe742b0ed187f8431b2a5e9e1634343c806c007d Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Tue, 8 Feb 2022 15:53:10 +1100 Subject: [PATCH 01/12] Simplify filters state. --- .../components/facets/facet.js | 2 +- .../components/facets/post-type-facet.js | 4 +- .../components/facets/price-range-facet.js | 18 +++++---- .../components/facets/taxonomy-terms-facet.js | 31 +++++++-------- .../components/tools/clear-constraints.js | 16 ++++++-- assets/js/instant-results/functions.js | 34 +++++----------- assets/js/instant-results/index.js | 1 - assets/js/instant-results/reducer.js | 39 ++++++++----------- .../Feature/InstantResults/InstantResults.php | 7 ++-- 9 files changed, 70 insertions(+), 82 deletions(-) diff --git a/assets/js/instant-results/components/facets/facet.js b/assets/js/instant-results/components/facets/facet.js index 0a9f273c32..a2974d2566 100644 --- a/assets/js/instant-results/components/facets/facet.js +++ b/assets/js/instant-results/components/facets/facet.js @@ -34,8 +34,8 @@ export default ({ index, label, name, postTypes, type }) => { ); default: 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 a184bbdbc3..159d17b192 100644 --- a/assets/js/instant-results/components/facets/post-type-facet.js +++ b/assets/js/instant-results/components/facets/post-type-facet.js @@ -24,9 +24,9 @@ import { ActiveContraint } from '../tools/active-constraints'; export default ({ defaultIsOpen, label }) => { const { state: { + aggregations: { post_type: { post_type: { buckets = [] } = {} } = {} }, + args: { post_type: selectedPostTypes = [] }, isLoading, - filters: { post_type: selectedPostTypes = [] }, - postTypesAggregation: { post_types: { buckets = [] } = {} } = {}, }, dispatch, } = useContext(Context); 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 e2bedca102..6d868b96c4 100644 --- a/assets/js/instant-results/components/facets/price-range-facet.js +++ b/assets/js/instant-results/components/facets/price-range-facet.js @@ -24,12 +24,14 @@ import { ActiveContraint } from '../tools/active-constraints'; export default ({ defaultIsOpen, label }) => { const { state: { + aggregations: { + price_range: { + max_price: { value: maxAgg = null } = {}, + min_price: { value: minAgg = null } = {}, + } = {}, + }, + args: { max_price: maxArg = null, min_price: minArg = null }, isLoading, - priceRangeAggregations: { - max_price: { value: maxAgg = null } = {}, - min_price: { value: minAgg = null } = {}, - } = {}, - filters: { price_range: [minArg = null, maxArg = null] = [] }, }, dispatch, } = useContext(Context); @@ -70,7 +72,9 @@ export default ({ defaultIsOpen, label }) => { * @param {number[]} values Lowest and highest values. */ const onAfterChange = (values) => { - dispatch({ type: 'APPLY_FILTER', payload: { price_range: values } }); + const [min_price, max_price] = values; + + dispatch({ type: 'APPLY_FILTERS', payload: { min_price, max_price } }); }; /** @@ -87,7 +91,7 @@ export default ({ defaultIsOpen, label }) => { * Handle clearing the filter. */ const onClear = () => { - dispatch({ type: 'APPLY_FILTER', payload: { price_range: [] } }); + dispatch({ type: 'APPLY_FILTERS', payload: { max_price: null, min_price: null } }); }; /** diff --git a/assets/js/instant-results/components/facets/taxonomy-terms-facet.js b/assets/js/instant-results/components/facets/taxonomy-terms-facet.js index 3271a93675..f8a43a3e51 100644 --- a/assets/js/instant-results/components/facets/taxonomy-terms-facet.js +++ b/assets/js/instant-results/components/facets/taxonomy-terms-facet.js @@ -19,31 +19,26 @@ import { ActiveContraint } from '../tools/active-constraints'; * @param {Object} props Components props. * @param {boolean} props.defaultIsOpen Whether the panel is open by default. * @param {string} props.label Facet label. + * @param {string} props.name Facet name. * @param {Array} props.postTypes Facet post types. - * @param {string} props.taxonomy Facet taxonomy. * @return {WPElement} Component element. */ -export default ({ defaultIsOpen, label, postTypes, taxonomy }) => { +export default ({ defaultIsOpen, label, postTypes, name }) => { const { state: { isLoading, - filters: { [taxonomy]: selectedTerms = [] }, - taxonomyTermsAggregations: { - [taxonomy]: { taxonomy_terms: { buckets = [] } = {} } = {}, - } = {}, + args: { [name]: selectedTerms = [] }, + aggregations: { [name]: { [name]: { buckets = [] } = {} } = {} } = {}, }, dispatch, } = useContext(Context); /** * A unique label for the facet. Adds additional context to the label if - * another taxonomy with the same label is being used as a facet. + * another facet with the same label is being used. */ const uniqueLabel = useMemo(() => { - const isNotUnique = facets.some( - (facet) => facet.label === label && facet.name !== taxonomy, - ); - + const isNotUnique = facets.some((facet) => facet.label === label && facet.name !== name); const typeLabels = postTypes.map((postType) => postTypeLabels[postType].plural); const typeSeparator = __(', ', 'elasticpress'); @@ -55,7 +50,7 @@ export default ({ defaultIsOpen, label, postTypes, taxonomy }) => { typeLabels.join(typeSeparator), ) : label; - }, [label, postTypes, taxonomy]); + }, [label, postTypes, name]); /** * Create list of filter options from aggregation buckets. @@ -67,12 +62,12 @@ export default ({ defaultIsOpen, label, postTypes, taxonomy }) => { */ const reduceOptions = useCallback( (options, { key }) => { - const { name, parent, term_id, term_order } = JSON.parse(key); + const { name: label, parent, term_id, term_order } = JSON.parse(key); options.push({ checked: selectedTerms.includes(term_id), - id: `ep-search-${taxonomy}-${term_id}`, - label: name, + id: `ep-search-${name}-${term_id}`, + label, parent: parent.toString(), order: term_order, value: term_id.toString(), @@ -80,7 +75,7 @@ export default ({ defaultIsOpen, label, postTypes, taxonomy }) => { return options; }, - [selectedTerms, taxonomy], + [selectedTerms, name], ); /** @@ -113,7 +108,7 @@ export default ({ defaultIsOpen, label, postTypes, taxonomy }) => { * @param {string[]} terms Selected terms. */ const onChange = (terms) => { - dispatch({ type: 'APPLY_FILTERS', payload: { [taxonomy]: terms } }); + dispatch({ type: 'APPLY_FILTERS', payload: { [name]: terms } }); }; /** @@ -126,7 +121,7 @@ export default ({ defaultIsOpen, label, postTypes, taxonomy }) => { terms.splice(terms.indexOf(term), 1); - dispatch({ type: 'APPLY_FILTERS', payload: { [taxonomy]: terms } }); + dispatch({ type: 'APPLY_FILTERS', payload: { [name]: terms } }); }; return ( diff --git a/assets/js/instant-results/components/tools/clear-constraints.js b/assets/js/instant-results/components/tools/clear-constraints.js index 986525f185..d1627c9ca6 100644 --- a/assets/js/instant-results/components/tools/clear-constraints.js +++ b/assets/js/instant-results/components/tools/clear-constraints.js @@ -18,7 +18,7 @@ import SmallButton from '../common/small-button'; */ export default () => { const { - state: { filters }, + state: { args }, dispatch, } = useContext(Context); @@ -32,8 +32,18 @@ export default () => { * @return {boolean} Whether there are active filters. */ const hasFilters = useMemo(() => { - return facets.some(({ name }) => filters[name]?.length > 0); - }, [filters]); + return facets.some(({ name, type }) => { + switch (type) { + case 'post_type': + case 'taxonomy': + return args[name]?.length > 0; + case 'price_range': + return args.max_price || args.min_price; + default: + return args[name]; + } + }); + }, [args]); /** * Handle clicking button. diff --git a/assets/js/instant-results/functions.js b/assets/js/instant-results/functions.js index e278504009..c230e4df02 100644 --- a/assets/js/instant-results/functions.js +++ b/assets/js/instant-results/functions.js @@ -48,35 +48,19 @@ export const getPostTypesFromForm = (form) => { * @return {URLSearchParams} URLSearchParams instance. */ export const getURLParamsFromState = (state) => { - const { args, filters } = state; + const { args } = state; - const filterArgs = Object.entries(filters).reduce((filterArgs, [filter, value]) => { - switch (filter) { - case 'price_range': - if (value.length > 0) { - filterArgs.min_price = value[0]; - filterArgs.max_price = value[1]; - } - - break; - case 'post_type': - if (value.length > 0) { - filterArgs[filter] = value.join(','); - } - - break; - default: - if (value.length > 0) { - filterArgs[`tax-${filter}`] = value.join(','); - } - - break; + const init = Object.entries(args).reduce((init, [key, value]) => { + if (Array.isArray(value)) { + if (value.length > 0) { + init[key] = value.join(','); + } + } else { + init[key] = value; } - return filterArgs; + return init; }, {}); - const init = { ...args, ...filterArgs }; - return new URLSearchParams(init); }; diff --git a/assets/js/instant-results/index.js b/assets/js/instant-results/index.js index c003b83faf..2d283d802d 100644 --- a/assets/js/instant-results/index.js +++ b/assets/js/instant-results/index.js @@ -184,7 +184,6 @@ const App = () => { state.args.offset, state.args.search, state.isOpen, - state.filters, ]); return ( diff --git a/assets/js/instant-results/reducer.js b/assets/js/instant-results/reducer.js index 4046606f12..efa421fa4e 100644 --- a/assets/js/instant-results/reducer.js +++ b/assets/js/instant-results/reducer.js @@ -7,6 +7,7 @@ import { facets, highlightTag, matchType } from './config'; * Initial state. */ export const initialArg = { + aggregations: {}, args: { highlight: highlightTag, offset: 0, @@ -15,18 +16,12 @@ export const initialArg = { per_page: 6, relation: matchType === 'all' ? 'and' : 'or', search: '', - elasticpress: '1', }, - filters: {}, isLoading: false, isOpen: false, isSidebarOpen: false, - poppingState: false, - postTypesAggregation: {}, - priceRangeAggregations: {}, searchResults: [], searchedTerm: '', - taxonomyTermsAggregations: {}, totalResults: 0, }; @@ -44,19 +39,25 @@ export const reducer = (state, { type, payload }) => { switch (type) { case 'APPLY_FILTERS': { - newState.args.offset = 0; - newState.filters = { ...state.filters, ...payload }; - + newState.args = { ...state.args, ...payload, offset: 0 }; break; } case 'CLEAR_FILTERS': { - newState.filters = facets.reduce( - (filters, { name }) => { - delete filters[name]; + newState.args = facets.reduce( + (args, { name, type }) => { + switch (type) { + case 'price_range': + delete args.max_price; + delete args.min_price; + break; + default: + delete args[name]; + break; + } - return filters; + return args; }, - { ...state.filters }, + { ...state.args }, ); break; @@ -75,11 +76,7 @@ export const reducer = (state, { type, payload }) => { case 'NEW_SEARCH_RESULTS': { const { hits: { hits, total }, - aggregations: { - post_type: postTypesAggregation, - price_range: priceRangeAggregation, - ...taxonomyTermsAggregations - } = {}, + aggregations, } = payload; /** @@ -87,11 +84,9 @@ export const reducer = (state, { type, payload }) => { */ const totalNumber = typeof total === 'number' ? total : total.value; - newState.postTypesAggregation = postTypesAggregation; - newState.priceRangeAggregations = priceRangeAggregation; + newState.aggregations = aggregations; newState.searchResults = hits; newState.searchedTerm = newState.args.search; - newState.taxonomyTermsAggregations = taxonomyTermsAggregations; newState.totalResults = totalNumber; break; diff --git a/includes/classes/Feature/InstantResults/InstantResults.php b/includes/classes/Feature/InstantResults/InstantResults.php index afb9fba289..e6ce55dfad 100644 --- a/includes/classes/Feature/InstantResults/InstantResults.php +++ b/includes/classes/Feature/InstantResults/InstantResults.php @@ -653,7 +653,7 @@ public function get_facets() { 'frontend' => __( 'Type', 'elasticpress' ), ), 'aggs' => array( - 'post_types' => array( + 'post_type' => array( 'terms' => array( 'field' => 'post_type.raw', ), @@ -668,6 +668,7 @@ public function get_facets() { $taxonomies = apply_filters( 'ep_facet_include_taxonomies', $taxonomies ); foreach ( $taxonomies as $slug => $taxonomy ) { + $name = 'tax-' . $slug; $labels = get_taxonomy_labels( $taxonomy ); $admin_label = sprintf( @@ -677,7 +678,7 @@ public function get_facets() { $slug ); - $facets[ $slug ] = array( + $facets[ $name ] = array( 'type' => 'taxonomy', 'post_types' => $taxonomy->object_type, 'labels' => array( @@ -685,7 +686,7 @@ public function get_facets() { 'frontend' => $labels->singular_name, ), 'aggs' => array( - 'taxonomy_terms' => array( + $name => array( 'terms' => array( 'field' => 'terms.' . $slug . '.facet', 'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ), From bb17033ba7cc3a45da838b6b311ec1cbd9300159 Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Thu, 17 Feb 2022 18:00:37 +1100 Subject: [PATCH 02/12] Support history API for navigating modal. --- .../components/facets/post-type-facet.js | 4 +- .../components/facets/price-range-facet.js | 4 +- .../components/facets/search-term-facet.js | 20 +++-- .../components/facets/taxonomy-terms-facet.js | 4 +- .../components/tools/clear-constraints.js | 2 +- .../instant-results/components/tools/sort.js | 2 +- assets/js/instant-results/functions.js | 26 +++++- assets/js/instant-results/index.js | 80 ++++++++++--------- assets/js/instant-results/reducer.js | 55 +++++-------- 9 files changed, 114 insertions(+), 83 deletions(-) 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 159d17b192..652b2b6ab4 100644 --- a/assets/js/instant-results/components/facets/post-type-facet.js +++ b/assets/js/instant-results/components/facets/post-type-facet.js @@ -70,7 +70,7 @@ export default ({ defaultIsOpen, label }) => { * @param {string[]} postTypes Selected post types. */ const onChange = (postTypes) => { - dispatch({ type: 'APPLY_FILTERS', payload: { post_type: postTypes } }); + dispatch({ type: 'APPLY_ARGS', payload: { post_type: postTypes } }); }; /** @@ -84,7 +84,7 @@ export default ({ defaultIsOpen, label }) => { postTypes.splice(index, 1); - dispatch({ type: 'APPLY_FILTERS', payload: { post_type: postTypes } }); + dispatch({ type: 'APPLY_ARGS', payload: { post_type: postTypes } }); }; return ( 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 6d868b96c4..624607064a 100644 --- a/assets/js/instant-results/components/facets/price-range-facet.js +++ b/assets/js/instant-results/components/facets/price-range-facet.js @@ -74,7 +74,7 @@ export default ({ defaultIsOpen, label }) => { const onAfterChange = (values) => { const [min_price, max_price] = values; - dispatch({ type: 'APPLY_FILTERS', payload: { min_price, max_price } }); + dispatch({ type: 'APPLY_ARGS', payload: { min_price, max_price } }); }; /** @@ -91,7 +91,7 @@ export default ({ defaultIsOpen, label }) => { * Handle clearing the filter. */ const onClear = () => { - dispatch({ type: 'APPLY_FILTERS', payload: { max_price: null, min_price: null } }); + dispatch({ type: 'APPLY_ARGS', payload: { max_price: null, min_price: null } }); }; /** diff --git a/assets/js/instant-results/components/facets/search-term-facet.js b/assets/js/instant-results/components/facets/search-term-facet.js index 2844356c74..4fafa835b0 100644 --- a/assets/js/instant-results/components/facets/search-term-facet.js +++ b/assets/js/instant-results/components/facets/search-term-facet.js @@ -1,13 +1,14 @@ /** * WordPress dependencies. */ -import { useContext, WPElement } from '@wordpress/element'; +import { useContext, useState, WPElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies. */ import Context from '../../context'; +import { useDebounce } from '../../hooks'; import { ActiveContraint } from '../tools/active-constraints'; /** @@ -24,21 +25,30 @@ export default () => { dispatch, } = useContext(Context); + const [value, setValue] = useState(search); + + /** + * Dispatch the change, with debouncing. + */ + const dispatchChange = useDebounce((value) => { + dispatch({ type: 'NEW_SEARCH_TERM', payload: value }); + }, 300); + /** * Handle input changes. * * @param {Event} event Change event. */ const onChange = (event) => { - dispatch({ type: 'SET_SEARCH_TERM', payload: event.target.value }); - dispatch({ type: 'CLEAR_FILTERS' }); + setValue(event.target.value); + dispatchChange(event.target.value); }; /** * Handle clearing. */ const onClear = () => { - dispatch({ type: 'SET_SEARCH_TERM', payload: '' }); + dispatch({ type: 'NEW_SEARCH_TERM', payload: '' }); }; return ( @@ -47,7 +57,7 @@ export default () => { className="ep-search-input" placeholder={__('Search…', 'elasticpress')} type="search" - value={search} + value={value} onChange={onChange} /> {searchedTerm && ( diff --git a/assets/js/instant-results/components/facets/taxonomy-terms-facet.js b/assets/js/instant-results/components/facets/taxonomy-terms-facet.js index f8a43a3e51..6f4a081da6 100644 --- a/assets/js/instant-results/components/facets/taxonomy-terms-facet.js +++ b/assets/js/instant-results/components/facets/taxonomy-terms-facet.js @@ -108,7 +108,7 @@ export default ({ defaultIsOpen, label, postTypes, name }) => { * @param {string[]} terms Selected terms. */ const onChange = (terms) => { - dispatch({ type: 'APPLY_FILTERS', payload: { [name]: terms } }); + dispatch({ type: 'APPLY_ARGS', payload: { [name]: terms } }); }; /** @@ -121,7 +121,7 @@ export default ({ defaultIsOpen, label, postTypes, name }) => { terms.splice(terms.indexOf(term), 1); - dispatch({ type: 'APPLY_FILTERS', payload: { [name]: terms } }); + dispatch({ type: 'APPLY_ARGS', payload: { [name]: terms } }); }; return ( diff --git a/assets/js/instant-results/components/tools/clear-constraints.js b/assets/js/instant-results/components/tools/clear-constraints.js index d1627c9ca6..2e74f40588 100644 --- a/assets/js/instant-results/components/tools/clear-constraints.js +++ b/assets/js/instant-results/components/tools/clear-constraints.js @@ -51,7 +51,7 @@ export default () => { * @return {void} */ const onClick = () => { - dispatch({ type: 'CLEAR_FILTERS' }); + dispatch({ type: 'CLEAR_FACETS' }); }; return ( diff --git a/assets/js/instant-results/components/tools/sort.js b/assets/js/instant-results/components/tools/sort.js index ca36074a26..d67b774259 100644 --- a/assets/js/instant-results/components/tools/sort.js +++ b/assets/js/instant-results/components/tools/sort.js @@ -40,7 +40,7 @@ export default () => { const onChange = (event) => { const { orderby, order } = sortOptions[event.target.value]; - dispatch({ type: 'SORT_RESULTS', payload: { orderby, order } }); + dispatch({ type: 'APPLY_ARGS', payload: { orderby, order } }); }; return ( diff --git a/assets/js/instant-results/functions.js b/assets/js/instant-results/functions.js index c230e4df02..2aecfa8f62 100644 --- a/assets/js/instant-results/functions.js +++ b/assets/js/instant-results/functions.js @@ -1,7 +1,31 @@ /** * Internal deendencies. */ -import { currencyCode } from './config'; +import { currencyCode, facets } from './config'; + +/** + * Clear facet filters from a set of args. + * + * @param {Object} args Args to clear facets from. + * @return {Object} Cleared args. + */ +export const clearFacetsFromArgs = (args) => { + const clearedArgs = { ...args }; + + facets.forEach(({ name, type }) => { + switch (type) { + case 'price_range': + delete clearedArgs.max_price; + delete clearedArgs.min_price; + break; + default: + delete clearedArgs[name]; + break; + } + }); + + return clearedArgs; +}; /** * Format a number as a price. diff --git a/assets/js/instant-results/index.js b/assets/js/instant-results/index.js index 2d283d802d..2e21740021 100644 --- a/assets/js/instant-results/index.js +++ b/assets/js/instant-results/index.js @@ -10,7 +10,7 @@ import { __ } from '@wordpress/i18n'; */ import Context from './context'; import { getPostTypesFromForm, getURLParamsFromState } from './functions'; -import { useDebounce, useGetResults } from './hooks'; +import { useGetResults } from './hooks'; import { reducer, initialArg } from './reducer'; import Layout from './components/layout'; import Modal from './components/common/modal'; @@ -29,19 +29,11 @@ const App = () => { stateRef.current = state; - /** - * Close the modal. - */ - const openModal = useCallback(() => { - dispatch({ type: 'OPEN_MODAL' }); - }, []); - /** * Close the modal. */ const closeModal = useCallback(() => { dispatch({ type: 'CLOSE_MODAL' }); - dispatch({ type: 'CLEAR_FILTERS' }); inputRef.current.focus(); }, []); @@ -86,9 +78,24 @@ const App = () => { }, [finishLoading, getResults, startLoading, updateResults]); /** - * Debounced search function. + * Push state to history. */ - const doSearchDebounced = useDebounce(doSearch, 250); + const pushState = useCallback(() => { + const { history } = modalRef.current.ownerDocument.defaultView; + const { args, isOpen, isPoppingState } = stateRef.current; + + if (isPoppingState) { + return; + } + + const state = JSON.stringify({ ...args, isOpen }); + + if (history.state) { + history.pushState(state, document.title); + } else { + history.replaceState(state, document.title); + } + }, []); /** * Handle escape key press. @@ -104,47 +111,46 @@ const App = () => { [closeModal], ); + /** + * Handle popstate event. + * + * @param {Event} event popstate event. + */ + const onPopState = useCallback((event) => { + if (event.state) { + dispatch({ type: 'POP_STATE', payload: JSON.parse(event.state) }); + } + }, []); + /** * Handle submitting the search form. * * @param {Event} event Input event. */ - const onSubmit = useCallback( - (event) => { - event.preventDefault(); - - inputRef.current = event.target.s; + const onSubmit = useCallback((event) => { + event.preventDefault(); - const searchTerm = inputRef.current.value; - const postTypes = getPostTypesFromForm(inputRef.current.form); + inputRef.current = event.target.s; - dispatch({ type: 'SET_SEARCH_TERM', payload: searchTerm }); - dispatch({ type: 'APPLY_FILTERS', payload: { post_type: postTypes } }); + const search = inputRef.current.value; + const post_type = getPostTypesFromForm(inputRef.current.form); - openModal(); - }, - [openModal], - ); + dispatch({ type: 'APPLY_ARGS', payload: { search, post_type } }); + }, []); /** * Handle changes to search parameters. */ const handleChanges = () => { - const { - args: { search }, - isOpen, - searchedTerm, - } = stateRef.current; + const { isOpen } = stateRef.current; + + pushState(); if (!isOpen) { return; } - if (search !== searchedTerm) { - doSearchDebounced(); - } else { - doSearch(); - } + doSearch(); }; /** @@ -161,6 +167,7 @@ const App = () => { }); modal.ownerDocument.body.addEventListener('keydown', onEscape); + modal.ownerDocument.defaultView.addEventListener('popstate', onPopState); return () => { inputs.forEach((input) => { @@ -168,16 +175,17 @@ const App = () => { }); modal.ownerDocument.body.removeEventListener('keydown', onEscape); + modal.ownerDocument.defaultView.removeEventListener('popstate', onPopState); }; }; /** * Effects. */ - useEffect(handleEvents, [onEscape, onSubmit]); + useEffect(handleEvents, [onEscape, onPopState, onSubmit]); useEffect(handleChanges, [ doSearch, - doSearchDebounced, + pushState, state.args, state.args.orderby, state.args.order, diff --git a/assets/js/instant-results/reducer.js b/assets/js/instant-results/reducer.js index efa421fa4e..8f2c045741 100644 --- a/assets/js/instant-results/reducer.js +++ b/assets/js/instant-results/reducer.js @@ -1,7 +1,8 @@ /** * Internal dependencies. */ -import { facets, highlightTag, matchType } from './config'; +import { highlightTag, matchType } from './config'; +import { clearFacetsFromArgs } from './functions'; /** * Initial state. @@ -20,6 +21,7 @@ export const initialArg = { isLoading: false, isOpen: false, isSidebarOpen: false, + isPoppingState: false, searchResults: [], searchedTerm: '', totalResults: 0, @@ -35,42 +37,23 @@ export const initialArg = { * @return {Object} Updated state. */ export const reducer = (state, { type, payload }) => { - const newState = { ...state }; + const newState = { ...state, isPoppingState: false }; switch (type) { - case 'APPLY_FILTERS': { - newState.args = { ...state.args, ...payload, offset: 0 }; + case 'APPLY_ARGS': { + newState.args = { ...newState.args, ...payload, offset: 0 }; + newState.isOpen = true; break; } - case 'CLEAR_FILTERS': { - newState.args = facets.reduce( - (args, { name, type }) => { - switch (type) { - case 'price_range': - delete args.max_price; - delete args.min_price; - break; - default: - delete args[name]; - break; - } - - return args; - }, - { ...state.args }, - ); - + case 'CLEAR_FACETS': { + newState.args = clearFacetsFromArgs(newState.args); break; } - case 'SET_SEARCH_TERM': { + case 'NEW_SEARCH_TERM': { + newState.args = clearFacetsFromArgs(newState.args); newState.args.offset = 0; newState.args.search = payload; - break; - } - case 'SORT_RESULTS': { - newState.args.offset = 0; - newState.args.order = payload.order; - newState.args.orderby = payload.orderby; + break; } case 'NEW_SEARCH_RESULTS': { @@ -111,14 +94,20 @@ export const reducer = (state, { type, payload }) => { newState.isSidebarOpen = !state.isSidebarOpen; break; } - case 'OPEN_MODAL': { - newState.isOpen = true; - break; - } case 'CLOSE_MODAL': { + newState.args = clearFacetsFromArgs(newState.args); newState.isOpen = false; break; } + case 'POP_STATE': { + const { isOpen, ...args } = payload; + + newState.args = args; + newState.isOpen = isOpen; + newState.isPoppingState = true; + + break; + } default: break; } From 525ff1c114a84303798dad79badfa59abab28295 Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Tue, 22 Feb 2022 23:43:40 +1100 Subject: [PATCH 03/12] Support opening the modal with pre-defined args if they are found in the URL. --- assets/js/instant-results/config.js | 6 +- assets/js/instant-results/functions.js | 63 ++++++++--- assets/js/instant-results/index.js | 36 +++++-- assets/js/instant-results/reducer.js | 6 +- assets/js/instant-results/utilities.js | 100 ++++++++++++++++++ .../Feature/InstantResults/InstantResults.php | 73 +++++++++++++ 6 files changed, 255 insertions(+), 29 deletions(-) create mode 100644 assets/js/instant-results/utilities.js diff --git a/assets/js/instant-results/config.js b/assets/js/instant-results/config.js index 5b6aa6ce23..735fcef329 100644 --- a/assets/js/instant-results/config.js +++ b/assets/js/instant-results/config.js @@ -9,11 +9,12 @@ import { __ } from '@wordpress/i18n'; const { apiEndpoint, apiHost, + argsSchema, currencyCode, facets, - highlightTag, isWooCommerce, matchType, + paramPrefix, postTypeLabels, taxonomyLabels, } = window.epInstantResults; @@ -60,11 +61,12 @@ if (isWooCommerce) { export { apiEndpoint, apiHost, + argsSchema, currencyCode, facets, - highlightTag, isWooCommerce, matchType, + paramPrefix, postTypeLabels, sortOptions, taxonomyLabels, diff --git a/assets/js/instant-results/functions.js b/assets/js/instant-results/functions.js index 2aecfa8f62..9dde3007ee 100644 --- a/assets/js/instant-results/functions.js +++ b/assets/js/instant-results/functions.js @@ -2,6 +2,7 @@ * Internal deendencies. */ import { currencyCode, facets } from './config'; +import { sanitizeArg, sanitizeParam } from './utilities'; /** * Clear facet filters from a set of args. @@ -66,25 +67,59 @@ export const getPostTypesFromForm = (form) => { }; /** - * Get query parameters for an API request from the state. + * Get permalink URL parameters from args. * - * @param {Object} state State. + * @typedef {Object} ArgSchema + * @property {string} type Arg type. + * @property {any} [default] Default arg value. + * @property {Array} [allowedValues] Array of allowed values. + * + * @param {Object} args Args + * @param {ArgSchema} schema Args schema. + * @param {string} [prefix] Prefix to prepend to args. * @return {URLSearchParams} URLSearchParams instance. */ -export const getURLParamsFromState = (state) => { - const { args } = state; - - const init = Object.entries(args).reduce((init, [key, value]) => { - if (Array.isArray(value)) { - if (value.length > 0) { - init[key] = value.join(','); - } - } else { - init[key] = value; +export const getUrlParamsFromArgs = (args, schema, prefix = '') => { + const urlParams = new URLSearchParams(); + + Object.entries(schema).forEach(([arg, options]) => { + const param = prefix + arg; + const value = typeof args[arg] !== 'undefined' ? sanitizeParam(args[arg], options) : null; + + if (value !== null) { + urlParams.set(param, value); + } + }); + + return urlParams; +}; + +/** + * Build request args from URL parameters using a given schema. + * + * @typedef {Object} ArgSchema + * @property {string} type Arg type. + * @property {any} [default] Default arg value. + * @property {Array} [allowedValues] Array of allowed values. + * + * @param {URLSearchParams} urlParams URL parameters. + * @param {object.} schema Schema to build args from. + * @param {string} [prefix] Parameter prefix. + * @param {boolean} [useDefaults] Whether to populate params with default values. + * @return {object.} Query args. + */ +export const getArgsFromUrlParams = (urlParams, schema, prefix = '', useDefaults = true) => { + const args = Object.entries(schema).reduce((args, [arg, options]) => { + const param = urlParams.get(prefix + arg); + const value = + typeof param !== 'undefined' ? sanitizeArg(param, options, useDefaults) : null; + + if (value !== null) { + args[arg] = value; } - return init; + return args; }, {}); - return new URLSearchParams(init); + return args; }; diff --git a/assets/js/instant-results/index.js b/assets/js/instant-results/index.js index 2e21740021..a2e127f29b 100644 --- a/assets/js/instant-results/index.js +++ b/assets/js/instant-results/index.js @@ -8,10 +8,11 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies. */ +import { argsSchema, paramPrefix } from './config'; import Context from './context'; -import { getPostTypesFromForm, getURLParamsFromState } from './functions'; +import { getArgsFromUrlParams, getUrlParamsFromArgs, getPostTypesFromForm } from './functions'; import { useGetResults } from './hooks'; -import { reducer, initialArg } from './reducer'; +import { reducer, initialState } from './reducer'; import Layout from './components/layout'; import Modal from './components/common/modal'; @@ -22,7 +23,7 @@ import Modal from './components/common/modal'; */ const App = () => { const getResults = useGetResults(); - const [state, dispatch] = useReducer(reducer, initialArg); + const [state, dispatch] = useReducer(reducer, initialState); const inputRef = useRef(); const modalRef = useRef(); const stateRef = useRef(state); @@ -34,7 +35,10 @@ const App = () => { */ const closeModal = useCallback(() => { dispatch({ type: 'CLOSE_MODAL' }); - inputRef.current.focus(); + + if (inputRef.current) { + inputRef.current.focus(); + } }, []); /** @@ -66,7 +70,7 @@ const App = () => { * Perform a search. */ const doSearch = useCallback(async () => { - const urlParams = getURLParamsFromState(stateRef.current); + const urlParams = getUrlParamsFromArgs(stateRef.current.args, argsSchema); startLoading(); @@ -89,11 +93,13 @@ const App = () => { } const state = JSON.stringify({ ...args, isOpen }); + const params = getUrlParamsFromArgs(args, argsSchema, paramPrefix).toString(); + const url = isOpen ? `?${params}` : window.location.origin + window.location.pathname; if (history.state) { - history.pushState(state, document.title); + history.pushState(state, document.title, url); } else { - history.replaceState(state, document.title); + history.replaceState(state, document.title, window.location.href); } }, []); @@ -166,7 +172,6 @@ const App = () => { input.form.addEventListener('submit', onSubmit); }); - modal.ownerDocument.body.addEventListener('keydown', onEscape); modal.ownerDocument.defaultView.addEventListener('popstate', onPopState); return () => { @@ -174,14 +179,26 @@ const App = () => { input.form.removeEventListener('submit', onSubmit); }); - modal.ownerDocument.body.removeEventListener('keydown', onEscape); modal.ownerDocument.defaultView.removeEventListener('popstate', onPopState); }; }; + /** + * Open modal with pre-defined args if they are found in the URL. + */ + const handleInit = () => { + const urlParams = new URLSearchParams(window.location.search); + const args = getArgsFromUrlParams(urlParams, argsSchema, paramPrefix, false); + + if (Object.keys(args).length > 0) { + dispatch({ type: 'APPLY_ARGS', payload: args }); + } + }; + /** * Effects. */ + useEffect(handleInit, []); useEffect(handleEvents, [onEscape, onPopState, onSubmit]); useEffect(handleChanges, [ doSearch, @@ -191,7 +208,6 @@ const App = () => { state.args.order, state.args.offset, state.args.search, - state.isOpen, ]); return ( diff --git a/assets/js/instant-results/reducer.js b/assets/js/instant-results/reducer.js index 8f2c045741..fdfe0c4cbe 100644 --- a/assets/js/instant-results/reducer.js +++ b/assets/js/instant-results/reducer.js @@ -1,16 +1,16 @@ /** * Internal dependencies. */ -import { highlightTag, matchType } from './config'; +import { matchType } from './config'; import { clearFacetsFromArgs } from './functions'; /** * Initial state. */ -export const initialArg = { +export const initialState = { aggregations: {}, args: { - highlight: highlightTag, + highlight: '', offset: 0, orderby: 'relevance', order: 'desc', diff --git a/assets/js/instant-results/utilities.js b/assets/js/instant-results/utilities.js new file mode 100644 index 0000000000..b84162a9c8 --- /dev/null +++ b/assets/js/instant-results/utilities.js @@ -0,0 +1,100 @@ +/** + * Sanitize an argument value based on its type. + * + * @param {*} value The value. + * @param {Object} options Sanitization options. + * @param {'integer'|'integers'|'string'|'strings'} options.type (optional) Value type. + * @param {Array} options.allowedValues (optional) Allowed values. + * @param {*} options.default (optional) Default value. + * @param {boolean} [useDefaults] Whether to return default values. + * @return {*} Sanitized value. + */ +export const sanitizeArg = (value, options, useDefaults = true) => { + let sanitizedValue = null; + + switch (value && options.type) { + case 'number': + sanitizedValue = parseFloat(value, 10) || null; + break; + case 'numbers': + sanitizedValue = decodeURIComponent(value) + .split(',') + .map((v) => parseFloat(v, 10)) + .filter(Boolean); + break; + case 'string': + sanitizedValue = value.toString(); + break; + case 'strings': + sanitizedValue = decodeURIComponent(value) + .split(',') + .map((v) => v.toString().trim()); + break; + default: + break; + } + + /** + * If there is a list of allowed values, make sure the value is + * allowed. + */ + if (options.allowedValues) { + sanitizedValue = options.allowedValues.includes(sanitizedValue) ? sanitizedValue : null; + } + + /** + * Populate a default value if one is available and we still don't + * have a value. + */ + if (useDefaults && sanitizedValue === null && typeof options.default !== 'undefined') { + sanitizedValue = options.default; + } + + return sanitizedValue; +}; + +/** + * Sanitize a parameter value based on its type. + * + * @param {*} value The value. + * @param {Object} options Sanitization options. + * @param {'integer'|'integers'|'string'|'strings'} options.type (optional) Value type. + * @param {Array} options.allowedValues (optional) Allowed values. + * @param {*} options.default (optional) Default value. + * @param {boolean} [useDefaults] Whether to return default values. + * @return {*} Sanitized value. + */ +export const sanitizeParam = (value, options, useDefaults = true) => { + let sanitizedValue = null; + + switch (value && options.type) { + case 'number': + case 'string': + sanitizedValue = value; + break; + case 'numbers': + case 'strings': + sanitizedValue = value.join(','); + break; + default: + break; + } + + /** + * If there is a list of allowed values, make sure the value is + * allowed. + */ + if (options.allowedValues) { + sanitizedValue = options.allowedValues.includes(sanitizedValue) ? sanitizedValue : null; + } + + /** + * Populate a default value if one is available and we still don't + * have a value. + */ + if (useDefaults && sanitizedValue === null && typeof options.default !== 'undefined') { + sanitizedValue = options.default; + } + + return sanitizedValue; +}; diff --git a/includes/classes/Feature/InstantResults/InstantResults.php b/includes/classes/Feature/InstantResults/InstantResults.php index e6ce55dfad..dec30b4d12 100644 --- a/includes/classes/Feature/InstantResults/InstantResults.php +++ b/includes/classes/Feature/InstantResults/InstantResults.php @@ -280,11 +280,13 @@ public function enqueue_frontend_assets() { 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, 'matchType' => $this->settings['match_type'], + 'paramPrefix' => 'ep-', 'postTypeLabels' => $this->get_post_type_labels(), ) ); @@ -659,6 +661,11 @@ public function get_facets() { ), ), ), + 'args' => array( + 'post_type' => array( + 'type' => 'strings', + ), + ), ); /** @@ -693,6 +700,11 @@ public function get_facets() { ), ), ), + 'args' => array( + $name => array( + 'type' => 'strings', + ), + ), ); } @@ -719,6 +731,14 @@ public function get_facets() { ), ), ), + 'args' => array( + 'max_price' => array( + 'type' => 'number', + ), + 'min_price' => array( + 'type' => 'number', + ), + ), ); } @@ -770,4 +790,57 @@ public function get_facets_for_admin() { return $facets; } + + /** + * Get schema for search args. + * + * @return array Search args schema. + */ + public function get_args_schema() { + $args = array( + 'highlight' => array( + 'type' => 'string', + 'default' => $this->settings['highlight_tag'], + 'allowedValues' => [ $this->settings['highlight_tag'] ], + ), + 'offset' => array( + 'type' => 'number', + 'default' => 0, + ), + 'orderby' => array( + 'type' => 'string', + 'default' => 'relevance', + 'allowedValues' => [ 'date', 'price', 'relevance' ], + ), + 'order' => array( + 'type' => 'string', + 'default' => 'desc', + 'allowedValues' => [ 'asc', 'desc' ], + ), + 'per_page' => array( + 'type' => 'number', + 'default' => 6, + ), + 'search' => array( + 'type' => 'string', + 'default' => '', + ), + 'relation' => array( + 'type' => 'string', + 'default' => 'and', + 'allowedValues' => [ 'and', 'or' ], + ), + ); + + $selected_facets = explode( ',', $this->settings['facets'] ); + $available_facets = $this->get_facets(); + + foreach ( $selected_facets as $key ) { + $facet = $available_facets[ $key ]; + + $args = array_merge( $args, $facet['args'] ); + } + + return $args; + } } From b1eb6234b23d78ebc66d87b26a480e887322076b Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Wed, 23 Feb 2022 14:43:28 +1100 Subject: [PATCH 04/12] Support post_type arg regardless of whether facet is present. --- .../Feature/InstantResults/InstantResults.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/includes/classes/Feature/InstantResults/InstantResults.php b/includes/classes/Feature/InstantResults/InstantResults.php index ceabe3bc2e..deafbd1049 100644 --- a/includes/classes/Feature/InstantResults/InstantResults.php +++ b/includes/classes/Feature/InstantResults/InstantResults.php @@ -661,11 +661,14 @@ public function get_facets() { ), ), ), - 'args' => array( - 'post_type' => array( - 'type' => 'strings', - ), - ), + /** + * The post_type arg needs to be supported regardless of whether + * the Post Type facet is present to be able to support setting the + * post type from the search form. + * + * @see ElasticPress\Feature\InstantResults::get_args_schema() + */ + 'args' => array(), ); /** @@ -821,6 +824,9 @@ public function get_args_schema() { 'type' => 'number', 'default' => 6, ), + 'post_type' => array( + 'type' => 'strings', + ), 'search' => array( 'type' => 'string', 'default' => '', From c8ee30511c798e5d53d708881569984d86a7febb Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Thu, 24 Feb 2022 16:39:59 +1100 Subject: [PATCH 05/12] Fix JSDoc related linting warnings. --- .../components/facets/taxonomy-terms-facet.js | 8 +++--- assets/js/instant-results/functions.js | 26 +++++++++---------- assets/js/instant-results/utilities.js | 24 ++++++++--------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/assets/js/instant-results/components/facets/taxonomy-terms-facet.js b/assets/js/instant-results/components/facets/taxonomy-terms-facet.js index b542afd673..da56f47008 100644 --- a/assets/js/instant-results/components/facets/taxonomy-terms-facet.js +++ b/assets/js/instant-results/components/facets/taxonomy-terms-facet.js @@ -16,11 +16,11 @@ import { ActiveContraint } from '../tools/active-constraints'; /** * Taxonomy filter component. * - * @param {Object} props Components props. + * @param {Object} props Components props. * @param {boolean} props.defaultIsOpen Whether the panel is open by default. - * @param {string} props.label Facet label. - * @param {string} props.name Facet name. - * @param {Array} props.postTypes Facet post types. + * @param {string} props.label Facet label. + * @param {string} props.name Facet name. + * @param {Array} props.postTypes Facet post types. * @return {WPElement} Component element. */ export default ({ defaultIsOpen, label, postTypes, name }) => { diff --git a/assets/js/instant-results/functions.js b/assets/js/instant-results/functions.js index acf0170590..e19641ae03 100644 --- a/assets/js/instant-results/functions.js +++ b/assets/js/instant-results/functions.js @@ -70,13 +70,13 @@ export const getPostTypesFromForm = (form) => { * Get permalink URL parameters from args. * * @typedef {Object} ArgSchema - * @property {string} type Arg type. - * @property {any} [default] Default arg value. - * @property {Array} [allowedValues] Array of allowed values. + * @property {string} type Arg type. + * @property {any} [default] Default arg value. + * @property {Array} [allowedValues] Array of allowed values. * - * @param {Object} args Args - * @param {ArgSchema} schema Args schema. - * @param {string} [prefix] Prefix to prepend to args. + * @param {Object} args Args + * @param {ArgSchema} schema Args schema. + * @param {string} [prefix] Prefix to prepend to args. * @return {URLSearchParams} URLSearchParams instance. */ export const getUrlParamsFromArgs = (args, schema, prefix = '') => { @@ -98,14 +98,14 @@ export const getUrlParamsFromArgs = (args, schema, prefix = '') => { * Build request args from URL parameters using a given schema. * * @typedef {Object} ArgSchema - * @property {string} type Arg type. - * @property {any} [default] Default arg value. - * @property {Array} [allowedValues] Array of allowed values. + * @property {string} type Arg type. + * @property {any} [default] Default arg value. + * @property {Array} [allowedValues] Array of allowed values. * - * @param {URLSearchParams} urlParams URL parameters. - * @param {object.} schema Schema to build args from. - * @param {string} [prefix] Parameter prefix. - * @param {boolean} [useDefaults] Whether to populate params with default values. + * @param {URLSearchParams} urlParams URL parameters. + * @param {object.} schema Schema to build args from. + * @param {string} [prefix] Parameter prefix. + * @param {boolean} [useDefaults] Whether to populate params with default values. * @return {object.} Query args. */ export const getArgsFromUrlParams = (urlParams, schema, prefix = '', useDefaults = true) => { diff --git a/assets/js/instant-results/utilities.js b/assets/js/instant-results/utilities.js index b84162a9c8..da444ca2cc 100644 --- a/assets/js/instant-results/utilities.js +++ b/assets/js/instant-results/utilities.js @@ -1,12 +1,12 @@ /** * Sanitize an argument value based on its type. * - * @param {*} value The value. - * @param {Object} options Sanitization options. - * @param {'integer'|'integers'|'string'|'strings'} options.type (optional) Value type. - * @param {Array} options.allowedValues (optional) Allowed values. - * @param {*} options.default (optional) Default value. - * @param {boolean} [useDefaults] Whether to return default values. + * @param {*} value The value. + * @param {Object} options Sanitization options. + * @param {'number'|'numbers'|'string'|'strings'} options.type (optional) Value type. + * @param {Array} options.allowedValues (optional) Allowed values. + * @param {*} options.default (optional) Default value. + * @param {boolean} [useDefaults] Whether to return default values. * @return {*} Sanitized value. */ export const sanitizeArg = (value, options, useDefaults = true) => { @@ -56,12 +56,12 @@ export const sanitizeArg = (value, options, useDefaults = true) => { /** * Sanitize a parameter value based on its type. * - * @param {*} value The value. - * @param {Object} options Sanitization options. - * @param {'integer'|'integers'|'string'|'strings'} options.type (optional) Value type. - * @param {Array} options.allowedValues (optional) Allowed values. - * @param {*} options.default (optional) Default value. - * @param {boolean} [useDefaults] Whether to return default values. + * @param {*} value The value. + * @param {Object} options Sanitization options. + * @param {'number'|'numbers'|'string'|'strings'} options.type (optional) Value type. + * @param {Array} options.allowedValues (optional) Allowed values. + * @param {*} options.default (optional) Default value. + * @param {boolean} [useDefaults] Whether to return default values. * @return {*} Sanitized value. */ export const sanitizeParam = (value, options, useDefaults = true) => { From 4a21aa0e5ab49734df01ec3c814784938fac006a Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Fri, 25 Feb 2022 16:42:27 +1100 Subject: [PATCH 06/12] Fixed an issue caused by incorrect or outdated facet keys. --- includes/classes/Feature/InstantResults/InstantResults.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/includes/classes/Feature/InstantResults/InstantResults.php b/includes/classes/Feature/InstantResults/InstantResults.php index 0115c4715f..9ea02dda8c 100644 --- a/includes/classes/Feature/InstantResults/InstantResults.php +++ b/includes/classes/Feature/InstantResults/InstantResults.php @@ -842,11 +842,10 @@ public function get_args_schema() { $available_facets = $this->get_facets(); foreach ( $selected_facets as $key ) { - $facet = $available_facets[ $key ]; - - $args = array_merge( $args, $facet['args'] ); + if ( isset( $available_facets[ $key ] ) ) { + $args = array_merge( $args, $available_facets[ $key ]['args'] ); + } } - return $args; } } From 482a046eaebc898f583db0cfa5e32149e910a5a6 Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Fri, 25 Feb 2022 17:38:07 +1100 Subject: [PATCH 07/12] Add missing key. Fixes crash in debug mode. --- assets/js/instant-results/components/layout.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/assets/js/instant-results/components/layout.js b/assets/js/instant-results/components/layout.js index bdd7d8b971..1400965a65 100644 --- a/assets/js/instant-results/components/layout.js +++ b/assets/js/instant-results/components/layout.js @@ -25,7 +25,7 @@ import Sort from './tools/sort'; */ export default () => { const { - state: { isLoading, isSidebarOpen }, + state: { isLoading }, } = useContext(Context); return ( @@ -41,10 +41,17 @@ export default () => {
- + - {facets.map((facet, index) => ( - + {facets.map(({ label, name, postTypes, type }, index) => ( + ))} From f5e0e52bdd98452836e2dcb1d5efde72b8eef91c Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Fri, 25 Feb 2022 17:38:29 +1100 Subject: [PATCH 08/12] Memoize context value. Fixes linting value. --- assets/js/instant-results/index.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/assets/js/instant-results/index.js b/assets/js/instant-results/index.js index a2e127f29b..b54d10326f 100644 --- a/assets/js/instant-results/index.js +++ b/assets/js/instant-results/index.js @@ -2,7 +2,15 @@ * WordPress dependencies. */ import { SlotFillProvider } from '@wordpress/components'; -import { render, useCallback, useEffect, useReducer, useRef, WPElement } from '@wordpress/element'; +import { + render, + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + WPElement, +} from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -210,8 +218,13 @@ const App = () => { state.args.search, ]); + /** + * Create context. + */ + const context = useMemo(() => ({ state, dispatch }), [state, dispatch]); + return ( - + Date: Fri, 25 Feb 2022 17:39:03 +1100 Subject: [PATCH 09/12] Move Show All button into render function. Fixes linting error. --- .../components/common/checkbox-list.js | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/assets/js/instant-results/components/common/checkbox-list.js b/assets/js/instant-results/components/common/checkbox-list.js index 41ff712d96..5f714fcd32 100644 --- a/assets/js/instant-results/components/common/checkbox-list.js +++ b/assets/js/instant-results/components/common/checkbox-list.js @@ -199,29 +199,6 @@ export default ({ disabled, label, options, onChange, selected, sortBy }) => { listEl.current.focus(); }; - /** - * Show all button component. - * - * @return {WPElement} Element. - */ - const ShowAllButton = () => - options.length > optionsLimit && ( - - {showAll - ? __('Show fewer options', 'elasticpress') - : sprintf( - /* translators: %d: Number of additional options available. */ - _n( - 'Show %d more option', - 'Show %d more options', - options.length - optionsLimit, - 'elasticpress', - ), - options.length - optionsLimit, - )} - - ); - return ( <> {options.length > 0 && ( @@ -241,7 +218,23 @@ export default ({ disabled, label, options, onChange, selected, sortBy }) => { } )} - + + {options.length > optionsLimit && ( + + {showAll + ? __('Show fewer options', 'elasticpress') + : sprintf( + /* translators: %d: Number of additional options available. */ + _n( + 'Show %d more option', + 'Show %d more options', + options.length - optionsLimit, + 'elasticpress', + ), + options.length - optionsLimit, + )} + + )} ); }; From 8d000aebdd70fe69b344c589bf3be53b9b45881c Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Fri, 25 Feb 2022 17:39:21 +1100 Subject: [PATCH 10/12] Replace empty Fragment with null. Fixes linting error. --- assets/js/instant-results/components/facets/facet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/instant-results/components/facets/facet.js b/assets/js/instant-results/components/facets/facet.js index 02efbaad86..7f35a52544 100644 --- a/assets/js/instant-results/components/facets/facet.js +++ b/assets/js/instant-results/components/facets/facet.js @@ -39,6 +39,6 @@ export default ({ index, label, name, postTypes, type }) => { /> ); default: - return <>; + return null; } }; From 99ceb85c424aa721eed597f4aadafb046343f249 Mon Sep 17 00:00:00 2001 From: Jacob Peattie Date: Fri, 25 Feb 2022 17:43:50 +1100 Subject: [PATCH 11/12] Fix search input not changing on popped state. --- .../components/facets/search-term-facet.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/assets/js/instant-results/components/facets/search-term-facet.js b/assets/js/instant-results/components/facets/search-term-facet.js index 4fafa835b0..885c99885a 100644 --- a/assets/js/instant-results/components/facets/search-term-facet.js +++ b/assets/js/instant-results/components/facets/search-term-facet.js @@ -1,7 +1,7 @@ /** * WordPress dependencies. */ -import { useContext, useState, WPElement } from '@wordpress/element'; +import { useContext, useEffect, useState, WPElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -51,6 +51,19 @@ export default () => { dispatch({ type: 'NEW_SEARCH_TERM', payload: '' }); }; + /** + * Handle an external change to the search value, such as from popping + * state. + */ + const handleSearch = () => { + setValue(search); + }; + + /** + * Effects. + */ + useEffect(handleSearch, [search]); + return ( <> Date: Fri, 25 Feb 2022 17:44:35 +1100 Subject: [PATCH 12/12] Remove unused property defintition. Fixes linting error. --- assets/js/blocks/related-posts/Edit.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/assets/js/blocks/related-posts/Edit.js b/assets/js/blocks/related-posts/Edit.js index 0195f64985..5358029191 100644 --- a/assets/js/blocks/related-posts/Edit.js +++ b/assets/js/blocks/related-posts/Edit.js @@ -36,10 +36,9 @@ class Edit extends Component { // Use 0 if in the Widgets Screen const postId = wp.data.select('core/editor').getCurrentPostId() ?? 0; - this.fetchRequest = wp - .apiFetch({ - path: addQueryArgs(`/wp/v2/posts/${postId}/related`, urlArgs), - }) + wp.apiFetch({ + path: addQueryArgs(`/wp/v2/posts/${postId}/related`, urlArgs), + }) .then((posts) => { this.setState({ posts }); })