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"