#85: updated to react router 4 and migrated code to support a new api

This commit is contained in:
SleepWalker 2017-05-25 22:11:57 +03:00
parent 6f4eb97b48
commit 6b81385dc5
42 changed files with 496 additions and 324 deletions

View File

@ -195,10 +195,10 @@
"react/jsx-indent-props": "warn",
"react/jsx-key": "warn",
"react/jsx-max-props-per-line": ["warn", {"maximum": 3}],
"react/jsx-no-bind": "warn",
"react/jsx-no-bind": "off",
"react/jsx-no-duplicate-props": "warn",
"react/jsx-no-literals": "warn",
"react/jsx-no-undef": "warn",
"react/jsx-no-undef": "error",
"react/jsx-pascal-case": "warn",
"react/jsx-uses-react": "warn",
"react/jsx-uses-vars": "warn",

77
npm-shrinkwrap.json generated
View File

@ -3789,9 +3789,9 @@
"dev": true
},
"history": {
"version": "3.3.0",
"from": "history@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/history/-/history-3.3.0.tgz"
"version": "4.6.1",
"from": "history@>=4.5.1 <5.0.0",
"resolved": "https://registry.npmjs.org/history/-/history-4.6.1.tgz"
},
"hoek": {
"version": "2.16.3",
@ -5991,7 +5991,8 @@
"query-string": {
"version": "4.3.4",
"from": "query-string@>=4.1.0 <5.0.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz"
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
"dev": true
},
"querystring": {
"version": "0.2.0",
@ -6168,9 +6169,53 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.4.tgz"
},
"react-router": {
"version": "3.0.5",
"from": "react-router@3.0.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-3.0.5.tgz"
"version": "4.1.1",
"from": "react-router@>=4.1.1 <5.0.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.1.1.tgz",
"dependencies": {
"invariant": {
"version": "2.2.2",
"from": "invariant@>=2.2.2 <3.0.0",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz"
},
"isarray": {
"version": "0.0.1",
"from": "isarray@0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
},
"js-tokens": {
"version": "3.0.1",
"from": "js-tokens@^3.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz"
},
"loose-envify": {
"version": "1.3.1",
"from": "loose-envify@^1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz"
},
"path-to-regexp": {
"version": "1.7.0",
"from": "path-to-regexp@>=1.5.3 <2.0.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz"
}
}
},
"react-router-dom": {
"version": "4.1.1",
"from": "react-router-dom@latest",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.1.1.tgz",
"dependencies": {
"js-tokens": {
"version": "3.0.1",
"from": "js-tokens@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz"
},
"loose-envify": {
"version": "1.3.1",
"from": "loose-envify@>=1.3.1 <2.0.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz"
}
}
},
"react-side-effect": {
"version": "1.1.0",
@ -6569,6 +6614,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
"dev": true
},
"resolve-pathname": {
"version": "2.1.0",
"from": "resolve-pathname@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.1.0.tgz"
},
"restore-cursor": {
"version": "1.0.1",
"from": "restore-cursor@>=1.0.1 <2.0.0",
@ -7052,7 +7102,8 @@
"strict-uri-encode": {
"version": "1.1.0",
"from": "strict-uri-encode@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz"
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"dev": true
},
"string_decoder": {
"version": "0.10.31",
@ -7520,6 +7571,11 @@
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.1.7.tgz",
"dev": true
},
"url-search-params-polyfill": {
"version": "1.2.0",
"from": "url-search-params-polyfill@latest",
"resolved": "https://registry.npmjs.org/url-search-params-polyfill/-/url-search-params-polyfill-1.2.0.tgz"
},
"user-home": {
"version": "1.1.1",
"from": "user-home@>=1.1.1 <2.0.0",
@ -7608,6 +7664,11 @@
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
"dev": true
},
"value-equal": {
"version": "0.2.1",
"from": "value-equal@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.2.1.tgz"
},
"varstream": {
"version": "0.3.2",
"from": "varstream@>=0.3.2 <0.4.0",

View File

@ -38,10 +38,11 @@
"react-intl": "^2.0.0",
"react-motion": "^0.4.0",
"react-redux": "^5.0.0",
"react-router": "^3.0.0",
"react-router-dom": "^4.1.1",
"redux": "^3.0.4",
"redux-localstorage": "^0.4.1",
"redux-thunk": "^2.0.0",
"url-search-params-polyfill": "^1.2.0",
"webfontloader": "^1.6.26",
"whatwg-fetch": "^2.0.0"
},

View File

@ -1,7 +1,7 @@
import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import loader from 'services/loader';

View File

@ -1,4 +1,4 @@
import { browserHistory } from 'react-router';
import { browserHistory } from 'services/history';
import { sessionStorage } from 'services/localStorage';
import authentication from 'services/api/authentication';

View File

@ -1,9 +0,0 @@
import React, { Component } from 'react';
export default class OAuthInit extends Component {
static displayName = 'OAuthInit';
render() {
return <span />;
}
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import icons from 'components/ui/icons.scss';
import BaseAuthBody from 'components/auth/BaseAuthBody';

View File

@ -1,4 +1,4 @@
import { browserHistory } from 'react-router';
import { browserHistory } from 'services/history';
import logger from 'services/logger';
import localStorage from 'services/localStorage';

View File

@ -13,15 +13,17 @@ export default class ActivationBody extends BaseAuthBody {
static panelId = 'activation';
static propTypes = {
params: PropTypes.shape({
key: PropTypes.string
match: PropTypes.shape({
params: PropTypes.shape({
key: PropTypes.string
})
})
};
autoFocusField = this.props.params && this.props.params.key ? null : 'key';
autoFocusField = this.props.match.params && this.props.match.params.key ? null : 'key';
render() {
const {key} = this.props.params;
const {key} = this.props.match.params;
const email = this.context.user.email;
return (

View File

@ -16,16 +16,18 @@ export default class RecoverPasswordBody extends BaseAuthBody {
static hasGoBack = true;
static propTypes = {
params: PropTypes.shape({
key: PropTypes.string
match: PropTypes.shape({
params: PropTypes.shape({
key: PropTypes.string
})
})
};
autoFocusField = this.props.params && this.props.params.key ? 'newPassword' : 'key';
autoFocusField = this.props.match.params && this.props.match.params.key ? 'newPassword' : 'key';
render() {
const {user} = this.context;
const {key} = this.props.params;
const {key} = this.props.match.params;
return (
<div>

View File

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { Input, Checkbox, Captcha } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import { LangMenu } from 'components/langMenu';

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { FormattedMessage as Message, FormattedRelative as Relative, FormattedHTMLMessage as HTMLMessage } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import Helmet from 'react-helmet';
import { userShape } from 'components/user/User';
@ -15,7 +15,7 @@ import messages from './Profile.intl.json';
import RulesPage from 'pages/rules/RulesPage';
export default class Profile extends Component {
class Profile extends Component {
static displayName = 'Profile';
static propTypes = {
user: userShape
@ -130,3 +130,9 @@ export default class Profile extends Component {
this.UUID = el;
}
}
import { connect } from 'react-redux';
export default connect((state) => ({
user: state.user
}))(Profile);

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import styles from './profile.scss';

View File

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import FormComponent from 'components/ui/form/FormComponent';

View File

@ -1,7 +1,7 @@
import React, { Component, PropTypes } from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { browserHistory } from 'react-router';
import { browserHistory } from 'services/history';
import styles from './popup.scss';
@ -18,7 +18,7 @@ export class PopupStack extends Component {
componentWillMount() {
document.addEventListener('keyup', this.onKeyPress);
this.unlistenTransition = browserHistory.listenBefore(this.onRouteLeave);
this.unlistenTransition = browserHistory.listen(this.onRouteLeave);
}
componentWillUnmount() {

View File

@ -1,49 +0,0 @@
/**
* Implements scroll to animation with momentum effect
*
* @see http://ariya.ofilabs.com/2013/11/javascript-kinetic-scrolling-part-2.html
*/
import { rAF, getScrollTop } from 'functions';
const TIME_CONSTANT = 100; // higher numbers - slower animation
export function scrollTo(y) {
const start = Date.now();
let scrollWasTouched = false;
rAF(() => { // wrap in rAF to optimize initial reading of scrollTop
const contentHeight = document.documentElement.scrollHeight;
const windowHeight = window.innerHeight;
if (contentHeight < y + windowHeight) {
y = contentHeight - windowHeight;
}
const amplitude = y - getScrollTop();
(function animateScroll() {
const elapsed = Date.now() - start;
let delta = -amplitude * Math.exp(-elapsed / TIME_CONSTANT);
if (Math.abs(delta) > 0.5 && !scrollWasTouched) {
rAF(animateScroll);
} else {
delta = 0;
document.removeEventListener('mousewheel', markScrollTouched);
document.removeEventListener('touchstart', markScrollTouched);
}
if (scrollWasTouched) {
return;
}
const newScrollTop = y + delta;
window.scrollTo(0, newScrollTop);
}());
});
document.addEventListener('mousewheel', markScrollTouched);
document.addEventListener('touchstart', markScrollTouched);
function markScrollTouched() {
scrollWasTouched = true;
}
}

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import buttons from 'components/ui/buttons.scss';

View File

@ -0,0 +1,19 @@
import { PropTypes } from 'react';
import { Route } from 'react-router-dom';
import AuthFlowRouteContents from './AuthFlowRouteContents';
export default function AuthFlowRoute(props) {
const {component: Component, ...routeProps} = props;
return (
<Route {...routeProps} render={(props) => (
<AuthFlowRouteContents routerProps={props} component={Component} />
)}/>
);
}
AuthFlowRoute.propTypes = {
component: PropTypes.any,
routerProps: PropTypes.object
};

View File

@ -0,0 +1,51 @@
import { Component, PropTypes } from 'react';
import { Redirect } from 'react-router-dom';
import authFlow from 'services/authFlow';
export default class AuthFlowRouteContents extends Component {
static propTypes = {
component: PropTypes.any,
routerProps: PropTypes.object
};
state = {
component: null
};
componentDidMount() {
this.handleProps(this.props);
}
componentWillReceiveProps(nextProps) {
this.handleProps(nextProps);
}
render() {
return this.state.component;
}
handleProps(props) {
const {routerProps} = props;
authFlow.handleRequest({
path: routerProps.location.pathname,
params: routerProps.match.params,
query: routerProps.location.query
}, this.onRedirect.bind(this), this.onRouteAllowed.bind(this, props));
}
onRedirect(path) {
this.setState({
component: <Redirect to={path} />
});
}
onRouteAllowed(props) {
const {component: Component} = props;
this.setState({
component: <Component {...props.routerProps} />
});
}
}

View File

@ -0,0 +1,20 @@
import authFlow from 'services/authFlow';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
const PrivateRoute = ({user, component: Component, ...rest}) => (
<Route {...rest} render={(props) => (
user.isGuest ? (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
) : (
<Component {...props}/>
)
)}/>
);
export default connect((state) => ({
user: state.user
}))(PrivateRoute);

View File

@ -92,3 +92,83 @@ export function getScrollTop() {
const doc = document.documentElement;
return (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
}
/**
* Implements scroll to animation with momentum effect
*
* @see http://ariya.ofilabs.com/2013/11/javascript-kinetic-scrolling-part-2.html
*/
const TIME_CONSTANT = 100; // higher numbers - slower animation
export function scrollTo(y) {
const start = Date.now();
let scrollWasTouched = false;
rAF(() => { // wrap in rAF to optimize initial reading of scrollTop
const contentHeight = document.documentElement.scrollHeight;
const windowHeight = window.innerHeight;
if (contentHeight < y + windowHeight) {
y = contentHeight - windowHeight;
}
const amplitude = y - getScrollTop();
(function animateScroll() {
const elapsed = Date.now() - start;
let delta = -amplitude * Math.exp(-elapsed / TIME_CONSTANT);
if (Math.abs(delta) > 0.5 && !scrollWasTouched) {
rAF(animateScroll);
} else {
delta = 0;
document.removeEventListener('mousewheel', markScrollTouched);
document.removeEventListener('touchstart', markScrollTouched);
}
if (scrollWasTouched) {
return;
}
const newScrollTop = y + delta;
window.scrollTo(0, newScrollTop);
}());
});
document.addEventListener('mousewheel', markScrollTouched);
document.addEventListener('touchstart', markScrollTouched);
function markScrollTouched() {
scrollWasTouched = true;
}
}
const SCROLL_ANCHOR_OFFSET = 80; // 50 + 30 (header height + some spacing)
// Первый скролл выполняется сразу после загрузки страницы, так что чтобы снизить
// нагрузку на рендеринг мы откладываем первый скрол на 200ms
let isFirstScroll = true;
/**
* Scrolls to page's top or #anchor link, if any
*/
export function restoreScroll() {
const {hash} = location;
setTimeout(() => {
isFirstScroll = false;
const id = hash.replace('#', '');
const el = id ? document.getElementById(id) : null;
const viewPort = document.body;
if (!viewPort) {
console.log('Can not find viewPort element'); // eslint-disable-line
return;
}
let y = 0;
if (el) {
const {top} = el.getBoundingClientRect();
y = getScrollTop() + top - SCROLL_ANCHOR_OFFSET;
}
scrollTo(y, viewPort);
}, isFirstScroll ? 200 : 0);
}

View File

@ -4,17 +4,19 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Provider as ReduxProvider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import { Router, Route, Switch } from 'react-router-dom';
import { factory as userFactory } from 'components/user/factory';
import { IntlProvider } from 'components/i18n';
import routesFactory from 'routes';
import authFlow from 'services/authFlow';
import storeFactory from 'storeFactory';
import bsodFactory from 'components/ui/bsod/factory';
import loader from 'services/loader';
import logger from 'services/logger';
import font from 'services/font';
import history from 'services/history';
import history, { browserHistory } from 'services/history';
import RootPage from 'pages/root/RootPage';
import AuthFlowRoute from 'containers/AuthFlowRoute';
history.init();
@ -24,7 +26,8 @@ logger.init({
const store = storeFactory();
bsodFactory(store, stopLoading);
bsodFactory(store, () => loader.hide());
authFlow.setStore(store);
Promise.all([
userFactory(store),
@ -34,11 +37,11 @@ Promise.all([
ReactDOM.render(
<ReduxProvider store={store}>
<IntlProvider>
<Router history={browserHistory} onUpdate={() => {
restoreScroll();
stopLoading();
}}>
{routesFactory(store)}
<Router history={browserHistory}>
<Switch>
<AuthFlowRoute path="/oauth2/:version/:clientId?" component={() => null} />
<Route path="/" component={RootPage} />
</Switch>
</Router>
</IntlProvider>
</ReduxProvider>,
@ -48,45 +51,6 @@ Promise.all([
initAnalytics();
});
function stopLoading() {
loader.hide();
}
import { scrollTo } from 'components/ui/scrollTo';
import { getScrollTop } from 'functions';
const SCROLL_ANCHOR_OFFSET = 80; // 50 + 30 (header height + some spacing)
// Первый скролл выполняется сразу после загрузки страницы, так что чтобы снизить
// нагрузку на рендеринг мы откладываем первый скрол на 200ms
let isFirstScroll = true;
/**
* Scrolls to page's top or #anchor link, if any
*/
function restoreScroll() {
const {hash} = location;
setTimeout(() => {
isFirstScroll = false;
const id = hash.replace('#', '');
const el = id ? document.getElementById(id) : null;
const viewPort = document.body;
if (!viewPort) {
console.log('Can not find viewPort element'); // eslint-disable-line
return;
}
let y = 0;
if (el) {
const {top} = el.getBoundingClientRect();
y = getScrollTop() + top - SCROLL_ANCHOR_OFFSET;
}
scrollTo(y, viewPort);
}, isFirstScroll ? 200 : 0);
}
import { loadScript, debounce } from 'functions';
const trackPageView = debounce(_trackPageView);
function initAnalytics() {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { FooterMenu } from 'components/footerMenu';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet';

View File

@ -1,10 +1,22 @@
import React, { Component, PropTypes } from 'react';
import { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Route, Switch, Redirect } from 'react-router-dom';
import AppInfo from 'components/auth/appInfo/AppInfo';
import PanelTransition from 'components/auth/PanelTransition';
import Register from 'components/auth/register/Register';
import Login from 'components/auth/login/Login';
import Permissions from 'components/auth/permissions/Permissions';
import ChooseAccount from 'components/auth/chooseAccount/ChooseAccount';
import Activation from 'components/auth/activation/Activation';
import ResendActivation from 'components/auth/resendActivation/ResendActivation';
import Password from 'components/auth/password/Password';
import AcceptRules from 'components/auth/acceptRules/AcceptRules';
import ForgotPassword from 'components/auth/forgotPassword/ForgotPassword';
import RecoverPassword from 'components/auth/recoverPassword/RecoverPassword';
import Finish from 'components/auth/finish/Finish';
import styles from './auth.scss';
class AuthPage extends Component {
@ -31,7 +43,20 @@ class AuthPage extends Component {
<AppInfo {...client} onGoToAuth={this.onGoToAuth} />
</div>
<div className={styles.content}>
<PanelTransition {...this.props} />
<Switch>
<Route path="/login" render={renderPanelTransition(Login)} />
<Route path="/password" render={renderPanelTransition(Password)} />
<Route path="/register" render={renderPanelTransition(Register)} />
<Route path="/activation/:key?" render={renderPanelTransition(Activation)} />
<Route path="/resend-activation" render={renderPanelTransition(ResendActivation)} />
<Route path="/oauth/permissions" render={renderPanelTransition(Permissions)} />
<Route path="/oauth/choose-account" render={renderPanelTransition(ChooseAccount)} />
<Route path="/oauth/finish" component={Finish} />
<Route path="/accept-rules" render={renderPanelTransition(AcceptRules)} />
<Route path="/forgot-password" render={renderPanelTransition(ForgotPassword)} />
<Route path="/recover-password/:key?" render={renderPanelTransition(RecoverPassword)} />
<Redirect to="/404" />
</Switch>
</div>
</div>
);
@ -44,7 +69,23 @@ class AuthPage extends Component {
};
}
function renderPanelTransition(factory) {
const {Title, Body, Footer, Links} = factory();
return (props) => (
<PanelTransition
key="panel-transition"
Title={<Title />}
Body={<Body {...props} />}
Footer={<Footer />}
Links={<Links />}
{...props}
/>
);
}
export default connect((state) => ({
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
export default withRouter(connect((state) => ({
client: state.auth.client
}))(AuthPage);
}))(AuthPage));

View File

@ -1,22 +0,0 @@
import React, { Component } from 'react';
import ProfilePage from 'pages/profile/ProfilePage';
import Profile from 'components/profile/Profile';
class IndexPage extends Component {
displayName = 'IndexPage';
render() {
return (
<ProfilePage>
<Profile {...this.props} />
</ProfilePage>
);
}
}
import { connect } from 'react-redux';
export default connect((state) => ({
user: state.user
}))(IndexPage);

View File

@ -10,32 +10,33 @@ class ChangeEmailPage extends Component {
static propTypes = {
email: PropTypes.string.isRequired,
lang: PropTypes.string.isRequired,
params: PropTypes.shape({
step: PropTypes.oneOf(['step1', 'step2', 'step3']),
code: PropTypes.string
history: PropTypes.shape({
push: PropTypes.func
}).isRequired,
match: PropTypes.shape({
params: PropTypes.shape({
step: PropTypes.oneOf(['step1', 'step2', 'step3']),
code: PropTypes.string
})
})
};
static contextTypes = {
router: PropTypes.shape({
push: PropTypes.func
}).isRequired,
onSubmit: PropTypes.func.isRequired,
goToProfile: PropTypes.func.isRequired
};
componentWillMount() {
const step = this.props.params.step;
const step = this.props.match.params.step;
if (step && !/^step\d$/.test(step)) {
if (step && !/^step[123]$/.test(step)) {
// wrong param value
// TODO: probably we should decide with something better here
this.context.router.push('/');
this.props.history.push('/404');
}
}
render() {
const {params: {step = 'step1', code}} = this.props;
const {step = 'step1', code} = this.props.match.params;
return (
<ChangeEmail
@ -50,7 +51,7 @@ class ChangeEmailPage extends Component {
}
onChangeStep = (step) => {
this.context.router.push(`/profile/change-email/step${++step}`);
this.props.history.push(`/profile/change-email/step${++step}`);
};
onSubmit = (step, form) => {

View File

@ -1,7 +1,14 @@
import React, { Component, PropTypes } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import logger from 'services/logger';
import Profile from 'components/profile/Profile';
import ChangePasswordPage from 'pages/profile/ChangePasswordPage';
import ChangeUsernamePage from 'pages/profile/ChangeUsernamePage';
import ChangeEmailPage from 'pages/profile/ChangeEmailPage';
import { FooterMenu } from 'components/footerMenu';
import styles from './profile.scss';
@ -31,7 +38,13 @@ class ProfilePage extends Component {
render() {
return (
<div className={styles.container}>
{this.props.children}
<Switch>
<Route path="/profile/change-password" component={ChangePasswordPage} />
<Route path="/profile/change-username" component={ChangeUsernamePage} />
<Route path="/profile/change-email/:step?/:code?" component={ChangeEmailPage} />
<Route path="/" exact component={Profile} />
<Redirect to="/404" />
</Switch>
<div className={styles.footer}>
<FooterMenu />
@ -42,7 +55,7 @@ class ProfilePage extends Component {
}
import { connect } from 'react-redux';
import { browserHistory } from 'react-router';
import { browserHistory } from 'services/history';
import { fetchUserData } from 'components/user/actions';
import { create as createPopup } from 'components/ui/popup/actions';
import PasswordRequestForm from 'components/profile/passwordRequestForm/PasswordRequestForm';

View File

@ -1,14 +1,22 @@
import React, { PropTypes } from 'react';
import { Component, PropTypes } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router';
import { Route, Link, Switch } from 'react-router-dom';
import classNames from 'classnames';
import AuthPage from 'pages/auth/AuthPage';
import ProfilePage from 'pages/profile/ProfilePage';
import RulesPage from 'pages/rules/RulesPage';
import PageNotFound from 'pages/404/PageNotFound';
import { restoreScroll } from 'functions';
import PrivateRoute from 'containers/PrivateRoute';
import AuthFlowRoute from 'containers/AuthFlowRoute';
import Userbar from 'components/userbar/Userbar';
import PopupStack from 'components/ui/popup/PopupStack';
import loader from 'services/loader';
import styles from './root.scss';
import messages from './RootPage.intl.json';
/* global process: false */
@ -19,36 +27,59 @@ if (process.env.NODE_ENV === 'production') {
DevTools = require('containers/DevTools').default;
}
function RootPage(props) {
const isRegisterPage = props.location.pathname === '/register';
class RootPage extends Component {
componentDidMount() {
this.onPageUpdate();
}
document.body.style.overflow = props.isPopupActive ? 'hidden' : '';
componentDidUpdate() {
this.onPageUpdate();
}
return (
<div className={styles.root}>
<div id="view-port" className={classNames(styles.viewPort, {
[styles.isPopupActive]: props.isPopupActive
})}>
<div className={styles.header}>
<div className={styles.headerContent}>
<Link to="/" className={styles.logo} onClick={props.resetAuth}>
<Message {...messages.siteName} />
</Link>
<div className={styles.userbar}>
<Userbar {...props}
guestAction={isRegisterPage ? 'login' : 'register'}
/>
onPageUpdate() {
loader.hide();
restoreScroll();
}
render() {
const props = this.props;
const isRegisterPage = props.location.pathname === '/register';
document.body.style.overflow = props.isPopupActive ? 'hidden' : '';
return (
<div className={styles.root}>
<div id="view-port" className={classNames(styles.viewPort, {
[styles.isPopupActive]: props.isPopupActive
})}>
<div className={styles.header}>
<div className={styles.headerContent}>
<Link to="/" className={styles.logo} onClick={props.onLogoClick}>
<Message {...messages.siteName} />
</Link>
<div className={styles.userbar}>
<Userbar {...props}
guestAction={isRegisterPage ? 'login' : 'register'}
/>
</div>
</div>
</div>
<div className={styles.body}>
<Switch>
<PrivateRoute path="/profile" component={ProfilePage} />
<Route path="/404" component={PageNotFound} />
<Route path="/rules" component={RulesPage} />
<AuthFlowRoute exact path="/" component={ProfilePage} />
<AuthFlowRoute path="/" component={AuthPage} />
<Route component={PageNotFound} />
</Switch>
</div>
</div>
<div className={styles.body}>
{props.children}
</div>
<PopupStack />
<DevTools />
</div>
<PopupStack />
<DevTools />
</div>
);
);
}
}
RootPage.displayName = 'RootPage';
@ -56,17 +87,21 @@ RootPage.propTypes = {
location: PropTypes.shape({
pathname: PropTypes.string
}).isRequired,
user: PropTypes.shape({
isGuest: PropTypes.boolean
}),
children: PropTypes.element,
resetAuth: PropTypes.func.isRequired,
onLogoClick: PropTypes.func.isRequired,
isPopupActive: PropTypes.bool.isRequired
};
import { connect } from 'react-redux';
import { resetAuth } from 'components/auth/actions';
import { withRouter } from 'react-router';
export default connect((state) => ({
export default withRouter(connect((state) => ({
user: state.user,
isPopupActive: state.popup.popups.length > 0
}), {
resetAuth
})(RootPage);
onLogoClick: resetAuth
})(RootPage));

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet';
@ -61,19 +61,18 @@ const rules = [
export default class RulesPage extends Component {
static propTypes = {
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
hash: PropTypes.string
})
};
static contextTypes = {
router: PropTypes.shape({
createLocation: PropTypes.func.required,
replace: PropTypes.func.required
}).isRequired,
history: PropTypes.shape({
replace: PropTypes.func
}).isRequired
};
render() {
let {hash} = this.props.location;
if (hash) {
hash = hash.substring(1);
}
@ -136,9 +135,9 @@ export default class RulesPage extends Component {
}
const {id} = event.currentTarget;
const {router} = this.context;
const newLocation = router.createLocation({...location, hash: `#${id}`});
router.replace(newLocation);
const newPath = `${this.props.location.pathname}${this.props.location.search}#${id}`;
this.props.history.replace(newPath);
}
static getTitleHash(sectionIndex) {

View File

@ -1,4 +1,5 @@
import 'babel-polyfill';
import 'url-search-params-polyfill';
import 'whatwg-fetch';
import { shim as shimPromiseFinaly } from 'promise.prototype.finally';

View File

@ -1,82 +0,0 @@
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import RootPage from 'pages/root/RootPage';
import IndexPage from 'pages/index/IndexPage';
import AuthPage from 'pages/auth/AuthPage';
import RulesPage from 'pages/rules/RulesPage';
import PageNotFound from 'pages/404/PageNotFound';
import ProfilePage from 'pages/profile/ProfilePage';
import ProfileChangePasswordPage from 'pages/profile/ChangePasswordPage';
import ProfileChangeUsernamePage from 'pages/profile/ChangeUsernamePage';
import ProfileChangeEmailPage from 'pages/profile/ChangeEmailPage';
import OAuthInit from 'components/auth/OAuthInit';
import Register from 'components/auth/register/Register';
import Login from 'components/auth/login/Login';
import Permissions from 'components/auth/permissions/Permissions';
import ChooseAccount from 'components/auth/chooseAccount/ChooseAccount';
import Activation from 'components/auth/activation/Activation';
import ResendActivation from 'components/auth/resendActivation/ResendActivation';
import Password from 'components/auth/password/Password';
import AcceptRules from 'components/auth/acceptRules/AcceptRules';
import ForgotPassword from 'components/auth/forgotPassword/ForgotPassword';
import RecoverPassword from 'components/auth/recoverPassword/RecoverPassword';
import Finish from 'components/auth/finish/Finish';
import authFlow from 'services/authFlow';
export default function routesFactory(store) {
authFlow.setStore(store);
const startAuthFlow = {
onEnter: ({location: {query, pathname: path}, params}, replace, callback) =>
authFlow.handleRequest({path, params, query}, replace, callback)
};
const userOnly = {
onEnter: (nextState, replace) => {
const {user} = store.getState();
if (user.isGuest) {
replace('/');
}
}
};
// TODO: when react-router v3 is out, should update to oauth2(/v1)(/:clientId)
// to oauth2(/:version)(/:clientId) with the help of new route matching options
return (
<Route path="/" component={RootPage}>
<IndexRoute component={IndexPage} {...startAuthFlow} />
<Route path="rules" component={RulesPage} />
<Route path="oauth2(/v1)(/:clientId)" component={OAuthInit} {...startAuthFlow} />
<Route path="auth" component={AuthPage}>
<Route path="/login" components={new Login()} {...startAuthFlow} />
<Route path="/password" components={new Password()} {...startAuthFlow} />
<Route path="/register" components={new Register()} {...startAuthFlow} />
<Route path="/activation(/:key)" components={new Activation()} {...startAuthFlow} />
<Route path="/resend-activation" components={new ResendActivation()} {...startAuthFlow} />
<Route path="/oauth/permissions" components={new Permissions()} {...startAuthFlow} />
<Route path="/oauth/choose-account" components={new ChooseAccount()} {...startAuthFlow} />
<Route path="/oauth/finish" component={Finish} {...startAuthFlow} />
<Route path="/accept-rules" components={new AcceptRules()} {...startAuthFlow} />
<Route path="/forgot-password" components={new ForgotPassword()} {...startAuthFlow} />
<Route path="/recover-password(/:key)" components={new RecoverPassword()} {...startAuthFlow} />
</Route>
<Route path="profile" component={ProfilePage} {...userOnly}>
<Route path="change-password" component={ProfileChangePasswordPage} />
<Route path="change-username" component={ProfileChangeUsernamePage} />
<Route path="change-email(/:step)(/:code)" component={ProfileChangeEmailPage} />
</Route>
<Route path="*" component={PageNotFound} />
</Route>
);
}

View File

@ -1,4 +1,4 @@
import { browserHistory } from 'react-router';
import { browserHistory } from 'services/history';
import logger from 'services/logger';
import localStorage from 'services/localStorage';
@ -100,7 +100,7 @@ export default class AuthFlow {
getRequest() {
return {
path: '',
query: {},
query: new URLSearchParams(),
params: {},
...this.currentRequest
};

View File

@ -6,14 +6,14 @@ export default class OAuthState extends AbstractState {
const {query, params} = context.getRequest();
return context.run('oAuthValidate', {
clientId: query.client_id || params.clientId,
redirectUrl: query.redirect_uri,
responseType: query.response_type,
description: query.description,
scope: query.scope,
prompt: query.prompt,
loginHint: query.login_hint,
state: query.state
clientId: query.get('client_id') || params.clientId,
redirectUrl: query.get('redirect_uri'),
responseType: query.get('response_type'),
description: query.get('description'),
scope: query.get('scope'),
prompt: query.get('prompt'),
loginHint: query.get('login_hint'),
state: query.get('state')
}).then(() => context.setState(new CompleteState()));
}
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage as Message, FormattedRelative as Relative } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import messages from './errorsDict.intl.json';

View File

@ -2,6 +2,22 @@
* A helper wrapper service around window.history
*/
import createBrowserHistory from 'history/createBrowserHistory';
export const browserHistory = createBrowserHistory();
browserHistory.listen(() => {
patchHistory(browserHistory);
});
function patchHistory(history) {
Object.assign(history.location,
{query: new URLSearchParams(history.location.search)}
);
}
patchHistory(browserHistory);
export default {
init() {
this.initialLength = window.history.length;

View File

@ -1,7 +1,7 @@
import expect from 'unexpected';
import sinon from 'sinon';
import { browserHistory } from 'react-router';
import { browserHistory } from 'services/history';
import logger from 'services/logger';
import { InternalServerError } from 'services/request';

View File

@ -1,4 +1,5 @@
import expect from 'unexpected';
import sinon from 'sinon';
import AuthFlow from 'services/authFlow/AuthFlow';
@ -33,7 +34,7 @@ describe('AuthFlow.functional', () => {
navigate = function navigate(path, extra = {}) { // emulates router behaviour
if (navigate.lastUrl !== path) {
navigate.lastUrl = path;
flow.handleRequest({path, query: {}, params: {}, ...extra}, navigate);
flow.handleRequest({path, query: new URLSearchParams(), params: {}, ...extra}, navigate);
}
};

View File

@ -1,4 +1,5 @@
import expect from 'unexpected';
import sinon from 'sinon';
import AuthFlow from 'services/authFlow/AuthFlow';
import AbstractState from 'services/authFlow/AbstractState';
@ -352,7 +353,7 @@ describe('AuthFlow', () => {
expect(flow.getRequest(), 'to equal', {
...request,
query: {},
query: new URLSearchParams(),
params: {}
});
});

View File

@ -1,3 +1,5 @@
import sinon from 'sinon';
import OAuthState from 'services/authFlow/OAuthState';
import CompleteState from 'services/authFlow/CompleteState';
@ -29,11 +31,14 @@ describe('OAuthState', () => {
description: 'description',
scope: 'scope',
prompt: 'none',
login_hint: 1,
login_hint: '1',
state: 'state'
};
context.getRequest.returns({query, params: {}});
context.getRequest.returns({
query: new URLSearchParams(query),
params: {}
});
expectRun(
mock,
@ -63,7 +68,7 @@ describe('OAuthState', () => {
};
context.getRequest.returns({
query,
query: new URLSearchParams(query),
params: {clientId}
});
@ -93,7 +98,7 @@ describe('OAuthState', () => {
};
context.getRequest.returns({
query,
query: new URLSearchParams(query),
params: {clientId}
});
@ -115,7 +120,7 @@ describe('OAuthState', () => {
it('should transition to complete state on success', () => {
const promise = Promise.resolve();
context.getRequest.returns({query: {}, params: {}});
context.getRequest.returns({query: new URLSearchParams(), params: {}});
mock.expects('run').returns(promise);
expectState(mock, CompleteState);

View File

@ -52,4 +52,19 @@ describe('services/request', () => {
});
});
});
describe('#buildQuery', () => {
it('should build query', () => {
const data = {
notSet: undefined,
numeric: 1,
complexString: 'sdfgs sdfg ',
positive: true,
negative: false
};
const expectedQs = 'notSet=&numeric=1&complexString=sdfgs%20sdfg%20&positive=1&negative=0';
expect(request.buildQuery(data), 'to equal', expectedQs);
});
});
});

View File

@ -103,7 +103,7 @@ const webpackConfig = {
'react/addons': true
} : {},
devtool: isTest ? 'inline-source-map' : 'eval',
devtool: 'cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({