From 691ef87f2af580cc54f5be4b0a8f8d047c792708 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Thu, 1 Jul 2021 14:46:11 +0200 Subject: [PATCH] feat(DynamicWidgets): add implementation (#3056) * feat(DynamicWidgets): add implementation **Summary** Adds a new widget _ExperimentalDynamicWidgets_ that can be used to conditionally render other widgets. This condition is based on the search results. At the moment there isn't yet a default way to enforce facet ordering in the Algolia engine, thus the data available to `transformItems` is an empty array. This will change once the Algolia engine adds support for facet ordering and this widget will move out of experimental mode. **Result** ```jsx { // add a condition based on the results, eg. if you add the ordering via a query rule: return results.userData[0].facetOrdering; }} > ``` See also: - https://github.com/algolia/instantsearch.js/pull/4687 - https://github.com/algolia/vue-instantsearch/pull/922 - DX-1665 * fix TS * feat: retrieve from results * update dependencies * read from facet ordering * expose widget from root * change error message and test more * make and expose a dom component * update bundlesizes * clarify test * redo some of the tests --- examples/autocomplete/package.json | 2 +- examples/default-theme/package.json | 2 +- examples/e-commerce/package.json | 2 +- examples/geo-search/package.json | 2 +- examples/media/package.json | 2 +- examples/multi-index/package.json | 2 +- examples/next/package.json | 2 +- .../package.json | 2 +- examples/react-native/package.json | 2 +- examples/react-router-v3/package.json | 2 +- examples/react-router/package.json | 2 +- examples/server-side-rendering/package.json | 2 +- examples/tourism/package.json | 2 +- package.json | 15 +- .../react-instantsearch-core/package.json | 2 +- .../__tests__/connectDynamicWidgets.js | 302 ++++++++++++++++ .../src/connectors/connectDynamicWidgets.ts | 37 ++ .../react-instantsearch-core/src/index.ts | 6 + .../src/widgets/DynamicWidgets.tsx | 52 +++ .../src/widgets/InstantSearch.tsx | 3 +- .../widgets/__tests__/DynamicWidgets.test.tsx | 331 ++++++++++++++++++ packages/react-instantsearch-dom/package.json | 2 +- packages/react-instantsearch-dom/src/index.js | 4 + .../src/widgets/DynamicWidgets.js | 15 + .../widgets/__tests__/DynamicWidgets.test.tsx | 107 ++++++ .../react-instantsearch-native/src/index.ts | 2 + packages/react-instantsearch/connectors.js | 1 + packages/react-instantsearch/dom.js | 1 + packages/react-instantsearch/native.js | 1 + stories/DynamicWidgets.stories.js | 45 +++ yarn.lock | 258 +++++++------- 31 files changed, 1065 insertions(+), 145 deletions(-) create mode 100644 packages/react-instantsearch-core/src/connectors/__tests__/connectDynamicWidgets.js create mode 100644 packages/react-instantsearch-core/src/connectors/connectDynamicWidgets.ts create mode 100644 packages/react-instantsearch-core/src/widgets/DynamicWidgets.tsx create mode 100644 packages/react-instantsearch-core/src/widgets/__tests__/DynamicWidgets.test.tsx create mode 100644 packages/react-instantsearch-dom/src/widgets/DynamicWidgets.js create mode 100644 packages/react-instantsearch-dom/src/widgets/__tests__/DynamicWidgets.test.tsx create mode 100644 stories/DynamicWidgets.stories.js diff --git a/examples/autocomplete/package.json b/examples/autocomplete/package.json index 2732fbd313..e59281d1a9 100644 --- a/examples/autocomplete/package.json +++ b/examples/autocomplete/package.json @@ -13,7 +13,7 @@ "react-test-renderer": "16.8.6" }, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "antd": "3.23.2", "lodash": "4.17.15", "prop-types": "15.6.0", diff --git a/examples/default-theme/package.json b/examples/default-theme/package.json index 17270f0ec4..0792bad511 100644 --- a/examples/default-theme/package.json +++ b/examples/default-theme/package.json @@ -3,7 +3,7 @@ "version": "6.11.2", "private": true, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "qs": "6.8.0", "react": "16.8.6", "react-dom": "16.8.6", diff --git a/examples/e-commerce/package.json b/examples/e-commerce/package.json index cf13f06424..796a9dbd30 100644 --- a/examples/e-commerce/package.json +++ b/examples/e-commerce/package.json @@ -3,7 +3,7 @@ "version": "6.11.2", "private": true, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "classnames": "2.2.6", "qs": "6.8.0", "react": "16.8.6", diff --git a/examples/geo-search/package.json b/examples/geo-search/package.json index 9cfa2c9711..b9c6d94f5a 100644 --- a/examples/geo-search/package.json +++ b/examples/geo-search/package.json @@ -13,7 +13,7 @@ "react-test-renderer": "16.8.6" }, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "instantsearch.css": "7.3.1", "qs": "6.8.0", "react": "16.8.6", diff --git a/examples/media/package.json b/examples/media/package.json index dc335761eb..5e55bb02fe 100644 --- a/examples/media/package.json +++ b/examples/media/package.json @@ -3,7 +3,7 @@ "version": "6.11.2", "private": true, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "qs": "6.8.0", "react": "16.8.6", "react-dom": "16.8.6", diff --git a/examples/multi-index/package.json b/examples/multi-index/package.json index f4328fbae2..26b350dd05 100644 --- a/examples/multi-index/package.json +++ b/examples/multi-index/package.json @@ -13,7 +13,7 @@ "react-test-renderer": "16.8.6" }, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "instantsearch.css": "7.3.1", "prop-types": "15.6.0", "react": "16.8.6", diff --git a/examples/next/package.json b/examples/next/package.json index 7806c0e3bd..d7248c19ca 100644 --- a/examples/next/package.json +++ b/examples/next/package.json @@ -18,7 +18,7 @@ "style-loader": "1.0.0" }, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "next": "9.1.1", "prop-types": "15.6.2", "qs": "6.8.0", diff --git a/examples/react-native-query-suggestions/package.json b/examples/react-native-query-suggestions/package.json index 52b83ab65c..3d8bb1d829 100644 --- a/examples/react-native-query-suggestions/package.json +++ b/examples/react-native-query-suggestions/package.json @@ -12,7 +12,7 @@ "test": "jest" }, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "expo": "37.0.3", "lodash": "4.17.15", "prop-types": "15.6.2", diff --git a/examples/react-native/package.json b/examples/react-native/package.json index 0033a07564..a1038c7706 100644 --- a/examples/react-native/package.json +++ b/examples/react-native/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@ptomasroos/react-native-multi-slider": "2.2.2", - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "expo": "37.0.3", "lodash": "4.17.15", "prop-types": "15.6.2", diff --git a/examples/react-router-v3/package.json b/examples/react-router-v3/package.json index 2fe7845929..1b5bf79f86 100644 --- a/examples/react-router-v3/package.json +++ b/examples/react-router-v3/package.json @@ -13,7 +13,7 @@ "react-test-renderer": "16.8.6" }, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "instantsearch.css": "7.3.1", "lodash": "4.17.15", "prop-types": "15.6.0", diff --git a/examples/react-router/package.json b/examples/react-router/package.json index 3e80e1fc50..96f53b66a9 100644 --- a/examples/react-router/package.json +++ b/examples/react-router/package.json @@ -14,7 +14,7 @@ "react-test-renderer": "16.8.6" }, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "instantsearch.css": "7.3.1", "lodash": "4.17.15", "prop-types": "15.6.0", diff --git a/examples/server-side-rendering/package.json b/examples/server-side-rendering/package.json index 16348d8274..11be27cbe6 100644 --- a/examples/server-side-rendering/package.json +++ b/examples/server-side-rendering/package.json @@ -26,7 +26,7 @@ "webpack-node-externals": "1.7.2" }, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "express": "4.17.1", "prop-types": "15.6.0", "react": "16.8.6", diff --git a/examples/tourism/package.json b/examples/tourism/package.json index e03ff74975..52dc8011cd 100644 --- a/examples/tourism/package.json +++ b/examples/tourism/package.json @@ -3,7 +3,7 @@ "version": "6.11.2", "private": true, "dependencies": { - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "qs": "6.8.0", "react": "16.8.6", "react-dom": "16.8.6", diff --git a/package.json b/package.json index 2afdc5dd12..5c5cc9405a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "website:build": "yarn build && yarn webpack --config website/webpack.config.js" }, "devDependencies": { - "@algolia/client-search": "4.8.5", + "@algolia/client-search": "4.9.3", "@babel/cli": "7.4.4", "@babel/core": "7.4.5", "@babel/plugin-proposal-class-properties": "7.4.4", @@ -72,7 +72,7 @@ "@wdio/selenium-standalone-service": "5.16.10", "@wdio/spec-reporter": "5.16.11", "@wdio/static-server-service": "5.16.10", - "algoliasearch": "4.8.5", + "algoliasearch": "4.9.3", "argos-cli": "0.1.3", "babel-eslint": "10.0.2", "babel-jest": "24.9.0", @@ -114,6 +114,7 @@ "react": "16.8.6", "react-autosuggest": "9.4.3", "react-dom": "16.8.6", + "react-error-boundary": "3.1.3", "react-instantsearch-dom": "6.10.3", "react-instantsearch-dom-maps": "6.10.3", "react-native": "0.58.4", @@ -136,23 +137,23 @@ "bundlesize": [ { "path": "packages/react-instantsearch/dist/umd/Core.min.js", - "maxSize": "2.95 kB" + "maxSize": "3 kB" }, { "path": "packages/react-instantsearch/dist/umd/Connectors.min.js", - "maxSize": "22.1 kB" + "maxSize": "22.5 kB" }, { "path": "packages/react-instantsearch/dist/umd/Dom.min.js", - "maxSize": "34.90 kB" + "maxSize": "35.5 kB" }, { "path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js", - "maxSize": "25.80 kB" + "maxSize": "26.5 kB" }, { "path": "packages/react-instantsearch-dom/dist/umd/ReactInstantSearchDOM.min.js", - "maxSize": "38.41 kB" + "maxSize": "39 kB" }, { "path": "packages/react-instantsearch-dom-maps/dist/umd/ReactInstantSearchDOMMaps.min.js", diff --git a/packages/react-instantsearch-core/package.json b/packages/react-instantsearch-core/package.json index 3e9b5f0a75..a0ec290662 100644 --- a/packages/react-instantsearch-core/package.json +++ b/packages/react-instantsearch-core/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@babel/runtime": "^7.1.2", - "algoliasearch-helper": "^3.4.3", + "algoliasearch-helper": "^3.5.3", "prop-types": "^15.6.2", "react-fast-compare": "^3.0.0" }, diff --git a/packages/react-instantsearch-core/src/connectors/__tests__/connectDynamicWidgets.js b/packages/react-instantsearch-core/src/connectors/__tests__/connectDynamicWidgets.js new file mode 100644 index 0000000000..74b9638346 --- /dev/null +++ b/packages/react-instantsearch-core/src/connectors/__tests__/connectDynamicWidgets.js @@ -0,0 +1,302 @@ +import { SearchResults, SearchParameters } from 'algoliasearch-helper'; +import connector from '../connectDynamicWidgets'; + +jest.mock('../../core/createConnector', () => x => x); + +const EMPTY_RESPONSE = { + results: [ + { + hits: [], + nbHits: 0, + page: 0, + nbPages: 0, + hitsPerPage: 20, + exhaustiveNbHits: true, + query: '', + queryAfterRemoval: '', + params: + 'highlightPreTag=%3Cais-highlight-0000000000%3E&highlightPostTag=%3C%2Fais-highlight-0000000000%3E&query=&facets=%5B%5D&tagFilters=', + index: 'instant_search', + processingTimeMS: 2, + }, + ], +}; + +describe('connectDynamicWidgets', () => { + const empty = {}; + + describe('single index', () => { + const contextValue = { + mainTargetedIndex: 'index', + }; + + const createSingleIndexSearchResults = (result = {}, state) => ({ + results: new SearchResults(new SearchParameters(state), [ + { + ...EMPTY_RESPONSE.results[0], + ...result, + }, + ]), + }); + + describe('getProvidedProps', () => { + it('empty results', () => { + const props = { contextValue }; + const searchState = {}; + const searchResults = empty; + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + const expectation = { + attributesToRender: [], + }; + + expect(actual).toEqual(expectation); + }); + + it('attributesToRender is the return value of transformItems', () => { + const returnValue = ['test1', 'test2']; + const props = { contextValue, transformItems: () => returnValue }; + const searchState = {}; + const searchResults = createSingleIndexSearchResults(); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(actual.attributesToRender).toEqual(returnValue); + }); + + it('default items is []', () => { + const props = { contextValue, transformItems: items => items }; + const searchState = {}; + const searchResults = createSingleIndexSearchResults(); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(actual.attributesToRender).toEqual([]); + }); + + it('reads from facetOrdering by default', () => { + const props = { + contextValue, + ...connector.defaultProps, + }; + const searchState = {}; + const searchResults = createSingleIndexSearchResults({ + renderingContent: { + facetOrdering: { facet: { order: ['one', 'two'] } }, + }, + }); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(actual.attributesToRender).toEqual(['one', 'two']); + }); + + it('transformItems gets called with results', () => { + const props = { + contextValue, + transformItems: jest.fn(items => items), + }; + const searchState = {}; + const searchResults = createSingleIndexSearchResults({ + userData: [{ MOCK_FACET_ORDER: ['one', 'two'] }], + }); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(props.transformItems).toHaveBeenCalledTimes(1); + expect(props.transformItems).toHaveBeenCalledWith([], { + results: searchResults.results, + }); + + expect(actual.attributesToRender).toEqual([]); + }); + + it('userData is usable as a source for transformItems', () => { + const props = { + contextValue, + transformItems: (_items, { results }) => + results.userData[0].MOCK_FACET_ORDER, + }; + const searchState = {}; + const searchResults = createSingleIndexSearchResults({ + userData: [{ MOCK_FACET_ORDER: ['one', 'two'] }], + }); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(actual.attributesToRender).toEqual(['one', 'two']); + }); + }); + }); + + describe('multi index', () => { + const contextValue = { mainTargetedIndex: 'first' }; + const indexContextValue = { targetedIndex: 'second' }; + + const createMultiIndexSearchState = (state = {}) => ({ + indices: { + second: state, + }, + }); + + const createMultiIndexSearchResults = (result = {}, state) => ({ + results: { + second: new SearchResults(new SearchParameters(state), [ + { + ...EMPTY_RESPONSE.results[0], + ...result, + }, + ]), + }, + }); + + describe('getProvidedProps', () => { + it('empty results', () => { + const searchState = createMultiIndexSearchState(); + const props = { contextValue, indexContextValue }; + + const searchResults = empty; + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + const expectation = { + attributesToRender: [], + }; + + expect(actual).toEqual(expectation); + }); + + it('attributesToRender is the return value of transformItems', () => { + const returnValue = ['one', 'two']; + const props = { + contextValue, + indexContextValue, + transformItems: () => returnValue, + }; + const searchState = createMultiIndexSearchState(); + const searchResults = createMultiIndexSearchResults(); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(actual.attributesToRender).toEqual(returnValue); + }); + + it('default items is []', () => { + const props = { + contextValue, + indexContextValue, + transformItems: items => items, + }; + const searchState = createMultiIndexSearchState(); + const searchResults = createMultiIndexSearchResults(); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(actual.attributesToRender).toEqual([]); + }); + + it('reads from facetOrdering by default', () => { + const props = { + contextValue, + indexContextValue, + transformItems: items => items, + }; + const searchState = createMultiIndexSearchState(); + const searchResults = createMultiIndexSearchResults({ + renderingContent: { + facetOrdering: { facet: { order: ['one', 'two'] } }, + }, + }); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(actual.attributesToRender).toEqual(['one', 'two']); + }); + + it('transformItems gets called with results', () => { + const props = { + contextValue, + indexContextValue, + transformItems: jest.fn(items => items), + }; + const searchState = createMultiIndexSearchState(); + const searchResults = createMultiIndexSearchResults(); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(props.transformItems).toHaveBeenCalledTimes(1); + expect(props.transformItems).toHaveBeenCalledWith([], { + results: searchResults.results.second, + }); + + expect(actual.attributesToRender).toEqual([]); + }); + + it('userData is usable as a source for transformItems', () => { + const props = { + contextValue, + indexContextValue, + transformItems: (_items, { results }) => + results.userData[0].MOCK_FACET_ORDER, + }; + const searchState = createMultiIndexSearchState(); + const searchResults = createMultiIndexSearchResults({ + userData: [{ MOCK_FACET_ORDER: ['one', 'two'] }], + }); + + const actual = connector.getProvidedProps( + props, + searchState, + searchResults + ); + + expect(actual.attributesToRender).toEqual(['one', 'two']); + }); + }); + }); +}); diff --git a/packages/react-instantsearch-core/src/connectors/connectDynamicWidgets.ts b/packages/react-instantsearch-core/src/connectors/connectDynamicWidgets.ts new file mode 100644 index 0000000000..470ef6cc66 --- /dev/null +++ b/packages/react-instantsearch-core/src/connectors/connectDynamicWidgets.ts @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import createConnector from '../core/createConnector'; +import { getResults } from '../core/indexUtils'; + +export default createConnector({ + displayName: 'AlgoliaDynamicWidgets', + + defaultProps: { + transformItems: items => items, + }, + + propTypes: { + transformItems: PropTypes.func, + }, + + getProvidedProps(props, _searchState, searchResults) { + const results = getResults(searchResults, { + ais: props.contextValue, + multiIndexContext: props.indexContextValue, + }); + + if (!results) { + return { attributesToRender: [] }; + } + + const facetOrder = + (results.renderingContent && + results.renderingContent.facetOrdering && + results.renderingContent.facetOrdering.facet && + results.renderingContent.facetOrdering.facet.order) || + []; + + return { + attributesToRender: props.transformItems(facetOrder, { results }), + }; + }, +}); diff --git a/packages/react-instantsearch-core/src/index.ts b/packages/react-instantsearch-core/src/index.ts index 6cb712b172..d6fb1d2ecc 100644 --- a/packages/react-instantsearch-core/src/index.ts +++ b/packages/react-instantsearch-core/src/index.ts @@ -16,6 +16,9 @@ export { default as Configure } from './widgets/Configure'; export { default as ExperimentalConfigureRelatedItems, } from './widgets/ConfigureRelatedItems'; +export { + default as ExperimentalDynamicWidgets, +} from './widgets/DynamicWidgets'; export { default as QueryRuleContext } from './widgets/QueryRuleContext'; export { default as Index } from './widgets/Index'; export { default as InstantSearch } from './widgets/InstantSearch'; @@ -32,6 +35,9 @@ export { export { default as connectCurrentRefinements, } from './connectors/connectCurrentRefinements'; +export { + default as EXPERIMENTAL_connectDynamicWidgets, +} from './connectors/connectDynamicWidgets'; export { default as connectGeoSearch } from './connectors/connectGeoSearch'; export { default as connectHierarchicalMenu, diff --git a/packages/react-instantsearch-core/src/widgets/DynamicWidgets.tsx b/packages/react-instantsearch-core/src/widgets/DynamicWidgets.tsx new file mode 100644 index 0000000000..142b9ce398 --- /dev/null +++ b/packages/react-instantsearch-core/src/widgets/DynamicWidgets.tsx @@ -0,0 +1,52 @@ +import React, { Fragment, ReactChild, ReactNode } from 'react'; +import { getDisplayName } from '../core/utils'; +import connectDynamicWidgets from '../connectors/connectDynamicWidgets'; + +function getAttribute(component: ReactChild): string | undefined { + if (typeof component !== 'object') { + return undefined; + } + + if (component.props.attribute) { + return component.props.attribute; + } + if (Array.isArray(component.props.attributes)) { + return component.props.attributes[0]; + } + if (component.props.children) { + return getAttribute(React.Children.only(component.props.children)); + } + + return undefined; +} + +type DynamicWidgetsProps = { + children: ReactNode; + attributesToRender: string[]; +}; + +function DynamicWidgets({ children, attributesToRender }: DynamicWidgetsProps) { + const widgets: Map = new Map(); + + React.Children.forEach(children, child => { + const attribute = getAttribute(child); + if (!attribute) { + throw new Error( + `Could not find "attribute" prop for ${getDisplayName(child)}.` + ); + } + widgets.set(attribute, child); + }); + + // on initial render this will be empty, but React InstantSearch keeps + // search state for unmounted components in place, so routing works. + return ( + <> + {attributesToRender.map(attribute => ( + {widgets.get(attribute)} + ))} + + ); +} + +export default connectDynamicWidgets(DynamicWidgets); diff --git a/packages/react-instantsearch-core/src/widgets/InstantSearch.tsx b/packages/react-instantsearch-core/src/widgets/InstantSearch.tsx index 25262bdcac..ab63d161a7 100644 --- a/packages/react-instantsearch-core/src/widgets/InstantSearch.tsx +++ b/packages/react-instantsearch-core/src/widgets/InstantSearch.tsx @@ -13,6 +13,7 @@ import { MultiResponse } from '../types/algoliasearch'; import { ConnectorDescription } from '../core/createConnector'; type ResultsState = { + metadata: never[]; state: PlainSearchParameters; rawResults: MultiResponse; }; @@ -62,7 +63,7 @@ type Props = { props: object; }) => void; stalledSearchDelay?: number; - resultsState: ResultsState | { [indexId: string]: ResultsState }; + resultsState?: ResultsState | { [indexId: string]: ResultsState }; }; type State = { diff --git a/packages/react-instantsearch-core/src/widgets/__tests__/DynamicWidgets.test.tsx b/packages/react-instantsearch-core/src/widgets/__tests__/DynamicWidgets.test.tsx new file mode 100644 index 0000000000..0564c0ea00 --- /dev/null +++ b/packages/react-instantsearch-core/src/widgets/__tests__/DynamicWidgets.test.tsx @@ -0,0 +1,331 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { + connectHierarchicalMenu, + connectPagination, + connectRefinementList, +} from '../..'; +import { Panel } from 'react-instantsearch-dom'; +import DynamicWidgets from '../DynamicWidgets'; +import InstantSearch from '../InstantSearch'; + +const EMPTY_RESPONSE = { + results: [ + { + hits: [], + nbHits: 0, + page: 0, + nbPages: 0, + hitsPerPage: 20, + exhaustiveNbHits: true, + query: '', + queryAfterRemoval: '', + params: + 'highlightPreTag=%3Cais-highlight-0000000000%3E&highlightPostTag=%3C%2Fais-highlight-0000000000%3E&query=&facets=%5B%5D&tagFilters=', + index: 'instant_search', + processingTimeMS: 2, + }, + ], +}; + +const createSearchClient = () => ({ + search: jest.fn(() => Promise.resolve(EMPTY_RESPONSE)), + searchForFacetValues: jest.fn(() => Promise.resolve({})), +}); + +const RefinementList = connectRefinementList( + ({ attribute }) => `RefinementList(${attribute})` +); + +const HierarchicalMenu = connectHierarchicalMenu( + ({ attributes }) => `HierarchicalMenu(${attributes.join(',')})` +); + +const Pagination = connectPagination(() => { + return
pagination
; +}); + +describe('DynamicWidgets', () => { + describe('before results', () => { + test('does not render the result of transformItems', () => { + const searchClient = createSearchClient(); + + const { container } = render( + + ['test1']}> + + + + ); + + expect(container).toMatchInlineSnapshot(`
`); + }); + }); + + describe('with results', () => { + const resultsState = { + metadata: [], + rawResults: EMPTY_RESPONSE.results, + state: { + index: 'instant_search', + query: '', + }, + }; + + test('default items is empty', () => { + const searchClient = createSearchClient(); + + const { container } = render( + + items}> + + + + ); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + test('renders return of transformItems', () => { + const searchClient = createSearchClient(); + + const { container } = render( + + ['test1']}> + + + + + ); + + expect(container).toMatchInlineSnapshot(` +
+ RefinementList(test1) +
+ `); + }); + + test('renders from results', () => { + const searchClient = createSearchClient(); + + const RESULTS = [ + { + ...resultsState, + rawResults: [ + { + ...resultsState.rawResults[0], + renderingContent: { + facetOrdering: { facet: { order: ['test1', 'test2'] } }, + }, + }, + ], + }, + + { + ...resultsState, + rawResults: [ + { + ...resultsState.rawResults[0], + renderingContent: { + facetOrdering: { facet: { order: ['test2', 'test1'] } }, + }, + }, + ], + }, + + { + ...resultsState, + rawResults: [ + { + ...resultsState.rawResults[0], + renderingContent: { + facetOrdering: { facet: { order: ['test1'] } }, + }, + }, + ], + }, + ]; + + const Component = ({ + result, + }: { + result: React.ComponentProps['resultsState']; + }) => ( + + + + + + + ); + { + const { container } = render( + + ); + + expect(container).toMatchInlineSnapshot(` +
+ RefinementList(test1) + HierarchicalMenu(test2,test3) +
+ `); + } + { + const { container } = render( + + ); + + expect(container).toMatchInlineSnapshot(` +
+ HierarchicalMenu(test2,test3) + RefinementList(test1) +
+ `); + } + { + const { container } = render( + + ); + + expect(container).toMatchInlineSnapshot(` +
+ RefinementList(test1) +
+ `); + } + }); + + test('renders items in panel', () => { + const searchClient = createSearchClient(); + + const { container } = render( + + ['test1', 'test3']}> + + + + + + + + + + + ); + + expect(container).toMatchInlineSnapshot(` +
+ RefinementList(test1) +
+
+ RefinementList(test3) +
+
+
+ `); + }); + + test("does not render items that aren't directly in children", () => { + const fallbackRender = jest.fn(() => null); + + // prevent duplicate console errors still showing up + const spy = jest.spyOn(console, 'error'); + spy.mockImplementation(() => {}); + + const searchClient = createSearchClient(); + + const Wrapped = ({ attr }: { attr: string }) => ( +
+ +
+ ); + + render( + + + ['test1']}> + + + + + ); + + // note: this test now expects a failure, but if we ever implement deep + // fetching of attribute, we expect this to render Wrapped + expect( + (fallbackRender.mock.calls[0] as any)[0].error + ).toMatchInlineSnapshot( + `[Error: Could not find "attribute" prop for UnknownComponent.]` + ); + }); + + test('does not render items non-attribute widgets', () => { + const fallbackRender = jest.fn(() => null); + + // prevent duplicate console errors still showing up + const spy = jest.spyOn(console, 'error'); + spy.mockImplementation(() => {}); + + const searchClient = createSearchClient(); + + render( + + + ['test1']}> + + + + + ); + + expect( + (fallbackRender.mock.calls[0] as any)[0].error + ).toMatchInlineSnapshot( + `[Error: Could not find "attribute" prop for UnknownComponent.]` + ); + }); + }); +}); diff --git a/packages/react-instantsearch-dom/package.json b/packages/react-instantsearch-dom/package.json index 4f76f53518..d3195d54fb 100644 --- a/packages/react-instantsearch-dom/package.json +++ b/packages/react-instantsearch-dom/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@babel/runtime": "^7.1.2", - "algoliasearch-helper": "^3.4.3", + "algoliasearch-helper": "^3.5.3", "classnames": "^2.2.5", "prop-types": "^15.6.2", "react-fast-compare": "^3.0.0", diff --git a/packages/react-instantsearch-dom/src/index.js b/packages/react-instantsearch-dom/src/index.js index 1181caa4e3..a3d13c0028 100644 --- a/packages/react-instantsearch-dom/src/index.js +++ b/packages/react-instantsearch-dom/src/index.js @@ -18,6 +18,7 @@ export { EXPERIMENTAL_connectConfigureRelatedItems, } from 'react-instantsearch-core'; export { connectCurrentRefinements } from 'react-instantsearch-core'; +export { EXPERIMENTAL_connectDynamicWidgets } from 'react-instantsearch-core'; export { connectGeoSearch } from 'react-instantsearch-core'; export { connectHierarchicalMenu } from 'react-instantsearch-core'; export { connectHighlight } from 'react-instantsearch-core'; @@ -69,6 +70,9 @@ export { default as ToggleRefinement } from './widgets/ToggleRefinement'; export { default as VoiceSearch } from './widgets/VoiceSearch'; export { default as QueryRuleCustomData } from './widgets/QueryRuleCustomData'; export { default as EXPERIMENTAL_Answers } from './widgets/Answers'; +export { + default as ExperimentalDynamicWidgets, +} from './widgets/DynamicWidgets'; // hooks export { default as EXPERIMENTAL_useAnswers } from './hooks/useAnswers'; diff --git a/packages/react-instantsearch-dom/src/widgets/DynamicWidgets.js b/packages/react-instantsearch-dom/src/widgets/DynamicWidgets.js new file mode 100644 index 0000000000..c20b09c39a --- /dev/null +++ b/packages/react-instantsearch-dom/src/widgets/DynamicWidgets.js @@ -0,0 +1,15 @@ +/* eslint-disable react/prop-types */ +import React from 'react'; +import { ExperimentalDynamicWidgets as CoreDynamicWidgets } from 'react-instantsearch-core'; +import classNames from 'classnames'; +import { createClassNames } from '../core/utils'; + +const cx = createClassNames('DynamicWidgets'); + +export default function DynamicWidgets({ children, className, ...props }) { + return ( +
+ {children} +
+ ); +} diff --git a/packages/react-instantsearch-dom/src/widgets/__tests__/DynamicWidgets.test.tsx b/packages/react-instantsearch-dom/src/widgets/__tests__/DynamicWidgets.test.tsx new file mode 100644 index 0000000000..8c5465afd9 --- /dev/null +++ b/packages/react-instantsearch-dom/src/widgets/__tests__/DynamicWidgets.test.tsx @@ -0,0 +1,107 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import { + ExperimentalDynamicWidgets as CoreDynamicWidgets, + InstantSearch, +} from 'react-instantsearch-core'; +import DynamicWidgets from '../DynamicWidgets'; + +jest.mock('react-instantsearch-core', () => { + const original = jest.requireActual('react-instantsearch-core'); + return { + ...original, + ExperimentalDynamicWidgets: jest.fn(() => null), + }; +}); + +const EMPTY_RESPONSE = { + results: [ + { + hits: [], + nbHits: 0, + page: 0, + nbPages: 0, + hitsPerPage: 20, + exhaustiveNbHits: true, + query: '', + queryAfterRemoval: '', + params: + 'highlightPreTag=%3Cais-highlight-0000000000%3E&highlightPostTag=%3C%2Fais-highlight-0000000000%3E&query=&facets=%5B%5D&tagFilters=', + index: 'instant_search', + processingTimeMS: 2, + }, + ], +}; + +const createSearchClient = () => ({ + search: jest.fn(() => Promise.resolve(EMPTY_RESPONSE)), + searchForFacetValues: jest.fn(() => Promise.resolve({})), +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('DynamicWidgets', () => { + it('sets a className', () => { + const searchClient = createSearchClient(); + + const { container } = render( + + + + ); + + expect(container).toMatchInlineSnapshot(` +
+
+
+ `); + }); + + it('allows a custom className', () => { + const searchClient = createSearchClient(); + + const { container } = render( + + + + ); + + expect(container).toMatchInlineSnapshot(` +
+
+
+ `); + }); + + it('forwards props to core widget', () => { + const searchClient = createSearchClient(); + + const children = [
testing
,
testing2
]; + + const transformItems = () => {}; + + render( + + + {children} + + + ); + + expect(CoreDynamicWidgets).toHaveBeenCalledTimes(1); + expect(CoreDynamicWidgets).toHaveBeenCalledWith( + { children, transformItems, unknownOption: true }, + {} + ); + }); +}); diff --git a/packages/react-instantsearch-native/src/index.ts b/packages/react-instantsearch-native/src/index.ts index 3f92653c6c..5bdcc6fbf7 100644 --- a/packages/react-instantsearch-native/src/index.ts +++ b/packages/react-instantsearch-native/src/index.ts @@ -6,6 +6,7 @@ export { translatable } from 'react-instantsearch-core'; // Widget export { Configure } from 'react-instantsearch-core'; export { ExperimentalConfigureRelatedItems } from 'react-instantsearch-core'; +export { ExperimentalDynamicWidgets } from 'react-instantsearch-core'; export { QueryRuleContext } from 'react-instantsearch-core'; export { Index } from 'react-instantsearch-core'; export { InstantSearch } from 'react-instantsearch-core'; @@ -17,6 +18,7 @@ export { connectConfigure } from 'react-instantsearch-core'; export { EXPERIMENTAL_connectConfigureRelatedItems, } from 'react-instantsearch-core'; +export { EXPERIMENTAL_connectDynamicWidgets } from 'react-instantsearch-core'; export { connectCurrentRefinements } from 'react-instantsearch-core'; export { connectGeoSearch } from 'react-instantsearch-core'; export { connectHierarchicalMenu } from 'react-instantsearch-core'; diff --git a/packages/react-instantsearch/connectors.js b/packages/react-instantsearch/connectors.js index 675a194583..ca3b25ae12 100644 --- a/packages/react-instantsearch/connectors.js +++ b/packages/react-instantsearch/connectors.js @@ -4,6 +4,7 @@ export { connectConfigure } from 'react-instantsearch-core'; export { EXPERIMENTAL_connectConfigureRelatedItems, } from 'react-instantsearch-core'; +export { EXPERIMENTAL_connectDynamicWidgets } from 'react-instantsearch-core'; export { connectCurrentRefinements } from 'react-instantsearch-core'; export { connectGeoSearch } from 'react-instantsearch-core'; export { connectHierarchicalMenu } from 'react-instantsearch-core'; diff --git a/packages/react-instantsearch/dom.js b/packages/react-instantsearch/dom.js index be4ad5a2a8..3d48e68f44 100644 --- a/packages/react-instantsearch/dom.js +++ b/packages/react-instantsearch/dom.js @@ -4,6 +4,7 @@ export { Breadcrumb } from 'react-instantsearch-dom'; export { ClearRefinements } from 'react-instantsearch-dom'; export { Configure } from 'react-instantsearch-dom'; export { ExperimentalConfigureRelatedItems } from 'react-instantsearch-dom'; +export { ExperimentalDynamicWidgets } from 'react-instantsearch-dom'; export { CurrentRefinements } from 'react-instantsearch-dom'; export { HierarchicalMenu } from 'react-instantsearch-dom'; export { Highlight } from 'react-instantsearch-dom'; diff --git a/packages/react-instantsearch/native.js b/packages/react-instantsearch/native.js index 578c001494..3308a4814b 100644 --- a/packages/react-instantsearch/native.js +++ b/packages/react-instantsearch/native.js @@ -2,3 +2,4 @@ export { InstantSearch } from 'react-instantsearch-native'; export { Index } from 'react-instantsearch-native'; export { Configure } from 'react-instantsearch-native'; export { ExperimentalConfigureRelatedItems } from 'react-instantsearch-native'; +export { ExperimentalDynamicWidgets } from 'react-instantsearch-native'; diff --git a/stories/DynamicWidgets.stories.js b/stories/DynamicWidgets.stories.js new file mode 100644 index 0000000000..12639814c2 --- /dev/null +++ b/stories/DynamicWidgets.stories.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { + ExperimentalDynamicWidgets, + HierarchicalMenu, + Menu, + Panel, + RefinementList, +} from 'react-instantsearch-dom'; +import { WrapWithHits } from './util'; + +const stories = storiesOf('DynamicWidgets', module); + +stories.add('default', () => ( + + { + if (results._state.query === 'dog') { + return ['categories']; + } + if (results._state.query === 'lego') { + return ['categories', 'brand']; + } + return ['brand', 'hierarchicalCategories.lvl0', 'categories']; + }} + > + + + + + + + +)); diff --git a/yarn.lock b/yarn.lock index 8d8d612aa5..d7c479a467 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,109 +2,109 @@ # yarn lockfile v1 -"@algolia/cache-browser-local-storage@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.5.tgz#3eb10758c794d3cc8fc4e9f09e339d5b9589dc9c" - integrity sha512-9rs/Yi82ilgifweJamOy4DlJ4xPGsCN/zg+RKy4vjytNhOrkEHLRQC8vPZ3OhD8KVlw9lRQIZTlgjgFl8iMeeA== - dependencies: - "@algolia/cache-common" "4.8.5" - -"@algolia/cache-common@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.8.5.tgz#86f8a6878bd2fc5c8d889e48d18d3033faa0bcd8" - integrity sha512-4SvRWnagKtwBFAy8Rsfmv0/Uk53fZL+6dy2idwdx6SjMGKSs0y1Qv+thb4h/k/H5MONisAoT9C2rgZ/mqwh5yw== - -"@algolia/cache-in-memory@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.8.5.tgz#13055d54775f99aa4e1ce051e73079d0f207a3e6" - integrity sha512-XBBfqs28FbjwLboY3sxvuzBgYsuXdFsj2mUvkgxfb0GVEzwW4I0NM7KzSPwT+iht55WS1PgIOnynjmhPsrubCw== - dependencies: - "@algolia/cache-common" "4.8.5" - -"@algolia/client-account@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.8.5.tgz#92df1dd0a7bea06e329873c7098c72cc4dd8e9d6" - integrity sha512-DjXMpeCdY4J4IDBfowiG6Xl9ec/FhG1NpPQM0Uv4xXsc/TeeZ1JgbgNDhWe9jW0jBEALy+a/RmPrZ0vsxcadsg== - dependencies: - "@algolia/client-common" "4.8.5" - "@algolia/client-search" "4.8.5" - "@algolia/transporter" "4.8.5" - -"@algolia/client-analytics@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.8.5.tgz#1aa731a146b347022a0a9e0eb009f2b2f8d9825f" - integrity sha512-PQEY+chbHmZnRJdaWsvUYzDpEPr60az0EPUexdouvXGZId15/SnDaXjnf89F7tYmCzkHdUtG4bSvPzAupQ4AFA== - dependencies: - "@algolia/client-common" "4.8.5" - "@algolia/client-search" "4.8.5" - "@algolia/requester-common" "4.8.5" - "@algolia/transporter" "4.8.5" - -"@algolia/client-common@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.8.5.tgz#77e5d9bbfcb421fa8812cdd91943961c64793148" - integrity sha512-Dn8vog2VrGsJeOcBMcSAEIjBtPyogzUBGlh1DtVd0m8GN6q+cImCESl6DY846M2PTYWsLBKBksq37eUfSe9FxQ== - dependencies: - "@algolia/requester-common" "4.8.5" - "@algolia/transporter" "4.8.5" - -"@algolia/client-recommendation@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/client-recommendation/-/client-recommendation-4.8.5.tgz#f02f8f8ff3983597cae677ec0bc3eb01ae26121a" - integrity sha512-ffawCC1C25rCa8/JU2niRZgwr8aV9b2qsLVMo73GXFzi2lceXPAe9K68mt/BGHU+w7PFUwVHsV2VmB+G/HQRVw== - dependencies: - "@algolia/client-common" "4.8.5" - "@algolia/requester-common" "4.8.5" - "@algolia/transporter" "4.8.5" - -"@algolia/client-search@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.8.5.tgz#970a5c91847822dbd82565f97bd2a0c37a5d56e6" - integrity sha512-Ru2MljGZWrSQ0CVsDla11oGEPL/RinmVkLJfBtQ+/pk1868VfpAQFGKtOS/b8/xLrMA0Vm4EfC3Mgclk/p3KJA== - dependencies: - "@algolia/client-common" "4.8.5" - "@algolia/requester-common" "4.8.5" - "@algolia/transporter" "4.8.5" - -"@algolia/logger-common@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.8.5.tgz#ef275c532c21424f4b29b26ec2e27de2c973ad95" - integrity sha512-PS6NS6bpED0rAxgCPGhjZJg9why0PnoVEE7ZoCbPq6lsAOc6FPlQLri4OiLyU7wx8RWDoVtOadyzulqAAsfPSQ== - -"@algolia/logger-console@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.8.5.tgz#8fe547fdcf76574963503f7c4ff2673e792ae886" - integrity sha512-3+4gLSbwzuGmrb5go3IZNcFIYVMSbB4c8UMtWEJ/gDBtgGZIvT6f/KlvVSOHIhthSxaM3Y13V6Qile/SpGqc6A== - dependencies: - "@algolia/logger-common" "4.8.5" - -"@algolia/requester-browser-xhr@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.5.tgz#95e01e2dca38358055f08440f46d4f0b9f735280" - integrity sha512-M/Gf2vv/fU4+CqDW+wok7HPpEcLym3NtDzU9zaPzGYI/9X7o36581oyfnzt2pNfsXSQVj5a2pZVUWC3Z4SO27w== - dependencies: - "@algolia/requester-common" "4.8.5" - -"@algolia/requester-common@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.8.5.tgz#952dec3b36c14495af158914cd6c0e2c9ce72b5e" - integrity sha512-OIhsdwIrJVAlVlP7cwlt+RoR5AmxAoTGrFokOY9imVmgqXUUljdKO/DjhRL8vwYGFEidZ9efIjAIQ2B3XOhT9A== - -"@algolia/requester-node-http@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.8.5.tgz#a1e5a6d23a9a4e78abd5a2416f1a6c232b0a7e14" - integrity sha512-viHAjfo53A3VSE7Bb/nzgpSMZ3prPp2qti7Wg8w7qxhntppKp3Fln6t4Vp+BoPOqruLsj139xXhheAKeRcYa0w== - dependencies: - "@algolia/requester-common" "4.8.5" - -"@algolia/transporter@4.8.5": - version "4.8.5" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.8.5.tgz#86f4e286cb4eba6e62f1c0393c33cc17ff262fa9" - integrity sha512-Rb3cMlh/GoJK0+g+49GNA3IvR/EXsDEBwpyM+FOotSwxgiGt1wGBHM0K2v0GHwIEcuww02pl6KMDVlilA+qh0g== - dependencies: - "@algolia/cache-common" "4.8.5" - "@algolia/logger-common" "4.8.5" - "@algolia/requester-common" "4.8.5" +"@algolia/cache-browser-local-storage@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.9.3.tgz#44e6306a548a76c410b5f64a8a0a1b65f63183c9" + integrity sha512-t9yKMfPNxxEUk/PPbZtXj0GCttDk1pk0wV2eA5udIOgf+Wqb/77yH75zz1u8EmCBGPe+FWXjSVT/wS1tlQz7SA== + dependencies: + "@algolia/cache-common" "4.9.3" + +"@algolia/cache-common@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.9.3.tgz#0b3ca07c9af108433b4d3423a03511c3d053fed5" + integrity sha512-4dvzz28ESs7lRHmpBIjlmRloD9oGeD90E2C0QWNQYuAYosSdXGwW7vw4vdGRdPoL32t6u6S+47Bk6Dhcbw2ftA== + +"@algolia/cache-in-memory@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.9.3.tgz#0bae2ad1de6537ca28efaf5280051265903bfca5" + integrity sha512-e1eRpP/Ht9qmLw5Sp674N6Y0c59K0L2LBI71EBOlq1j+kVc+JxVO03he5g+nQ7JOwLijyJPrkbm3RvXb5CX0sA== + dependencies: + "@algolia/cache-common" "4.9.3" + +"@algolia/client-account@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.9.3.tgz#af9bf3612d05e87aa36372da50a2f0a9265de201" + integrity sha512-mSF0jiAo/tWKf/Z7mqhz6ETltrl+L+Zt2xuM3W5y1UOZvj47fn2ZcMRce8MQ+dd54t9iA8qIa+0XGlCSQf9lxA== + dependencies: + "@algolia/client-common" "4.9.3" + "@algolia/client-search" "4.9.3" + "@algolia/transporter" "4.9.3" + +"@algolia/client-analytics@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.9.3.tgz#cddb4a97d796291d91bd15735de008b2d80a0b1b" + integrity sha512-Z3EjegxsdTMRmOLoDBnCZJjdL3ZM4J/G7TMe2PIArdCJFWM4iDnO7/MvYasqpK0PPOCHRh0wS4yKG9rZOz6Vsw== + dependencies: + "@algolia/client-common" "4.9.3" + "@algolia/client-search" "4.9.3" + "@algolia/requester-common" "4.9.3" + "@algolia/transporter" "4.9.3" + +"@algolia/client-common@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.9.3.tgz#167a6863b55ffe7788ab9ac98b1b6fd0078f79df" + integrity sha512-6GAfuNqMrBN3094H0DzvQyxJoKUkyQpEr5OiFhH8I3lihI1rTtjEUrNDTsVp6e9VsR2OCRpnL9EEDv2HcGe8cw== + dependencies: + "@algolia/requester-common" "4.9.3" + "@algolia/transporter" "4.9.3" + +"@algolia/client-recommendation@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/client-recommendation/-/client-recommendation-4.9.3.tgz#e2031237b2904c9d9b946fc846c87a21ad67bc5a" + integrity sha512-r+MNluwnUTr1tgHWQ5BPRw0A0YJZp9sXjSVxPCY3a+N6BgLaX4E02+FA8VrqVs8uR7mMQSLaJHoeCKnmNPrk9w== + dependencies: + "@algolia/client-common" "4.9.3" + "@algolia/requester-common" "4.9.3" + "@algolia/transporter" "4.9.3" + +"@algolia/client-search@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.9.3.tgz#6259645ad5a7e7388727343806bcf2d0cf3e343a" + integrity sha512-8C6woYf6bY4Fh9H9nKY5IDDeBPwQ3nZn9QMQdgUj9ffDU8UzPqSivtLER1A+I81p1j9h+aBADRifwzIYtSXOkA== + dependencies: + "@algolia/client-common" "4.9.3" + "@algolia/requester-common" "4.9.3" + "@algolia/transporter" "4.9.3" + +"@algolia/logger-common@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.9.3.tgz#d9b976524313b11f11c6ec546e2ed451c7964a11" + integrity sha512-8hGQ5HQvjx2kr7GWOmpON1tcRX2+VHqVg4p+qJqCBsPFlXbAshUyRJkxuen20eem2EAA5Cmmo1fPy/jlqdMMHA== + +"@algolia/logger-console@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.9.3.tgz#f690bf8d5262a82425da26b42a519d7ec6c3784a" + integrity sha512-7FGulrAjS/oCVRShKJw5qFuyHOZk/44jolEtNtXvO/tZRR8hPPiow16Vrd3ByRSIhghkC5zj6at4nQhoPK+KqA== + dependencies: + "@algolia/logger-common" "4.9.3" + +"@algolia/requester-browser-xhr@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.9.3.tgz#1c0fd594e253b41786b3408ade9f63862fe0c577" + integrity sha512-hP4YgxcY1kol0d+joXpO4BJuXjgF+vy3eBPk8WCXvZucv8hl5Vqb4BLccDMck+sTqP4Tqglwh/KwVTQrpmi/wA== + dependencies: + "@algolia/requester-common" "4.9.3" + +"@algolia/requester-common@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.9.3.tgz#58fb72ca3f4f7714d75287ff568000ee6c2afac6" + integrity sha512-AgUw1iA/JkanZC+dhkSLyeiVgBhaaM3bI20f3cokuuDdz4X6F+hzi0vEpUZrEuNfnMLbUg8gxq3Vcg1/L9+9MA== + +"@algolia/requester-node-http@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.9.3.tgz#a2bf0e7048fe3b81c6b6b170f8f68a5789dadafe" + integrity sha512-+nz7rRnI9qNcdZjHpyAyvcDLAO9mGobqsAi0aicxMka/szU1HVUX6+pvSOiiOsD8ST3R13rJuufgHfWdDUysQg== + dependencies: + "@algolia/requester-common" "4.9.3" + +"@algolia/transporter@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.9.3.tgz#5a0933d4e59acdf88712156b2ab6f1b46c0a7f88" + integrity sha512-oJ68VSSpmUyB9EByqoyx25wgcrO9fgXtjH+pOtKoKmCW+RfxHW5agltJoQ808N8uq/AvP5ugMkRLGL3xf4WdzQ== + dependencies: + "@algolia/cache-common" "4.9.3" + "@algolia/logger-common" "4.9.3" + "@algolia/requester-common" "4.9.3" "@babel/cli@7.4.4": version "7.4.4" @@ -4809,25 +4809,32 @@ algoliasearch-helper@^3.4.3: dependencies: events "^1.1.1" -algoliasearch@4.8.5: - version "4.8.5" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.8.5.tgz#17a97b01c46c1ac5c1cd99d950d65e7064c8b8a9" - integrity sha512-GjKjpeevpePEJYinGokASNtIkl1t5EseNMlqDNAc+sXE8+iyyeqTyiJsN7bwlRG2BIremuslE/NlwdEfUuBLJw== - dependencies: - "@algolia/cache-browser-local-storage" "4.8.5" - "@algolia/cache-common" "4.8.5" - "@algolia/cache-in-memory" "4.8.5" - "@algolia/client-account" "4.8.5" - "@algolia/client-analytics" "4.8.5" - "@algolia/client-common" "4.8.5" - "@algolia/client-recommendation" "4.8.5" - "@algolia/client-search" "4.8.5" - "@algolia/logger-common" "4.8.5" - "@algolia/logger-console" "4.8.5" - "@algolia/requester-browser-xhr" "4.8.5" - "@algolia/requester-common" "4.8.5" - "@algolia/requester-node-http" "4.8.5" - "@algolia/transporter" "4.8.5" +algoliasearch-helper@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.5.3.tgz#fbf8b328bc103efdefde59a7d25eaffe85b2490f" + integrity sha512-DtSlOKAJ6TGkQD6u58g6/ABdMmHf3pAj6xVL5hJF+D4z9ldDRf/f5v6puNIxGOlJRwGVvFGyz34beYNqhLDUbQ== + dependencies: + events "^1.1.1" + +algoliasearch@4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.9.3.tgz#b22ef0ae0450304cdf5264369a29cefa71ea2b30" + integrity sha512-VLl9pYXhVB397xTW369sy13qw3m1hHzCfj9zSdeDDYVwTxHiiok/QvhPKAMIzjqyUoY07O8j+941UxYZjugsMQ== + dependencies: + "@algolia/cache-browser-local-storage" "4.9.3" + "@algolia/cache-common" "4.9.3" + "@algolia/cache-in-memory" "4.9.3" + "@algolia/client-account" "4.9.3" + "@algolia/client-analytics" "4.9.3" + "@algolia/client-common" "4.9.3" + "@algolia/client-recommendation" "4.9.3" + "@algolia/client-search" "4.9.3" + "@algolia/logger-common" "4.9.3" + "@algolia/logger-console" "4.9.3" + "@algolia/requester-browser-xhr" "4.9.3" + "@algolia/requester-common" "4.9.3" + "@algolia/requester-node-http" "4.9.3" + "@algolia/transporter" "4.9.3" "algoliasearch@>= 3.27.1 < 5": version "3.35.1" @@ -16724,6 +16731,13 @@ react-draggable@^3.1.1: classnames "^2.2.5" prop-types "^15.6.0" +react-error-boundary@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.3.tgz#276bfa05de8ac17b863587c9e0647522c25e2a0b" + integrity sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.4.tgz#88dfb88857c18ceb3b9f95076f850d7121776991"