From 05337ff23205ba1ef7f7f3525afd9db7541c5f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Souza?= Date: Wed, 7 Dec 2016 15:44:07 -0200 Subject: [PATCH] Relation search widget (#186) * search action/reducer refactor * Relation widget skeleton * search clearing * query action + reducer * Autocomplete component for RelationControl --- package.json | 1 + src/actions/entries.js | 53 --------- src/actions/search.js | 137 ++++++++++++++++++++++ src/components/Widgets.js | 4 + src/components/Widgets/RelationControl.js | 108 +++++++++++++++++ src/components/Widgets/RelationPreview.js | 10 ++ src/containers/SearchPage.js | 14 ++- src/index.css | 60 ++++++++++ src/reducers/entries.js | 27 +---- src/reducers/index.js | 8 +- src/reducers/search.js | 67 +++++++++++ yarn.lock | 51 ++++++-- 12 files changed, 446 insertions(+), 94 deletions(-) create mode 100644 src/actions/search.js create mode 100644 src/components/Widgets/RelationControl.js create mode 100644 src/components/Widgets/RelationPreview.js create mode 100644 src/reducers/search.js diff --git a/package.json b/package.json index 6d045fb8..17b84aed 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "prosemirror-view": "^0.12.0", "react": "^15.1.0", "react-addons-css-transition-group": "^15.3.1", + "react-autosuggest": "^7.0.1", "react-datetime": "^2.6.0", "react-dom": "^15.1.0", "react-hot-loader": "^3.0.0-beta.2", diff --git a/src/actions/entries.js b/src/actions/entries.js index 61b88e72..c80cf482 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -27,10 +27,6 @@ export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST'; export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS'; export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE'; -export const SEARCH_ENTRIES_REQUEST = 'SEARCH_ENTRIES_REQUEST'; -export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS'; -export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE'; - /* * Simple Action Creators (Internal) * We still need to export them for tests @@ -122,35 +118,6 @@ export function emmptyDraftCreated(entry) { payload: entry, }; } - -export function searchingEntries(searchTerm) { - return { - type: SEARCH_ENTRIES_REQUEST, - payload: { searchTerm }, - }; -} - -export function searchSuccess(searchTerm, entries, page) { - return { - type: SEARCH_ENTRIES_SUCCESS, - payload: { - searchTerm, - entries, - page, - }, - }; -} - -export function searchFailure(searchTerm, error) { - return { - type: SEARCH_ENTRIES_FAILURE, - payload: { - searchTerm, - error, - }, - }; -} - /* * Exported simple Action Creators */ @@ -244,23 +211,3 @@ export function persistEntry(collection, entryDraft) { }); }; } - -export function searchEntries(searchTerm, page = 0) { - return (dispatch, getState) => { - const state = getState(); - let collections = state.collections.keySeq().toArray(); - collections = collections.filter(collection => selectIntegration(state, collection, 'search')); - const integration = selectIntegration(state, collections[0], 'search'); - if (!integration) { - dispatch(searchFailure(searchTerm, 'Search integration is not configured.')); - } - const provider = integration ? - getIntegrationProvider(state.integrations, integration) - : currentBackend(state.config); - dispatch(searchingEntries(searchTerm)); - provider.search(collections, searchTerm, page).then( - response => dispatch(searchSuccess(searchTerm, response.entries, response.pagination)), - error => dispatch(searchFailure(searchTerm, error)) - ); - }; -} diff --git a/src/actions/search.js b/src/actions/search.js new file mode 100644 index 00000000..b87b55b3 --- /dev/null +++ b/src/actions/search.js @@ -0,0 +1,137 @@ +import { currentBackend } from '../backends/backend'; +import { getIntegrationProvider } from '../integrations'; +import { selectIntegration } from '../reducers'; + +/* + * Contant Declarations + */ +export const SEARCH_ENTRIES_REQUEST = 'SEARCH_ENTRIES_REQUEST'; +export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS'; +export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE'; + +export const QUERY_REQUEST = 'QUERY_REQUEST'; +export const QUERY_SUCCESS = 'QUERY_SUCCESS'; +export const QUERY_FAILURE = 'QUERY_FAILURE'; + +export const SEARCH_CLEAR = 'SEARCH_CLEAR'; + +/* + * Simple Action Creators (Internal) + * We still need to export them for tests + */ +export function searchingEntries(searchTerm) { + return { + type: SEARCH_ENTRIES_REQUEST, + payload: { searchTerm }, + }; +} + +export function searchSuccess(searchTerm, entries, page) { + return { + type: SEARCH_ENTRIES_SUCCESS, + payload: { + searchTerm, + entries, + page, + }, + }; +} + +export function searchFailure(searchTerm, error) { + return { + type: SEARCH_ENTRIES_FAILURE, + payload: { + searchTerm, + error, + }, + }; +} + +export function querying(collection, searchFields, searchTerm) { + return { + type: QUERY_REQUEST, + payload: { + collection, + searchFields, + searchTerm, + }, + }; +} + +export function querySuccess(collection, searchFields, searchTerm, response) { + return { + type: QUERY_SUCCESS, + payload: { + collection, + searchFields, + searchTerm, + response, + }, + }; +} + +export function queryFailure(collection, searchFields, searchTerm, error) { + return { + type: QUERY_SUCCESS, + payload: { + collection, + searchFields, + searchTerm, + error, + }, + }; +} + +/* + * Exported simple Action Creators + */ + +export function clearSearch() { + return { type: SEARCH_CLEAR }; +} + + +/* + * Exported Thunk Action Creators + */ + +// SearchEntries will search for complete entries in all collections. +export function searchEntries(searchTerm, page = 0) { + return (dispatch, getState) => { + const state = getState(); + let collections = state.collections.keySeq().toArray(); + collections = collections.filter(collection => selectIntegration(state, collection, 'search')); + const integration = selectIntegration(state, collections[0], 'search'); + if (!integration) { + dispatch(searchFailure(searchTerm, 'Search integration is not configured.')); + } + const provider = integration ? + getIntegrationProvider(state.integrations, integration) + : currentBackend(state.config); + dispatch(searchingEntries(searchTerm)); + provider.search(collections, searchTerm, page).then( + response => dispatch(searchSuccess(searchTerm, response.entries, response.pagination)), + error => dispatch(searchFailure(searchTerm, error)) + ); + }; +} + +// Instead of searching for complete entries, query will search for specific fields +// in specific collections and return raw data (no entries). +export function query(collection, searchFields, searchTerm) { + return (dispatch, getState) => { + const state = getState(); + const integration = selectIntegration(state, collection, 'search'); + if (!integration) { + dispatch(searchFailure(searchTerm, 'Search integration is not configured.')); + } + const provider = integration ? + getIntegrationProvider(state.integrations, integration) + : currentBackend(state.config); + dispatch(querying(collection, searchFields, searchTerm)); + provider.searchBy(searchFields, collection, searchTerm).then( + response => dispatch(querySuccess(collection, searchFields, searchTerm, response)), + error => dispatch(queryFailure(collection, searchFields, searchTerm, error)) + ); + }; +} diff --git a/src/components/Widgets.js b/src/components/Widgets.js index a0a6e118..7d4b89b4 100644 --- a/src/components/Widgets.js +++ b/src/components/Widgets.js @@ -21,6 +21,9 @@ import SelectControl from './Widgets/SelectControl'; import SelectPreview from './Widgets/SelectPreview'; import ObjectControl from './Widgets/ObjectControl'; import ObjectPreview from './Widgets/ObjectPreview'; +import RelationControl from './Widgets/RelationControl'; +import RelationPreview from './Widgets/RelationPreview'; + registry.registerWidget('string', StringControl, StringPreview); registry.registerWidget('text', TextControl, TextPreview); @@ -32,6 +35,7 @@ registry.registerWidget('date', DateControl, DatePreview); registry.registerWidget('datetime', DateTimeControl, DateTimePreview); registry.registerWidget('select', SelectControl, SelectPreview); registry.registerWidget('object', ObjectControl, ObjectPreview); +registry.registerWidget('relation', RelationControl, RelationPreview); registry.registerWidget('unknown', UnknownControl, UnknownPreview); export function resolveWidget(name) { // eslint-disable-line diff --git a/src/components/Widgets/RelationControl.js b/src/components/Widgets/RelationControl.js new file mode 100644 index 00000000..d5a2bd15 --- /dev/null +++ b/src/components/Widgets/RelationControl.js @@ -0,0 +1,108 @@ +import React, { Component, PropTypes } from 'react'; +import Autosuggest from 'react-autosuggest'; +import { connect } from 'react-redux'; +import { debounce } from 'lodash'; +import { Loader } from '../../components/UI/index'; +import { query, clearSearch } from '../../actions/search'; + + +function escapeRegexCharacters(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +class RelationControl extends Component { + static propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.node, + field: PropTypes.node, + isFetching: PropTypes.bool, + query: PropTypes.func.isRequired, + clearSearch: PropTypes.func.isRequired, + queryHits: PropTypes.array, // eslint-disable-line + }; + + onChange = (event, { newValue }) => { + this.props.onChange(newValue); + }; + + onSuggestionsFetchRequested = debounce(({ value }) => { + if (value.length < 3) return; + const { field } = this.props; + const collection = field.get('collection'); + const searchFields = field.get('searchFields').map(f => `data.${ f }`).toJS(); + this.props.query(collection, searchFields, value); + }, 80); + + onSuggestionsClearRequested = () => { + this.props.clearSearch(); + }; + + getMatchingHits = (value) => { + const { field, queryHits } = this.props; + const searchFields = field.get('searchFields').toJS(); + const escapedValue = escapeRegexCharacters(value.trim()); + const regex = new RegExp(`^ ${ escapedValue }`, 'i'); + + if (escapedValue === '') { + return []; + } + + return queryHits.filter((hit) => { + let testResult = false; + searchFields.forEach((f) => { + testResult = testResult || regex.test(hit.data[f]); + }); + return testResult; + }); + }; + + getSuggestionValue = (suggestion) => { + const { field } = this.props; + const valueField = field.get('valueField'); + return suggestion.data[valueField]; + }; + + renderSuggestion = (suggestion) => { + const { field } = this.props; + const valueField = field.get('valueField'); + return {suggestion.data[valueField]}; + }; + + render() { + const { value, isFetching, queryHits } = this.props; + + const inputProps = { + placeholder: '', + value: value || '', + onChange: this.onChange, + }; + + return ( +
+ + +
+ ); + } +} + +function mapStateToProps(state) { + const isFetching = state.search.get('isFetching'); + const queryHits = state.search.get('queryHits'); + return { isFetching, queryHits }; +} + +export default connect( + mapStateToProps, + { + query, + clearSearch, + } +)(RelationControl); diff --git a/src/components/Widgets/RelationPreview.js b/src/components/Widgets/RelationPreview.js new file mode 100644 index 00000000..b758e130 --- /dev/null +++ b/src/components/Widgets/RelationPreview.js @@ -0,0 +1,10 @@ +import React, { PropTypes } from 'react'; +import previewStyle from './defaultPreviewStyle'; + +export default function RelationPreview({ value }) { + return
{ value }
; +} + +RelationPreview.propTypes = { + value: PropTypes.node, +}; diff --git a/src/containers/SearchPage.js b/src/containers/SearchPage.js index b4c6df1f..4203a8e1 100644 --- a/src/containers/SearchPage.js +++ b/src/containers/SearchPage.js @@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { selectSearchedEntries } from '../reducers'; -import { searchEntries } from '../actions/entries'; +import { searchEntries, clearSearch } from '../actions/search'; import { Loader } from '../components/UI'; import EntryListing from '../components/EntryListing/EntryListing'; import styles from './breakpoints.css'; @@ -12,6 +12,7 @@ class SearchPage extends React.Component { static propTypes = { isFetching: PropTypes.bool, searchEntries: PropTypes.func.isRequired, + clearSearch: PropTypes.func.isRequired, searchTerm: PropTypes.string.isRequired, collections: ImmutablePropTypes.seq, entries: ImmutablePropTypes.list, @@ -30,9 +31,13 @@ class SearchPage extends React.Component { searchEntries(nextProps.searchTerm); } + componentWillUnmount() { + this.props.clearSearch(); + } + handleLoadMore = (page) => { const { searchTerm, searchEntries } = this.props; - searchEntries(searchTerm, page); + if (!isNaN(page)) searchEntries(searchTerm, page); }; render() { @@ -70,5 +75,8 @@ function mapStateToProps(state, ownProps) { export default connect( mapStateToProps, - { searchEntries } + { + searchEntries, + clearSearch, + } )(SearchPage); diff --git a/src/index.css b/src/index.css index fc708dfd..6d5c7871 100644 --- a/src/index.css +++ b/src/index.css @@ -33,6 +33,66 @@ h1 { } :global { + + & .react-autosuggest__container { + position: relative; + } + + & .react-autosuggest__input { + width: 240px; + height: 30px; + padding: 10px 20px; + font-family: Helvetica, sans-serif; + font-weight: 300; + font-size: 16px; + border: 1px solid #aaa; + border-radius: 4px; + } + + & .react-autosuggest__input:focus { + outline: none; + } + + & .react-autosuggest__container--open .react-autosuggest__input { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + & .react-autosuggest__suggestions-container { + display: none; + } + + & .react-autosuggest__container--open .react-autosuggest__suggestions-container { + display: block; + position: absolute; + top: 51px; + width: 100%; + border: 1px solid #aaa; + background-color: #fff; + font-family: Helvetica, sans-serif; + font-weight: 300; + font-size: 16px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + z-index: 2; + } + + & .react-autosuggest__suggestions-list { + margin: 0; + padding: 0; + list-style-type: none; + } + + & .react-autosuggest__suggestion { + cursor: pointer; + padding: 10px 20px; + } + + & .react-autosuggest__suggestion--focused { + background-color: #ddd; + } + + & .rdt { position: relative; } diff --git a/src/reducers/entries.js b/src/reducers/entries.js index f29c0d90..13bb851f 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -4,14 +4,13 @@ import { ENTRY_SUCCESS, ENTRIES_REQUEST, ENTRIES_SUCCESS, - SEARCH_ENTRIES_REQUEST, - SEARCH_ENTRIES_SUCCESS, } from '../actions/entries'; +import { SEARCH_ENTRIES_SUCCESS } from '../actions/search'; + let collection; let loadedEntries; let page; -let searchTerm; const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { switch (action.type) { @@ -43,29 +42,12 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { })); }); - case SEARCH_ENTRIES_REQUEST: - if (action.payload.searchTerm !== state.getIn(['search', 'term'])) { - return state.withMutations((map) => { - map.setIn(['search', 'isFetching'], true); - map.setIn(['search', 'term'], action.payload.searchTerm); - }); - } - return state; - case SEARCH_ENTRIES_SUCCESS: loadedEntries = action.payload.entries; - page = action.payload.page; - searchTerm = action.payload.searchTerm; return state.withMutations((map) => { loadedEntries.forEach(entry => ( map.setIn(['entities', `${ entry.collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) )); - const ids = List(loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug }))); - map.set('search', Map({ - page, - term: searchTerm, - ids: page === 0 ? ids : map.getIn(['search', 'ids'], List()).concat(ids), - })); }); default: @@ -82,9 +64,4 @@ export const selectEntries = (state, collection) => { return slugs && slugs.map(slug => selectEntry(state, collection, slug)); }; -export const selectSearchedEntries = (state) => { - const searchItems = state.getIn(['search', 'ids']); - return searchItems && searchItems.map(({ collection, slug }) => selectEntry(state, collection, slug)); -}; - export default entries; diff --git a/src/reducers/index.js b/src/reducers/index.js index 99f3448a..32690b6e 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -6,6 +6,7 @@ import entries, * as fromEntries from './entries'; import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow'; import entryDraft from './entryDraft'; import collections from './collections'; +import search from './search'; import medias, * as fromMedias from './medias'; import globalUI from './globalUI'; @@ -13,6 +14,7 @@ const reducers = { auth, config, collections, + search, integrations, editor, entries, @@ -33,8 +35,10 @@ export const selectEntry = (state, collection, slug) => export const selectEntries = (state, collection) => fromEntries.selectEntries(state.entries, collection); -export const selectSearchedEntries = state => - fromEntries.selectSearchedEntries(state.entries); +export const selectSearchedEntries = (state) => { + const searchItems = state.search.get('entryIds'); + return searchItems && searchItems.map(({ collection, slug }) => fromEntries.selectEntry(state.entries, collection, slug)); +}; export const selectUnpublishedEntry = (state, status, slug) => fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, status, slug); diff --git a/src/reducers/search.js b/src/reducers/search.js new file mode 100644 index 00000000..ae37d366 --- /dev/null +++ b/src/reducers/search.js @@ -0,0 +1,67 @@ +import { Map, List } from 'immutable'; + +import { + SEARCH_ENTRIES_REQUEST, + SEARCH_ENTRIES_SUCCESS, + QUERY_REQUEST, + QUERY_SUCCESS, + SEARCH_CLEAR, +} from '../actions/search'; + +let loadedEntries; +let response; +let page; +let searchTerm; + +const defaultState = Map({ isFetching: false, term: null, page: 0, entryIds: [], queryHits: [] }); + +const entries = (state = defaultState, action) => { + switch (action.type) { + case SEARCH_CLEAR: + return defaultState; + + case SEARCH_ENTRIES_REQUEST: + if (action.payload.searchTerm !== state.get('term')) { + return state.withMutations((map) => { + map.set('isFetching', true); + map.set('term', action.payload.searchTerm); + }); + } + return state; + + case SEARCH_ENTRIES_SUCCESS: + loadedEntries = action.payload.entries; + page = action.payload.page; + searchTerm = action.payload.searchTerm; + return state.withMutations((map) => { + const entryIds = List(loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug }))); + map.set('isFetching', false); + map.set('page', page); + map.set('term', searchTerm); + map.set('entryIds', page === 0 ? entryIds : map.get('entryIds', List()).concat(entryIds)); + }); + + case QUERY_REQUEST: + if (action.payload.searchTerm !== state.get('term')) { + return state.withMutations((map) => { + map.set('isFetching', true); + map.set('term', action.payload.searchTerm); + }); + } + return state; + + case QUERY_SUCCESS: + searchTerm = action.payload.searchTerm; + response = action.payload.response; + return state.withMutations((map) => { + map.set('isFetching', false); + map.set('term', searchTerm); + map.set('queryHits', response.hits); + }); + + default: + return state; + } +}; + +export default entries; diff --git a/yarn.lock b/yarn.lock index b1221d5c..9a7e2947 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1201,12 +1201,6 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -bricks.js@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/bricks.js/-/bricks.js-1.7.0.tgz#2863bde2f03cd48d41dcca88bea1a198c839f608" - dependencies: - knot.js "^1.1.1" - brorand@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5" @@ -4676,10 +4670,6 @@ kind-of@^3.0.2: dependencies: is-buffer "^1.0.2" -knot.js@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/knot.js/-/knot.js-1.1.1.tgz#35dc900d3c62813f0ca119c3d6a0a598e5cb6896" - known-css-properties@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.0.5.tgz#33de5b8279010a72db917d33119e4c27c078490a" @@ -6934,6 +6924,22 @@ react-addons-test-utils@^15.3.2: version "15.3.2" resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.3.2.tgz#c09a44f583425a4a9c1b38444d7a6c3e6f0f41f6" +react-autosuggest: + version "7.0.1" + resolved "https://registry.yarnpkg.com/react-autosuggest/-/react-autosuggest-7.0.1.tgz#e751d2c2e516a344f6cdc150672e85f134f5f2f1" + dependencies: + react-autowhatever "^7.0.0" + react-redux "^4.4.5" + redux "^3.6.0" + shallow-equal "^1.0.0" + +react-autowhatever@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/react-autowhatever/-/react-autowhatever-7.0.0.tgz#7ea19f8024183acf1568fc8e4b76c0d0cc250d00" + dependencies: + react-themeable "^1.1.0" + section-iterator "^2.0.0" + react-css-themr@~1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/react-css-themr/-/react-css-themr-1.4.1.tgz#26fa63fe0a8f7343b019f088f88475ca89da2d5a" @@ -7054,6 +7060,15 @@ react-redux@^4.4.0: lodash "^4.2.0" loose-envify "^1.1.0" +react-redux@^4.4.5: + version "4.4.6" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-4.4.6.tgz#4b9d32985307a11096a2dd61561980044fcc6209" + dependencies: + hoist-non-react-statics "^1.0.3" + invariant "^2.0.0" + lodash "^4.2.0" + loose-envify "^1.1.0" + react-router-redux@^4.0.5: version "4.0.6" resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.6.tgz#10cf98dce911d7dd912a05bdb07fee4d3c563dee" @@ -7090,6 +7105,12 @@ react-sortable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/react-sortable/-/react-sortable-1.2.0.tgz#5acd7e1910df665408957035acb5f2354519d849" +react-themeable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e" + dependencies: + object-assign "^3.0.0" + react-toolbox@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/react-toolbox/-/react-toolbox-1.2.2.tgz#ae8f3290da9e053625df97a63df7224943b79679" @@ -7279,7 +7300,7 @@ redux-thunk@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-1.0.3.tgz#778aa0099eea0595031ab6b39165f6670d8d26bd" -redux@^3.2.0, redux@^3.3.1, redux@^3.5.2: +redux@^3.2.0, redux@^3.3.1, redux@^3.5.2, redux@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" dependencies: @@ -7557,6 +7578,10 @@ sax@^1.1.4, sax@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" +section-iterator@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" + selection-position@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/selection-position/-/selection-position-1.0.0.tgz#e43f87151d94957efa170e10e02c901b47f703c7" @@ -7640,6 +7665,10 @@ sha.js@2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" +shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.0.0.tgz#508d1838b3de590ab8757b011b25e430900945f7" + shallowequal@0.2.x: version "0.2.2" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e"