diff --git a/app/components/Actions/Actions.js b/app/components/Actions/Actions.js
index 7296f1d55..0aebc3694 100644
--- a/app/components/Actions/Actions.js
+++ b/app/components/Actions/Actions.js
@@ -29,6 +29,10 @@ const Actions = styled(Flex)`
border-radius: 3px;
background: rgba(255, 255, 255, 0.9);
-webkit-backdrop-filter: blur(20px);
+
+ @media print {
+ display: none;
+ }
`;
export default Actions;
diff --git a/app/components/DocumentPreview/DocumentPreview.js b/app/components/DocumentPreview/DocumentPreview.js
index 244162aa1..ae7e089c7 100644
--- a/app/components/DocumentPreview/DocumentPreview.js
+++ b/app/components/DocumentPreview/DocumentPreview.js
@@ -98,13 +98,13 @@ class DocumentPreview extends Component {
{document.starred ? (
-
+
-
+
) : (
-
+
-
+
)}
!readOnly && 'cursor: text;'};
+
+ @media print {
+ display: none;
+ }
`;
const StyledEditor = styled(Editor)`
diff --git a/app/components/Editor/components/Contents.js b/app/components/Editor/components/Contents.js
index b9afe07bd..b7bcfb9cd 100644
--- a/app/components/Editor/components/Contents.js
+++ b/app/components/Editor/components/Contents.js
@@ -93,6 +93,10 @@ const Wrapper = styled.div`
right: 0;
top: 150px;
z-index: 100;
+
+ @media print {
+ display: none;
+ }
`;
const Anchor = styled.a`
diff --git a/app/components/Editor/components/Toolbar/BlockToolbar.js b/app/components/Editor/components/Toolbar/BlockToolbar.js
index faecb09d8..8e5bd48d9 100644
--- a/app/components/Editor/components/Toolbar/BlockToolbar.js
+++ b/app/components/Editor/components/Toolbar/BlockToolbar.js
@@ -197,6 +197,10 @@ const Bar = styled(Flex)`
left: auto;
right: -100%;
}
+
+ @media print {
+ display: none;
+ }
`;
const HiddenInput = styled.input`
diff --git a/app/components/Editor/components/Toolbar/Toolbar.js b/app/components/Editor/components/Toolbar/Toolbar.js
index 1032abfac..9e5e16b2f 100644
--- a/app/components/Editor/components/Toolbar/Toolbar.js
+++ b/app/components/Editor/components/Toolbar/Toolbar.js
@@ -153,4 +153,8 @@ const Menu = styled.div`
transform: translateY(-6px) scale(1);
opacity: 1;
`};
+
+ @media print {
+ display: none;
+ }
`;
diff --git a/app/components/ErrorBoundary/ErrorBoundary.js b/app/components/ErrorBoundary/ErrorBoundary.js
index 7577017a9..f34df8fac 100644
--- a/app/components/ErrorBoundary/ErrorBoundary.js
+++ b/app/components/ErrorBoundary/ErrorBoundary.js
@@ -5,8 +5,13 @@ import { observable } from 'mobx';
import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle';
+type Props = {
+ children?: ?React.Element,
+};
+
@observer
class ErrorBoundary extends Component {
+ props: Props;
@observable error: boolean = false;
componentDidCatch(error: Error, info: Object) {
@@ -27,9 +32,10 @@ class ErrorBoundary extends Component {
return (
- Something went wrong
+ 🛸 Something unexpected happened
- An unrecoverable error occurred. Please try{' '}
+ An unrecoverable error occurred{window.Bugsnag ||
+ (true && ' and our engineers have been notified')}. Please try{' '}
reloading.
diff --git a/app/components/Layout/Layout.js b/app/components/Layout/Layout.js
index 29878ed11..b5d755d00 100644
--- a/app/components/Layout/Layout.js
+++ b/app/components/Layout/Layout.js
@@ -124,6 +124,10 @@ const Container = styled(Flex)`
const Content = styled(Flex)`
margin-left: ${props => (props.editMode ? 0 : layout.sidebarWidth)};
transition: margin-left 200ms ease-in-out;
+
+ @media print {
+ margin-left: 0;
+ }
`;
export default withRouter(inject('user', 'auth', 'ui', 'documents')(Layout));
diff --git a/app/components/Sidebar/Sidebar.js b/app/components/Sidebar/Sidebar.js
index 92fbf89a0..d337d4220 100644
--- a/app/components/Sidebar/Sidebar.js
+++ b/app/components/Sidebar/Sidebar.js
@@ -29,7 +29,6 @@ type Props = {
@observer
class Sidebar extends Component {
props: Props;
- scrollable: ?HTMLDivElement;
handleCreateCollection = () => {
this.props.ui.setActiveModal('collection-new');
@@ -39,20 +38,6 @@ class Sidebar extends Component {
this.props.ui.setActiveModal('collection-edit');
};
- setScrollableRef = ref => {
- this.scrollable = ref;
- };
-
- scrollToActiveDocument = ref => {
- const scrollable = this.scrollable;
- if (!ref || !scrollable) return;
-
- const container = scrollable.getBoundingClientRect();
- const bounds = ref.getBoundingClientRect();
- const scrollTop = bounds.top + container.top;
- scrollable.scrollTop = scrollTop;
- };
-
render() {
const { auth, ui } = this.props;
const { user, team } = auth;
@@ -71,7 +56,7 @@ class Sidebar extends Component {
/>
-
+
}>
Home
@@ -88,7 +73,6 @@ class Sidebar extends Component {
history={this.props.history}
location={this.props.location}
onCreateCollection={this.handleCreateCollection}
- activeDocumentRef={this.scrollToActiveDocument}
/>
@@ -106,6 +90,11 @@ const Container = styled(Flex)`
width: ${layout.sidebarWidth};
background: ${color.smoke};
transition: left 200ms ease-in-out;
+
+ @media print {
+ display: none;
+ left: 0;
+ }
`;
const Section = styled(Flex)`
diff --git a/app/components/Sidebar/components/Collections.js b/app/components/Sidebar/components/Collections.js
index 3fa4066fb..0ad66df5a 100644
--- a/app/components/Sidebar/components/Collections.js
+++ b/app/components/Sidebar/components/Collections.js
@@ -27,7 +27,6 @@ type Props = {
collections: CollectionsStore,
documents: DocumentsStore,
onCreateCollection: () => void,
- activeDocumentRef: HTMLElement => void,
ui: UiStore,
};
@@ -36,14 +35,7 @@ class Collections extends Component {
props: Props;
render() {
- const {
- history,
- location,
- collections,
- ui,
- activeDocumentRef,
- documents,
- } = this.props;
+ const { history, location, collections, ui, documents } = this.props;
return (
@@ -55,7 +47,6 @@ class Collections extends Component {
location={location}
collection={collection}
activeDocument={documents.active}
- activeDocumentRef={activeDocumentRef}
prefetchDocument={documents.prefetchDocument}
ui={ui}
/>
@@ -79,7 +70,6 @@ type CollectionLinkProps = {
collection: Collection,
ui: UiStore,
activeDocument: ?Document,
- activeDocumentRef: HTMLElement => void,
prefetchDocument: (id: string) => Promise,
};
@@ -94,15 +84,32 @@ class CollectionLink extends Component {
this.dropzoneRef.open();
};
- render() {
+ renderDocuments() {
const {
history,
collection,
activeDocument,
- ui,
- activeDocumentRef,
prefetchDocument,
} = this.props;
+
+ return (
+
+ {collection.documents.map(document => (
+
+ ))}
+
+ );
+ }
+
+ render() {
+ const { history, collection, ui } = this.props;
const expanded = collection.id === ui.activeCollectionId;
return (
@@ -119,6 +126,9 @@ class CollectionLink extends Component {
to={collection.url}
icon={}
iconColor={collection.color}
+ expandedContent={this.renderDocuments()}
+ hideExpandToggle
+ expand={expanded}
>
{collection.name}
@@ -134,22 +144,6 @@ class CollectionLink extends Component {
/>
-
- {expanded && (
-
- {collection.documents.map(document => (
-
- ))}
-
- )}
);
@@ -206,7 +200,7 @@ const DocumentLink = observer(
expand={showChildren}
expandedContent={
document.children.length ? (
-
+
{document.children.map(childDocument => (
))}
-
+
) : (
undefined
)
@@ -235,7 +229,7 @@ const CollectionName = styled(Flex)`
padding: 0 0 4px;
`;
-const CollectionAction = styled.a`
+const CollectionAction = styled.span`
position: absolute;
right: 0;
color: ${color.slate};
@@ -262,7 +256,13 @@ const StyledDropToImport = styled(DropToImport)`
}
`;
-const Children = styled(Flex)`
+const CollectionChildren = styled(Flex)`
+ margin-top: -4px;
+ margin-left: 36px;
+`;
+
+const DocumentChildren = styled(Flex)`
+ margin-top: -4px;
margin-left: 12px;
`;
diff --git a/app/components/Sidebar/components/SidebarLink.js b/app/components/Sidebar/components/SidebarLink.js
index fe43ff2d2..d52b8dd5a 100644
--- a/app/components/Sidebar/components/SidebarLink.js
+++ b/app/components/Sidebar/components/SidebarLink.js
@@ -33,7 +33,7 @@ const StyledNavLink = styled(NavLink)`
overflow: hidden;
text-overflow: ellipsis;
padding: 4px 0;
- margin-left: ${({ hasChildren }) => (hasChildren ? '-20px;' : '0')};
+ margin-left: ${({ iconVisible }) => (iconVisible ? '-20px;' : '0')};
color: ${color.slateDark};
font-size: 15px;
cursor: pointer;
@@ -52,6 +52,7 @@ type Props = {
icon?: React$Element<*>,
expand?: boolean,
expandedContent?: React$Element<*>,
+ hideExpandToggle?: boolean,
iconColor?: string,
};
@@ -81,25 +82,35 @@ class SidebarLink extends Component {
};
render() {
- const { icon, children, onClick, to, expandedContent } = this.props;
+ const {
+ icon,
+ children,
+ onClick,
+ to,
+ expandedContent,
+ expand,
+ hideExpandToggle,
+ } = this.props;
const Component = to ? StyledNavLink : StyledDiv;
+ const showExpandIcon = expandedContent && !hideExpandToggle;
return (
{icon && {icon}}
- {expandedContent && (
+ {showExpandIcon && (
)}
{children}
- {this.expanded && expandedContent}
+ {/* Collection */ expand && hideExpandToggle && expandedContent}
+ {/* Document */ this.expanded && !hideExpandToggle && expandedContent}
);
}
diff --git a/app/menus/DocumentMenu.js b/app/menus/DocumentMenu.js
index e4dedde06..e6103d6e7 100644
--- a/app/menus/DocumentMenu.js
+++ b/app/menus/DocumentMenu.js
@@ -67,6 +67,7 @@ class DocumentMenu extends Component {
Download
+ Print
Move…
{allowDelete && (
diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js
index 9375ac37a..74bc1b4a5 100644
--- a/app/scenes/Collection/Collection.js
+++ b/app/scenes/Collection/Collection.js
@@ -49,6 +49,10 @@ class CollectionScene extends Component {
}
}
+ componentWillUnmount() {
+ this.props.ui.clearActiveCollection();
+ }
+
loadContent = async (id: string) => {
const { collections } = this.props;
diff --git a/app/scenes/Dashboard/Dashboard.js b/app/scenes/Dashboard/Dashboard.js
index 1eb9d07f5..5a4109690 100644
--- a/app/scenes/Dashboard/Dashboard.js
+++ b/app/scenes/Dashboard/Dashboard.js
@@ -45,12 +45,18 @@ class Dashboard extends Component {
{showContent ? (
{hasRecentlyViewed && [
- Recently viewed,
- ,
+ Recently viewed,
+ ,
]}
{hasRecentlyEdited && [
- Recently edited,
- ,
+ Recently edited,
+ ,
]}
) : (
diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js
index 0f2d3a21f..5cb9ea2e0 100644
--- a/app/scenes/Document/Document.js
+++ b/app/scenes/Document/Document.js
@@ -33,6 +33,7 @@ import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle';
import NewDocumentIcon from 'components/Icon/NewDocumentIcon';
import Actions, { Action, Separator } from 'components/Actions';
+import ErrorBoundary from 'components/ErrorBoundary';
import Search from 'scenes/Search';
const DISCARD_CHANGES = `
@@ -62,7 +63,7 @@ class DocumentScene extends Component {
@observable notFound = false;
@observable moveModalOpen: boolean = false;
- componentWillMount() {
+ componentDidMount() {
this.loadDocument(this.props);
}
@@ -217,75 +218,77 @@ class DocumentScene extends Component {
return (
- {isMoving && document && }
- {titleText && }
- {(this.isLoading || this.isSaving) && }
- {isFetching && (
-
-
-
- )}
- {!isFetching &&
- document && (
-
-
-
-
- {!isNew &&
- !this.isEditing && }
-
- {this.isEditing ? (
-
- ) : (
- Edit
- )}
-
- {this.isEditing && (
-
- Discard
-
- )}
- {!this.isEditing && (
-
-
-
- )}
- {!this.isEditing && }
-
- {!this.isEditing && (
-
-
-
- )}
-
-
-
+
+ {isMoving && document && }
+ {titleText && }
+ {(this.isLoading || this.isSaving) && }
+ {isFetching && (
+
+
+
)}
+ {!isFetching &&
+ document && (
+
+
+
+
+ {!isNew &&
+ !this.isEditing && }
+
+ {this.isEditing ? (
+
+ ) : (
+ Edit
+ )}
+
+ {this.isEditing && (
+
+ Discard
+
+ )}
+ {!this.isEditing && (
+
+
+
+ )}
+ {!this.isEditing && }
+
+ {!this.isEditing && (
+
+
+
+ )}
+
+
+
+ )}
+
);
}
diff --git a/app/scenes/Search/Search.js b/app/scenes/Search/Search.js
index 9d63fde7b..8766e3789 100644
--- a/app/scenes/Search/Search.js
+++ b/app/scenes/Search/Search.js
@@ -2,10 +2,13 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import keydown from 'react-keydown';
+import Waypoint from 'react-waypoint';
import { observable, action } from 'mobx';
import { observer, inject } from 'mobx-react';
import _ from 'lodash';
-import DocumentsStore from 'stores/DocumentsStore';
+import DocumentsStore, {
+ DEFAULT_PAGINATION_LIMIT,
+} from 'stores/DocumentsStore';
import { withRouter } from 'react-router-dom';
import { searchUrl } from 'utils/routeHelpers';
@@ -44,6 +47,7 @@ const ResultsWrapper = styled(Flex)`
`;
const ResultList = styled(Flex)`
+ margin-bottom: 150px;
opacity: ${props => (props.visible ? '1' : '0')};
transition: all 400ms cubic-bezier(0.65, 0.05, 0.36, 1);
`;
@@ -61,6 +65,8 @@ class Search extends Component {
@observable resultIds: string[] = []; // Document IDs
@observable query: string = '';
+ @observable offset: number = 0;
+ @observable allowLoadMore: boolean = true;
@observable isFetching = false;
componentDidMount() {
@@ -98,10 +104,27 @@ class Search extends Component {
handleQueryChange = () => {
const query = this.props.match.params.query;
this.query = query ? decodeURIComponent(query) : '';
+ this.allowLoadMore = true;
+
+ // To prevent "no results" showing before debounce kicks in
+ if (this.query) this.isFetching = true;
+
this.fetchResultsDebounced();
};
- fetchResultsDebounced = _.debounce(this.fetchResults, 250);
+ fetchResultsDebounced = _.debounce(this.fetchResults, 350, {
+ leading: false,
+ trailing: true,
+ });
+
+ @action
+ loadMoreResults = async () => {
+ // Don't paginate if there aren't more results or we're in the middle of fetching
+ if (!this.allowLoadMore || this.isFetching) return;
+
+ // Fetch more results
+ await this.fetchResults();
+ };
@action
fetchResults = async () => {
@@ -109,7 +132,19 @@ class Search extends Component {
if (this.query) {
try {
- this.resultIds = await this.props.documents.search(this.query);
+ const newResults = await this.props.documents.search(this.query, {
+ offset: this.offset,
+ limit: DEFAULT_PAGINATION_LIMIT,
+ });
+ this.resultIds = this.resultIds.concat(newResults);
+ if (
+ newResults.length === 0 ||
+ newResults.length < DEFAULT_PAGINATION_LIMIT
+ ) {
+ this.allowLoadMore = false;
+ } else {
+ this.offset += DEFAULT_PAGINATION_LIMIT;
+ }
} catch (e) {
console.error('Something went wrong');
}
@@ -157,7 +192,7 @@ class Search extends Component {
value={this.query}
/>
{showEmpty && No matching documents.}
-
+
+ {this.allowLoadMore && (
+
+ )}
diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js
index 8bdc77eac..6e280d5ff 100644
--- a/app/stores/DocumentsStore.js
+++ b/app/stores/DocumentsStore.js
@@ -17,8 +17,10 @@ import Document from 'models/Document';
import ErrorsStore from 'stores/ErrorsStore';
import CacheStore from 'stores/CacheStore';
import UiStore from 'stores/UiStore';
+import type { PaginationParams } from 'types';
const DOCUMENTS_CACHE_KEY = 'DOCUMENTS_CACHE_KEY';
+export const DEFAULT_PAGINATION_LIMIT = 25;
type Options = {
cache: CacheStore,
@@ -77,7 +79,10 @@ class DocumentsStore extends BaseStore {
/* Actions */
@action
- fetchAll = async (request: string = 'list', options: ?Object): Promise<*> => {
+ fetchAll = async (
+ request: string = 'list',
+ options: ?PaginationParams
+ ): Promise<*> => {
this.isFetching = true;
try {
@@ -99,12 +104,12 @@ class DocumentsStore extends BaseStore {
};
@action
- fetchRecentlyModified = async (options: ?Object): Promise<*> => {
+ fetchRecentlyModified = async (options: ?PaginationParams): Promise<*> => {
return await this.fetchAll('list', options);
};
@action
- fetchRecentlyViewed = async (options: ?Object): Promise<*> => {
+ fetchRecentlyViewed = async (options: ?PaginationParams): Promise<*> => {
const data = await this.fetchAll('viewed', options);
runInAction('DocumentsStore#fetchRecentlyViewed', () => {
@@ -119,8 +124,14 @@ class DocumentsStore extends BaseStore {
};
@action
- search = async (query: string): Promise<*> => {
- const res = await client.get('/documents.search', { query });
+ search = async (
+ query: string,
+ options?: PaginationParams
+ ): Promise => {
+ const res = await client.get('/documents.search', {
+ ...options,
+ query,
+ });
invariant(res && res.data, 'res or res.data missing');
const { data } = res;
data.forEach(documentData => this.add(new Document(documentData)));
diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js
index 3fc05269d..09e42feb9 100644
--- a/app/stores/UiStore.js
+++ b/app/stores/UiStore.js
@@ -35,6 +35,11 @@ class UiStore {
this.activeCollectionId = collection.id;
};
+ @action
+ clearActiveCollection = (): void => {
+ this.activeCollectionId = undefined;
+ };
+
@action
clearActiveDocument = (): void => {
this.activeDocumentId = undefined;
diff --git a/app/types/index.js b/app/types/index.js
index 63d861398..a03ef21eb 100644
--- a/app/types/index.js
+++ b/app/types/index.js
@@ -39,12 +39,21 @@ export type Document = {
views: number,
};
+// Pagination response in an API call
export type Pagination = {
limit: number,
nextPath: string,
offset: number,
};
+// Pagination request params
+export type PaginationParams = {
+ limit?: number,
+ offset?: number,
+ sort?: string,
+ direction?: 'ASC' | 'DESC',
+};
+
export type ApiKey = {
id: string,
name: ?string,
diff --git a/package.json b/package.json
index 5ed7c4ee3..215259d41 100644
--- a/package.json
+++ b/package.json
@@ -154,6 +154,7 @@
"react-modal": "^3.1.2",
"react-portal": "^4.0.0",
"react-router-dom": "^4.2.0",
+ "react-waypoint": "^7.3.1",
"redis": "^2.6.2",
"redis-lock": "^0.1.0",
"rimraf": "^2.5.4",
diff --git a/server/api/documents.js b/server/api/documents.js
index 2aeae2787..1c29fead3 100644
--- a/server/api/documents.js
+++ b/server/api/documents.js
@@ -142,13 +142,17 @@ router.post('documents.revisions', auth(), pagination(), async ctx => {
};
});
-router.post('documents.search', auth(), async ctx => {
+router.post('documents.search', auth(), pagination(), async ctx => {
const { query } = ctx.body;
+ const { offset, limit } = ctx.state.pagination;
ctx.assertPresent(query, 'query is required');
const user = await ctx.state.user;
- const documents = await Document.searchForUser(user, query);
+ const documents = await Document.searchForUser(user, query, {
+ offset,
+ limit,
+ });
const data = await Promise.all(
documents.map(async document => await presentDocument(ctx, document))
diff --git a/server/models/Document.js b/server/models/Document.js
index 3bbf8c9ed..1f4e2892f 100644
--- a/server/models/Document.js
+++ b/server/models/Document.js
@@ -160,16 +160,20 @@ Document.findById = async id => {
}
};
-Document.searchForUser = async (user, query, options = {}) => {
+Document.searchForUser = async (
+ user,
+ query,
+ options = {}
+): Promise => {
const limit = options.limit || 15;
const offset = options.offset || 0;
const sql = `
- SELECT * FROM documents
+ SELECT *, ts_rank(documents."searchVector", plainto_tsquery('english', :query)) as "searchRanking" FROM documents
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
"teamId" = '${user.teamId}'::uuid AND
"deletedAt" IS NULL
- ORDER BY ts_rank(documents."searchVector", plainto_tsquery('english', :query)) DESC
+ ORDER BY "searchRanking" DESC
LIMIT :limit OFFSET :offset;
`;
@@ -184,10 +188,17 @@ Document.searchForUser = async (user, query, options = {}) => {
})
.map(document => document.id);
+ // Second query to get views for the data
const withViewsScope = { method: ['withViews', user.id] };
- return Document.scope('defaultScope', withViewsScope).findAll({
+ const documents = await Document.scope(
+ 'defaultScope',
+ withViewsScope
+ ).findAll({
where: { id: ids },
});
+
+ // Order the documents in the same order as the first query
+ return _.sortBy(documents, doc => ids.indexOf(doc.id));
};
// Instance methods
diff --git a/yarn.lock b/yarn.lock
index 00bbdb2f8..bb5f34a5d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1825,6 +1825,10 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+consolidated-events@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-1.1.1.tgz#25395465b35e531395418b7bbecb5ecaf198d179"
+
constant-case@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46"
@@ -7215,7 +7219,7 @@ prop-types@>=15.5.6, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8
fbjs "^0.8.9"
loose-envify "^1.3.1"
-prop-types@^15.5.7, prop-types@^15.6.0:
+prop-types@^15.0.0, prop-types@^15.5.7, prop-types@^15.6.0:
version "15.6.0"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@@ -7510,6 +7514,13 @@ react-transform-hmr@^1.0.3:
global "^4.3.0"
react-proxy "^1.1.7"
+react-waypoint@^7.3.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-7.3.1.tgz#abb165d9b6c9590f8d82ceafbe61c2c887262a37"
+ dependencies:
+ consolidated-events "^1.1.0"
+ prop-types "^15.0.0"
+
react@^15.5.4:
version "15.6.2"
resolved "https://registry.npmjs.org/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"