Relation search widget (#186)

* search action/reducer refactor

* Relation widget skeleton

* search clearing

* query action + reducer

* Autocomplete component for RelationControl
This commit is contained in:
Cássio Souza 2016-12-07 15:44:07 -02:00 committed by GitHub
parent 4f6f4bfae9
commit 05337ff232
12 changed files with 446 additions and 94 deletions

View File

@ -121,6 +121,7 @@
"prosemirror-view": "^0.12.0", "prosemirror-view": "^0.12.0",
"react": "^15.1.0", "react": "^15.1.0",
"react-addons-css-transition-group": "^15.3.1", "react-addons-css-transition-group": "^15.3.1",
"react-autosuggest": "^7.0.1",
"react-datetime": "^2.6.0", "react-datetime": "^2.6.0",
"react-dom": "^15.1.0", "react-dom": "^15.1.0",
"react-hot-loader": "^3.0.0-beta.2", "react-hot-loader": "^3.0.0-beta.2",

View File

@ -27,10 +27,6 @@ export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST';
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS'; export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE'; 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) * Simple Action Creators (Internal)
* We still need to export them for tests * We still need to export them for tests
@ -122,35 +118,6 @@ export function emmptyDraftCreated(entry) {
payload: 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 * 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))
);
};
}

137
src/actions/search.js Normal file
View File

@ -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))
);
};
}

View File

@ -21,6 +21,9 @@ import SelectControl from './Widgets/SelectControl';
import SelectPreview from './Widgets/SelectPreview'; import SelectPreview from './Widgets/SelectPreview';
import ObjectControl from './Widgets/ObjectControl'; import ObjectControl from './Widgets/ObjectControl';
import ObjectPreview from './Widgets/ObjectPreview'; import ObjectPreview from './Widgets/ObjectPreview';
import RelationControl from './Widgets/RelationControl';
import RelationPreview from './Widgets/RelationPreview';
registry.registerWidget('string', StringControl, StringPreview); registry.registerWidget('string', StringControl, StringPreview);
registry.registerWidget('text', TextControl, TextPreview); registry.registerWidget('text', TextControl, TextPreview);
@ -32,6 +35,7 @@ registry.registerWidget('date', DateControl, DatePreview);
registry.registerWidget('datetime', DateTimeControl, DateTimePreview); registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
registry.registerWidget('select', SelectControl, SelectPreview); registry.registerWidget('select', SelectControl, SelectPreview);
registry.registerWidget('object', ObjectControl, ObjectPreview); registry.registerWidget('object', ObjectControl, ObjectPreview);
registry.registerWidget('relation', RelationControl, RelationPreview);
registry.registerWidget('unknown', UnknownControl, UnknownPreview); registry.registerWidget('unknown', UnknownControl, UnknownPreview);
export function resolveWidget(name) { // eslint-disable-line export function resolveWidget(name) { // eslint-disable-line

View File

@ -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 <span>{suggestion.data[valueField]}</span>;
};
render() {
const { value, isFetching, queryHits } = this.props;
const inputProps = {
placeholder: '',
value: value || '',
onChange: this.onChange,
};
return (
<div>
<Autosuggest
suggestions={queryHits}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} // eslint-disable-line
onSuggestionsClearRequested={this.onSuggestionsClearRequested} // eslint-disable-line
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
inputProps={inputProps}
/>
<Loader active={isFetching} />
</div>
);
}
}
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);

View File

@ -0,0 +1,10 @@
import React, { PropTypes } from 'react';
import previewStyle from './defaultPreviewStyle';
export default function RelationPreview({ value }) {
return <div style={previewStyle}>{ value }</div>;
}
RelationPreview.propTypes = {
value: PropTypes.node,
};

View File

@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectSearchedEntries } from '../reducers'; import { selectSearchedEntries } from '../reducers';
import { searchEntries } from '../actions/entries'; import { searchEntries, clearSearch } from '../actions/search';
import { Loader } from '../components/UI'; import { Loader } from '../components/UI';
import EntryListing from '../components/EntryListing/EntryListing'; import EntryListing from '../components/EntryListing/EntryListing';
import styles from './breakpoints.css'; import styles from './breakpoints.css';
@ -12,6 +12,7 @@ class SearchPage extends React.Component {
static propTypes = { static propTypes = {
isFetching: PropTypes.bool, isFetching: PropTypes.bool,
searchEntries: PropTypes.func.isRequired, searchEntries: PropTypes.func.isRequired,
clearSearch: PropTypes.func.isRequired,
searchTerm: PropTypes.string.isRequired, searchTerm: PropTypes.string.isRequired,
collections: ImmutablePropTypes.seq, collections: ImmutablePropTypes.seq,
entries: ImmutablePropTypes.list, entries: ImmutablePropTypes.list,
@ -30,9 +31,13 @@ class SearchPage extends React.Component {
searchEntries(nextProps.searchTerm); searchEntries(nextProps.searchTerm);
} }
componentWillUnmount() {
this.props.clearSearch();
}
handleLoadMore = (page) => { handleLoadMore = (page) => {
const { searchTerm, searchEntries } = this.props; const { searchTerm, searchEntries } = this.props;
searchEntries(searchTerm, page); if (!isNaN(page)) searchEntries(searchTerm, page);
}; };
render() { render() {
@ -70,5 +75,8 @@ function mapStateToProps(state, ownProps) {
export default connect( export default connect(
mapStateToProps, mapStateToProps,
{ searchEntries } {
searchEntries,
clearSearch,
}
)(SearchPage); )(SearchPage);

View File

@ -33,6 +33,66 @@ h1 {
} }
:global { :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 { & .rdt {
position: relative; position: relative;
} }

View File

@ -4,14 +4,13 @@ import {
ENTRY_SUCCESS, ENTRY_SUCCESS,
ENTRIES_REQUEST, ENTRIES_REQUEST,
ENTRIES_SUCCESS, ENTRIES_SUCCESS,
SEARCH_ENTRIES_REQUEST,
SEARCH_ENTRIES_SUCCESS,
} from '../actions/entries'; } from '../actions/entries';
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
let collection; let collection;
let loadedEntries; let loadedEntries;
let page; let page;
let searchTerm;
const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
switch (action.type) { 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: case SEARCH_ENTRIES_SUCCESS:
loadedEntries = action.payload.entries; loadedEntries = action.payload.entries;
page = action.payload.page;
searchTerm = action.payload.searchTerm;
return state.withMutations((map) => { return state.withMutations((map) => {
loadedEntries.forEach(entry => ( loadedEntries.forEach(entry => (
map.setIn(['entities', `${ entry.collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) 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: default:
@ -82,9 +64,4 @@ export const selectEntries = (state, collection) => {
return slugs && slugs.map(slug => selectEntry(state, collection, slug)); 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; export default entries;

View File

@ -6,6 +6,7 @@ import entries, * as fromEntries from './entries';
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow'; import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
import entryDraft from './entryDraft'; import entryDraft from './entryDraft';
import collections from './collections'; import collections from './collections';
import search from './search';
import medias, * as fromMedias from './medias'; import medias, * as fromMedias from './medias';
import globalUI from './globalUI'; import globalUI from './globalUI';
@ -13,6 +14,7 @@ const reducers = {
auth, auth,
config, config,
collections, collections,
search,
integrations, integrations,
editor, editor,
entries, entries,
@ -33,8 +35,10 @@ export const selectEntry = (state, collection, slug) =>
export const selectEntries = (state, collection) => export const selectEntries = (state, collection) =>
fromEntries.selectEntries(state.entries, collection); fromEntries.selectEntries(state.entries, collection);
export const selectSearchedEntries = state => export const selectSearchedEntries = (state) => {
fromEntries.selectSearchedEntries(state.entries); const searchItems = state.search.get('entryIds');
return searchItems && searchItems.map(({ collection, slug }) => fromEntries.selectEntry(state.entries, collection, slug));
};
export const selectUnpublishedEntry = (state, status, slug) => export const selectUnpublishedEntry = (state, status, slug) =>
fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, status, slug); fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, status, slug);

67
src/reducers/search.js Normal file
View File

@ -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;

View File

@ -1201,12 +1201,6 @@ braces@^1.8.2:
preserve "^0.2.0" preserve "^0.2.0"
repeat-element "^1.1.2" 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: brorand@^1.0.1:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5"
@ -4676,10 +4670,6 @@ kind-of@^3.0.2:
dependencies: dependencies:
is-buffer "^1.0.2" 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: known-css-properties@^0.0.5:
version "0.0.5" version "0.0.5"
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.0.5.tgz#33de5b8279010a72db917d33119e4c27c078490a" 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" version "15.3.2"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.3.2.tgz#c09a44f583425a4a9c1b38444d7a6c3e6f0f41f6" 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: react-css-themr@~1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/react-css-themr/-/react-css-themr-1.4.1.tgz#26fa63fe0a8f7343b019f088f88475ca89da2d5a" 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" lodash "^4.2.0"
loose-envify "^1.1.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: react-router-redux@^4.0.5:
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.6.tgz#10cf98dce911d7dd912a05bdb07fee4d3c563dee" 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" version "1.2.0"
resolved "https://registry.yarnpkg.com/react-sortable/-/react-sortable-1.2.0.tgz#5acd7e1910df665408957035acb5f2354519d849" 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: react-toolbox@^1.2.1:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/react-toolbox/-/react-toolbox-1.2.2.tgz#ae8f3290da9e053625df97a63df7224943b79679" 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" version "1.0.3"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-1.0.3.tgz#778aa0099eea0595031ab6b39165f6670d8d26bd" 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" version "3.6.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"
dependencies: dependencies:
@ -7557,6 +7578,10 @@ sax@^1.1.4, sax@~1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" 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: selection-position@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/selection-position/-/selection-position-1.0.0.tgz#e43f87151d94957efa170e10e02c901b47f703c7" 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" version "2.2.6"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" 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: shallowequal@0.2.x:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e"