Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

re-add segment metadata fix #1086 fix #1047 #1088

Merged
merged 14 commits into from
Jun 29, 2017
18 changes: 17 additions & 1 deletion src/server/createSSR.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import {applyMiddleware, createStore} from 'redux';
import thunkMiddleware from 'redux-thunk';
import makeSegmentSnippet from '@segment/snippet';
import getWebpackPublicPath from 'server/utils/getWebpackPublicPath';
import makeReducer from 'universal/redux/makeReducer';
import printStyles from 'universal/styles/theme/printStyles';
Expand Down Expand Up @@ -40,17 +41,32 @@ export default function createSSR(req, res) {
}
res.send(cachedPage);
} else {
/*
* When segment.io is configured during development, load the segment
* snippet here. For production use, refer to the Html.js component.
*/
const segKey = process.env.SEGMENT_WRITE_KEY;
const segmentSnippet = segKey && `
<script>
${makeSegmentSnippet.min({
host: 'cdn.segment.com',
apiKey: segKey
})}
</script>
`;

const devHtml = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/static/css/font-awesome.css"/>
</head>
<body>
<body>
<div id="root"></div>
<script src="/static/vendors.dll.js"></script>
<script src="/static/app.js"></script>
<script>${clientKeyLoader}</script>
${segmentSnippet}
</body>
</html>
`;
Expand Down
66 changes: 23 additions & 43 deletions src/universal/components/Action/Action.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import {css} from 'aphrodite-local-styles/no-important';
import PropTypes from 'prop-types';
import React from 'react';
import withStyles from 'universal/styles/withStyles';
import {css} from 'aphrodite-local-styles/no-important';
import Toast from 'universal/modules/toast/containers/Toast/Toast';
import {Route, Switch} from 'react-router-dom';
import LandingContainer from 'universal/modules/landing/containers/Landing/LandingContainer';
import AsyncRoute from 'universal/components/AsyncRoute/AsyncRoute';
import LandingContainer from 'universal/modules/landing/containers/Landing/LandingContainer';
import Toast from 'universal/modules/toast/containers/Toast/Toast';
import withStyles from 'universal/styles/withStyles';

const socketRoute = () => System.import('universal/components/SocketRoute/SocketRoute');
const invoice = () => System.import('universal/modules/invoice/containers/InvoiceContainer/InvoiceContainer');
const meetingSummary = () => System.import('universal/modules/summary/containers/MeetingSummary/MeetingSummaryContainer');
const welcome = () => System.import('universal/modules/welcome/containers/Welcome/Welcome');
const graphql = () => System.import('universal/modules/admin/containers/Graphql/GraphqlContainer');
const impersonate = () => System.import('universal/modules/admin/containers/Impersonate/ImpersonateContainer');
const invitation = () => System.import('universal/modules/invitation/containers/Invitation/InvitationContainer');
const signout = () => System.import('universal/containers/Signout/SignoutContainer');
const notFound = () => System.import('universal/components/NotFound/NotFound');

const Action = (props) => {
const {styles} = props;
Expand All @@ -14,45 +24,15 @@ const Action = (props) => {
<Toast />
<Switch>
<Route exact path="/" component={LandingContainer} />
<AsyncRoute
isPrivate
path="(/me|/meeting|/newteam|/team)"
mod={() => System.import('universal/components/SocketRoute/SocketRoute')}
/>
<AsyncRoute
isPrivate
path="/invoice/:invoiceId"
mod={() => System.import('universal/modules/invoice/containers/InvoiceContainer/InvoiceContainer')}
/>
<AsyncRoute
isPrivate
path="/summary/:meetingId"
mod={() => System.import('universal/modules/summary/containers/MeetingSummary/MeetingSummaryContainer')}
/>
<AsyncRoute
isPrivate
path="/welcome"
mod={() => System.import('universal/modules/welcome/containers/Welcome/Welcome')}
/>
<AsyncRoute
path="/admin/graphql"
mod={() => System.import('universal/modules/admin/containers/Graphql/GraphqlContainer')}
/>
<AsyncRoute
path="/admin/impersonate/:newUserId"
mod={() => System.import('universal/modules/admin/containers/Impersonate/ImpersonateContainer')}
/>
<AsyncRoute
path="/invitation/:id"
mod={() => System.import('universal/modules/invitation/containers/Invitation/InvitationContainer')}
/>
<AsyncRoute
path="/signout"
mod={() => System.import('universal/containers/Signout/SignoutContainer')}
/>
<AsyncRoute
mod={() => System.import('universal/components/NotFound/NotFound')}
/>
<AsyncRoute isAbstract isPrivate path="(/me|/meeting|/newteam|/team)" mod={socketRoute} />
<AsyncRoute isPrivate path="/invoice/:invoiceId" mod={invoice} />
<AsyncRoute isPrivate path="/summary/:meetingId" mod={meetingSummary} />
<AsyncRoute isPrivate path="/welcome" mod={welcome} />
<AsyncRoute path="/admin/graphql" mod={graphql} />
<AsyncRoute path="/admin/impersonate/:newUserId" mod={impersonate} />
<AsyncRoute path="/invitation/:id" mod={invitation} />
<AsyncRoute mod={signout} />
<AsyncRoute mod={notFound} />
</Switch>
</div>
);
Expand Down
27 changes: 19 additions & 8 deletions src/universal/components/AsyncRoute/AsyncRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,29 @@ import {Route} from 'react-router-dom';
import PropTypes from 'prop-types';
import Bundle from '../Bundle/Bundle';

const AsyncRoute = ({mod, exact, path, isPrivate, ...extra}) => (
<Route
exact={exact} path={path}
render={({history, location, match}) => (
<Bundle extra={extra} history={history} isPrivate={isPrivate} location={location} match={match} mod={mod} />
const AsyncRoute = ({mod, exact, path, isAbstract, isPrivate, extraProps}) => {
return (
<Route
exact={exact} path={path}
render={({history, location, match}) => (
<Bundle
isAbstractRoute={isAbstract}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

extraProps={extraProps}
history={history}
isPrivate={isPrivate}
location={location}
match={match}
mod={mod}
/>
)}
/>
);
/>
);
};

AsyncRoute.propTypes = {
exact: PropTypes.bool,
extra: PropTypes.object,
extraProps: PropTypes.object,
isAbstract: PropTypes.bool,
isPrivate: PropTypes.bool,
mod: PropTypes.func.isRequired,
path: PropTypes.string
Expand Down
52 changes: 46 additions & 6 deletions src/universal/components/Bundle/Bundle.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import requireAuth from 'universal/decorators/requireAuth/requireAuth';
import {segmentEventPage} from 'universal/redux/segmentActions';

const updateAnalyticsPage = (dispatch, lastPath, nextPath, title, params) => {
if (typeof document === 'undefined' || typeof window.analytics === 'undefined') return;
const properties = {
title,
referrer: lastPath,
path: nextPath,
params
};
dispatch(segmentEventPage(title, null, properties));
};

class Bundle extends Component {
static contextTypes = {
analytics: PropTypes.object,
store: PropTypes.object
};

static propTypes = {
extra: PropTypes.object,
extraProps: PropTypes.object,
history: PropTypes.object.isRequired,
isAbstractRoute: PropTypes.bool,
isPrivate: PropTypes.bool,
location: PropTypes.object.isRequired,
match: PropTypes.object,
Expand All @@ -24,13 +38,39 @@ class Bundle extends Component {
this.loadMod(this.props);
}

componentDidMount() {
const {location: {pathname: nextPath}, isAbstractRoute, match: {params}} = this.props;
if (!isAbstractRoute) {
const {store: {dispatch}} = this.context;
// can't use setTimeout, since react rendering is not guaranteed sync
// use requestIdleCallback to ensure that rendering eg '/me' has completed
window.requestIdleCallback(() => {
const {analytics: {lastPath, title}} = this.context;
updateAnalyticsPage(dispatch, lastPath, nextPath, title, params);
});
}
}

componentWillReceiveProps(nextProps) {
const {mod} = this.props;
if (mod !== nextProps.mod) {
const {mod} = nextProps;
if (mod !== this.props.mod) {
this.loadMod(nextProps);
}
}

componentDidUpdate(prevProps) {
// use cDU to allow helmet to update the document title for the subcomponents
const {isAbstractRoute} = this.props;
if (!isAbstractRoute) {
const {location: {pathname: nextPath}, match: {params}} = this.props;
const {location: {pathname: lastPath}} = prevProps;
if (lastPath !== nextPath) {
const {store: {dispatch}, analytics: {title}} = this.context;
updateAnalyticsPage(dispatch, lastPath, nextPath, title, params);
}
}
}

loadMod(props) {
this.setState({Mod: null});
const {isPrivate, mod} = props;
Expand All @@ -48,8 +88,8 @@ class Bundle extends Component {
render() {
const {Mod} = this.state;
if (!Mod) return null;
const {history, location, match, extra} = this.props;
return <Mod {...extra} history={history} location={location} match={match} />;
const {history, location, match, extraProps} = this.props;
return <Mod {...extraProps} history={history} location={location} match={match} />;
}
}

Expand Down
13 changes: 7 additions & 6 deletions src/universal/components/DashboardWrapper/DashboardWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import DashSidebar from 'universal/components/Dashboard/DashSidebar';
import DashLayoutContainer from 'universal/containers/DashLayoutContainer/DashLayoutContainer';
import AsyncRoute from 'universal/components/AsyncRoute/AsyncRoute';

const userDashboard = () => System.import('universal/modules/userDashboard/components/UserDashboard/UserDashboard');
const teamContainer = () => System.import('universal/modules/teamDashboard/containers/Team/TeamContainer');
const newTeam = () => System.import('universal/modules/newTeam/containers/NewTeamForm/NewTeamFormContainer');

const DashboardWrapper = () => {
return (
<DashLayoutContainer>
<DashSidebar />
<AsyncRoute path="/me" mod={() => System.import('universal/modules/userDashboard/components/UserDashboard/UserDashboard')} />
<AsyncRoute path="/team/:teamId" mod={() => System.import('universal/modules/teamDashboard/containers/Team/TeamContainer')} />
<AsyncRoute
path="/newteam/:newOrg?"
mod={() => System.import('universal/modules/newTeam/containers/NewTeamForm/NewTeamFormContainer')}
/>
<AsyncRoute isAbstract path="/me" mod={userDashboard} />
<AsyncRoute isAbstract path="/team/:teamId" mod={teamContainer} />
<AsyncRoute path="/newteam/:newOrg?" mod={newTeam} />
</DashLayoutContainer>
);
};
Expand Down
27 changes: 27 additions & 0 deletions src/universal/components/ParabolHelmet/ParabolHelmet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import Helmet from 'react-helmet';

export default class ParabolHelmet extends Component {
static contextTypes = {
analytics: PropTypes.object
};

static propTypes = {
title: PropTypes.string
};

componentDidMount() {
this.context.analytics.title = this.props.title;
}

componentWillReceiveProps(nextProps) {
if (nextProps.title !== this.props.title) {
this.context.analytics.title = nextProps.title;
}
}

render() {
return <Helmet {...this.props} />;
}
}
11 changes: 11 additions & 0 deletions src/universal/components/ProjectEditor/Draft.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/universal/components/ProjectEditor/ProjectEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import withKeyboardShortcuts from './withKeyboardShortcuts';
import withLinks from './withLinks';
import withSuggestions from './withSuggestions';
import entitizeText from 'universal/utils/draftjs/entitizeText';
import './Draft.css';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This css file fails a production build. See: https://circleci.com/gh/ParabolInc/action/2904

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh silly me i bet i didn't add it to the prod webpack config


class ProjectEditor extends Component {

Expand Down
48 changes: 15 additions & 33 deletions src/universal/components/SocketRoute/SocketRoute.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,35 @@
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import React from 'react';
import {DragDropContext as dragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import {Switch} from 'react-router-dom';
import {socketClusterReducer} from 'redux-socket-cluster';
import AsyncRoute from 'universal/components/AsyncRoute/AsyncRoute';
import socketWithPresence from 'universal/decorators/socketWithPresence/socketWithPresence';
import withReducer from '../../decorators/withReducer/withReducer';
import withAsync from 'react-async-hoc';

const parentMod = () => System.import('universal/components/DashboardWrapper/DashboardWrapper');
const meetingMod = () => System.import('universal/modules/meeting/containers/MeetingContainer/MeetingContainer');

class SocketRoute extends Component {
shouldComponentUpdate() {
return false;
}
render() {
return (
<Switch>
<AsyncRoute
path="(/me|/newteam|/team)"
mod={parentMod}
/>
<AsyncRoute
path="/meeting/:teamId/:localPhase?/:localPhaseItem?"
mod={meetingMod}
/>
</Switch>
);
}
}
const dashWrapper = () => System.import('universal/components/DashboardWrapper/DashboardWrapper');
const meetingContainer = () => System.import('universal/modules/meeting/containers/MeetingContainer/MeetingContainer');

const SocketRoute = () => {
return (
<Switch>
<AsyncRoute isAbstract path="(/me|/newteam|/team)" mod={dashWrapper} />
<AsyncRoute path="/meeting/:teamId/:localPhase?/:localPhaseItem?" mod={meetingContainer} />
</Switch>
);
};

SocketRoute.propTypes = {
match: PropTypes.object.isRequired
};

const fetchStyles = {
'/static/css/Draft.css': () => ({stylesLoaded: true})
};

export default
withAsync(undefined, fetchStyles)(
withReducer({socket: socketClusterReducer})(
dragDropContext(HTML5Backend)(
socketWithPresence(
SocketRoute
)
withReducer({socket: socketClusterReducer})(
dragDropContext(HTML5Backend)(
socketWithPresence(
SocketRoute
)
)
);
Loading