From f5f6f9647ed940d5154b0c1aeefc79f182782e29 Mon Sep 17 00:00:00 2001 From: mkrause Date: Fri, 2 Jun 2017 17:50:07 +0200 Subject: [PATCH 1/6] (#221) Implement profile widget in the header. --- package-lock.json | 17 ++++ .../app/components/ProtectedRoute.jsx | 18 ++++ .../app/components/ProtectedRoute.test.jsx | 18 ++++ .../components/app/components/Routes.jsx | 44 +++++----- .../ProtectedRoute.test.jsx.snap | 16 ++++ .../__snapshots__/Routes.test.jsx.snap | 13 ++- .../components/header/components/Header.jsx | 49 +++++++++-- .../profile/components/ProfileWidget.jsx | 87 +++++++++++++++++++ src/client/components/profile/index.js | 1 + 9 files changed, 227 insertions(+), 36 deletions(-) create mode 100644 src/client/components/app/components/ProtectedRoute.jsx create mode 100644 src/client/components/app/components/ProtectedRoute.test.jsx create mode 100644 src/client/components/app/components/__snapshots__/ProtectedRoute.test.jsx.snap create mode 100644 src/client/components/profile/components/ProfileWidget.jsx diff --git a/package-lock.json b/package-lock.json index 585e202..6bb3426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5876,6 +5876,23 @@ "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.4.5.tgz", "integrity": "sha1-4+iVoJcM8U7o+JAROvaBl6vz0LE=" }, + "react-icon-base": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/react-icon-base/-/react-icon-base-2.0.7.tgz", + "integrity": "sha1-C9GHNr1s55ym1pzoOHoH+41M7/4=", + "dependencies": { + "prop-types": { + "version": "15.5.8", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.8.tgz", + "integrity": "sha1-a3suFBCDvjjIWVqlH8VXdccZk5Q=" + } + } + }, + "react-icons": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-2.2.5.tgz", + "integrity": "sha1-+UJQHCGkzARWziu+5QMsk/YFHc8=" + }, "react-notification-system": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/react-notification-system/-/react-notification-system-0.2.14.tgz", diff --git a/src/client/components/app/components/ProtectedRoute.jsx b/src/client/components/app/components/ProtectedRoute.jsx new file mode 100644 index 0000000..1d52f98 --- /dev/null +++ b/src/client/components/app/components/ProtectedRoute.jsx @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import React from 'react' +import { Route, Redirect } from 'react-router-dom' +import { selectors } from '../../../data/session' + +const mapStateToProps = state => ({ + hasAuth: selectors.hasToken(state) +}) + +export const DumbProtectedRoute = ({ hasAuth, ...props }) => + hasAuth ? : + +DumbProtectedRoute.propTypes = { + hasAuth: PropTypes.bool.isRequired +} + +export default connect(mapStateToProps)(DumbProtectedRoute) diff --git a/src/client/components/app/components/ProtectedRoute.test.jsx b/src/client/components/app/components/ProtectedRoute.test.jsx new file mode 100644 index 0000000..bdcb6ae --- /dev/null +++ b/src/client/components/app/components/ProtectedRoute.test.jsx @@ -0,0 +1,18 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { shallowToJson } from 'enzyme-to-json' +import { DumbProtectedRoute } from './ProtectedRoute' + +describe('', () => { + it('renders a redirect when hasAuth is false', () => { + const wrapper = shallow( + null} /> + ) + expect(shallowToJson(wrapper)).toMatchSnapshot() + }) + + it('renders the route when hasAuth is true', () => { + const wrapper = shallow( null} />) + expect(shallowToJson(wrapper)).toMatchSnapshot() + }) +}) diff --git a/src/client/components/app/components/Routes.jsx b/src/client/components/app/components/Routes.jsx index 746104d..0b226a4 100644 --- a/src/client/components/app/components/Routes.jsx +++ b/src/client/components/app/components/Routes.jsx @@ -10,32 +10,32 @@ import Signup from '../../../scenes/Signup' import SignupConfirmContainer from '../../../scenes/SignupConfirm' import ResetPassword from '../../../scenes/ResetPassword' import Missing from '../../../scenes/Missing' +import ProtectedRoute from './ProtectedRoute' -function Routes() { - return ( -
-
- - +const Routes = () => ( +
+
+ + {/* Login */} + + + - {/* Login */} - - - + {/* Signup */} + + - {/* Signup */} - - + {/* Home */} + - {/* Profile */} - + {/* Profile */} + - {/* 404 */} - - - -
- ) -} + {/* 404 */} + +
+ +
+) export default Routes diff --git a/src/client/components/app/components/__snapshots__/ProtectedRoute.test.jsx.snap b/src/client/components/app/components/__snapshots__/ProtectedRoute.test.jsx.snap new file mode 100644 index 0000000..2b386ee --- /dev/null +++ b/src/client/components/app/components/__snapshots__/ProtectedRoute.test.jsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders a redirect when hasAuth is false 1`] = ` + +`; + +exports[` renders the route when hasAuth is true 1`] = ` + +`; diff --git a/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap b/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap index 84afe11..09129d0 100644 --- a/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap +++ b/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap @@ -4,11 +4,6 @@ exports[` renders correctly 1`] = `
- renders correctly 1`] = ` /> renders correctly 1`] = ` component={[Function]} path="/signup/confirm" /> - + diff --git a/src/client/components/header/components/Header.jsx b/src/client/components/header/components/Header.jsx index 44f97a0..c9ddc9f 100644 --- a/src/client/components/header/components/Header.jsx +++ b/src/client/components/header/components/Header.jsx @@ -1,13 +1,48 @@ +/* eslint-disable react/jsx-indent-props, react/jsx-closing-bracket-location */ + +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { withRouter } from 'react-router' import React from 'react' +import { RaisedButton } from 'material-ui' +import { colors } from '../../../styles' +import { selectors } from '../../../data/session' +import { ProfileWidget } from '../../profile' import Wrapper from './Wrapper' import Logo from './Logo' -function Header() { - return ( - - - - ) +const mapStateToProps = state => ({ + hasAuth: selectors.hasToken(state) +}) + +const LoginButton = ({ onLogin }) => ( + +) + +LoginButton.propTypes = { onLogin: PropTypes.func.isRequired } + +const DumbHeader = ({ hasAuth, history }) => ( + + + {hasAuth + ? + : { + history.push('/login') + }} + />} + +) + +DumbHeader.propTypes = { + hasAuth: PropTypes.bool.isRequired, + history: PropTypes.object.isRequired } -export default Header +export default connect(mapStateToProps)(withRouter(DumbHeader)) diff --git a/src/client/components/profile/components/ProfileWidget.jsx b/src/client/components/profile/components/ProfileWidget.jsx new file mode 100644 index 0000000..d0c1f79 --- /dev/null +++ b/src/client/components/profile/components/ProfileWidget.jsx @@ -0,0 +1,87 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { connect } from 'react-redux' +import { withRouter } from 'react-router' +import { IconMenu, MenuItem, Avatar } from 'material-ui' +import { HardwareKeyboardArrowDown, ActionSettings, ActionExitToApp } from 'material-ui/svg-icons' +import styled from 'styled-components' +import { colors } from '../../../styles' +import { fetchProfile } from '../actions' +import { logout } from '../../../data/session/actions' +import { getProfile } from '../selectors' + +const mapStateToProps = state => ({ profile: getProfile(state) }) +const mapStateToDispatch = dispatch => ({ + fetchProfile: () => dispatch(fetchProfile()), + logout: () => dispatch(logout()) +}) + +const ProfileName = styled.span` + color: ${colors.white}; + font-size: 0.8rem; + font-weight: 200; + margin-left: 15px; +` + +const ProfileWidgetContent = styled.div` + & * { + vertical-align: middle; + } + &:hover { + cursor: pointer; + } +` + +class ProfileWidget extends React.Component { + componentWillMount() { + this.props.fetchProfile() + } + + render() { + const { profile } = this.props + return ( + + {/* Note: must be wrapped in a div, otherwise click events are not properly attached */} + + + {profile.name} + + +
+ } + > + } + primaryText="Instellingen" + onTouchTap={() => { + this.props.history.push('/settings') + }} + /> + } + primaryText="Uitloggen" + onTouchTap={() => { + this.props.logout() + this.props.history.push('/login') + }} + /> + + ) + } +} + +ProfileWidget.propTypes = { + history: PropTypes.object.isRequired, + profile: PropTypes.object.isRequired, + fetchProfile: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired +} + +export default connect(mapStateToProps, mapStateToDispatch)(withRouter(ProfileWidget)) diff --git a/src/client/components/profile/index.js b/src/client/components/profile/index.js index 36b5837..b315997 100644 --- a/src/client/components/profile/index.js +++ b/src/client/components/profile/index.js @@ -4,4 +4,5 @@ import * as actionTypes from './actionTypes' export { actionTypes, sagas } export { default as ProfileForm } from './components/ProfileForm' export { default as PasswordForm } from './components/PasswordForm' +export { default as ProfileWidget } from './components/ProfileWidget' export { default as reducer } from './reducer' From b7377f0f5f06deaed4fe05cee6af052dcdb0cf96 Mon Sep 17 00:00:00 2001 From: mkrause Date: Fri, 2 Jun 2017 17:50:46 +0200 Subject: [PATCH 2/6] Disable no-confusing-arrow in eslint, due to conflict with prettier. --- .eslintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc b/.eslintrc index 17145bb..4192336 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ ], "rules": { "arrow-parens": 0, + "no-confusing-arrow": 0, "comma-dangle": 0, "func-names": ["error", "as-needed"], "import/no-extraneous-dependencies": 0, From 3c7b72ba7f1af26dda86d6709a0f9d17e0686416 Mon Sep 17 00:00:00 2001 From: mkrause Date: Fri, 2 Jun 2017 18:03:13 +0200 Subject: [PATCH 3/6] (#221) Add tests. --- .../__snapshots__/Routes.test.jsx.snap | 2 +- .../components/header/components/Header.jsx | 2 +- .../header/components/Header.test.jsx | 13 ++- .../__snapshots__/Header.test.jsx.snap | 12 ++- .../profile/components/ProfileWidget.jsx | 6 +- .../profile/components/ProfileWidget.test.jsx | 20 ++++ .../__snapshots__/ProfileWidget.test.jsx.snap | 98 +++++++++++++++++++ 7 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 src/client/components/profile/components/ProfileWidget.test.jsx create mode 100644 src/client/components/profile/components/__snapshots__/ProfileWidget.test.jsx.snap diff --git a/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap b/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap index 09129d0..944f51e 100644 --- a/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap +++ b/src/client/components/app/components/__snapshots__/Routes.test.jsx.snap @@ -2,7 +2,7 @@ exports[` renders correctly 1`] = `
-
+ ( LoginButton.propTypes = { onLogin: PropTypes.func.isRequired } -const DumbHeader = ({ hasAuth, history }) => ( +export const DumbHeader = ({ hasAuth, history }) => ( {hasAuth diff --git a/src/client/components/header/components/Header.test.jsx b/src/client/components/header/components/Header.test.jsx index 598aa73..57efbf2 100644 --- a/src/client/components/header/components/Header.test.jsx +++ b/src/client/components/header/components/Header.test.jsx @@ -1,11 +1,16 @@ import React from 'react' import { shallow } from 'enzyme' import { shallowToJson } from 'enzyme-to-json' -import Header from './Header' +import { DumbHeader } from './Header' -describe('
', () => { - it('renders correctly', () => { - const wrapper = shallow(
) +describe('', () => { + it('renders with login button when we are not authenticated', () => { + const wrapper = shallow() + expect(shallowToJson(wrapper)).toMatchSnapshot() + }) + + it('renders with a profile widget when we are authenticated', () => { + const wrapper = shallow() expect(shallowToJson(wrapper)).toMatchSnapshot() }) }) diff --git a/src/client/components/header/components/__snapshots__/Header.test.jsx.snap b/src/client/components/header/components/__snapshots__/Header.test.jsx.snap index 91a32ab..ff54379 100644 --- a/src/client/components/header/components/__snapshots__/Header.test.jsx.snap +++ b/src/client/components/header/components/__snapshots__/Header.test.jsx.snap @@ -1,7 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`
renders correctly 1`] = ` +exports[` renders with a profile widget when we are authenticated 1`] = ` + + +`; + +exports[` renders with login button when we are not authenticated 1`] = ` + + + `; diff --git a/src/client/components/profile/components/ProfileWidget.jsx b/src/client/components/profile/components/ProfileWidget.jsx index d0c1f79..054f26e 100644 --- a/src/client/components/profile/components/ProfileWidget.jsx +++ b/src/client/components/profile/components/ProfileWidget.jsx @@ -32,7 +32,7 @@ const ProfileWidgetContent = styled.div` } ` -class ProfileWidget extends React.Component { +export class DumbProfileWidget extends React.Component { componentWillMount() { this.props.fetchProfile() } @@ -77,11 +77,11 @@ class ProfileWidget extends React.Component { } } -ProfileWidget.propTypes = { +DumbProfileWidget.propTypes = { history: PropTypes.object.isRequired, profile: PropTypes.object.isRequired, fetchProfile: PropTypes.func.isRequired, logout: PropTypes.func.isRequired } -export default connect(mapStateToProps, mapStateToDispatch)(withRouter(ProfileWidget)) +export default connect(mapStateToProps, mapStateToDispatch)(withRouter(DumbProfileWidget)) diff --git a/src/client/components/profile/components/ProfileWidget.test.jsx b/src/client/components/profile/components/ProfileWidget.test.jsx new file mode 100644 index 0000000..2e7593a --- /dev/null +++ b/src/client/components/profile/components/ProfileWidget.test.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { shallowToJson } from 'enzyme-to-json' +import { DumbProfileWidget } from './ProfileWidget' + +describe('', () => { + it('renders correctly', () => { + const wrapper = shallow( + {}} + logout={() => {}} + /> + ) + expect(shallowToJson(wrapper)).toMatchSnapshot() + }) +}) diff --git a/src/client/components/profile/components/__snapshots__/ProfileWidget.test.jsx.snap b/src/client/components/profile/components/__snapshots__/ProfileWidget.test.jsx.snap new file mode 100644 index 0000000..5ccd36c --- /dev/null +++ b/src/client/components/profile/components/__snapshots__/ProfileWidget.test.jsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` + + + + + John Doe + + + +
+ } + multiple={false} + onItemTouchTap={[Function]} + onKeyboardFocus={[Function]} + onMouseDown={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + onMouseUp={[Function]} + onRequestChange={[Function]} + onTouchTap={[Function]} + open={null} + targetOrigin={ + Object { + "horizontal": "right", + "vertical": "top", + } + } + touchTapCloseDelay={200} + useLayerForClickAway={false} +> + } + onTouchTap={[Function]} + primaryText="Instellingen" + targetOrigin={ + Object { + "horizontal": "left", + "vertical": "top", + } + } + /> + } + onTouchTap={[Function]} + primaryText="Uitloggen" + targetOrigin={ + Object { + "horizontal": "left", + "vertical": "top", + } + } + /> + +`; From 0a0f4c4fdd14fbb45ed16a23b73ba6caaa52e658 Mon Sep 17 00:00:00 2001 From: mkrause Date: Fri, 2 Jun 2017 18:11:19 +0200 Subject: [PATCH 4/6] Convert arrow -> to function, to get CI to pass. --- src/client/components/app/components/ProtectedRoute.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/components/app/components/ProtectedRoute.jsx b/src/client/components/app/components/ProtectedRoute.jsx index 1d52f98..035a517 100644 --- a/src/client/components/app/components/ProtectedRoute.jsx +++ b/src/client/components/app/components/ProtectedRoute.jsx @@ -8,8 +8,9 @@ const mapStateToProps = state => ({ hasAuth: selectors.hasToken(state) }) -export const DumbProtectedRoute = ({ hasAuth, ...props }) => - hasAuth ? : +export function DumbProtectedRoute({ hasAuth, ...props }) { + return hasAuth ? : +} DumbProtectedRoute.propTypes = { hasAuth: PropTypes.bool.isRequired From 1d24f91806ae7442b3bf60fcef8893fc3bccd51d Mon Sep 17 00:00:00 2001 From: mkrause Date: Fri, 2 Jun 2017 21:12:26 +0200 Subject: [PATCH 5/6] (#221) Inline the login button. --- .../components/header/components/Header.jsx | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/client/components/header/components/Header.jsx b/src/client/components/header/components/Header.jsx index a00c883..5faa560 100644 --- a/src/client/components/header/components/Header.jsx +++ b/src/client/components/header/components/Header.jsx @@ -15,25 +15,17 @@ const mapStateToProps = state => ({ hasAuth: selectors.hasToken(state) }) -const LoginButton = ({ onLogin }) => ( - -) - -LoginButton.propTypes = { onLogin: PropTypes.func.isRequired } - export const DumbHeader = ({ hasAuth, history }) => ( {hasAuth ? - : { + : { history.push('/login') }} />} From ea37a7d406d3f646b8dad345e30fe5d0bc5d73e5 Mon Sep 17 00:00:00 2001 From: mkrause Date: Fri, 2 Jun 2017 21:41:41 +0200 Subject: [PATCH 6/6] (#221) Add test for login button. --- .../header/components/Header.test.jsx | 8 ++++++++ .../__snapshots__/Header.test.jsx.snap | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/client/components/header/components/Header.test.jsx b/src/client/components/header/components/Header.test.jsx index 57efbf2..7802e86 100644 --- a/src/client/components/header/components/Header.test.jsx +++ b/src/client/components/header/components/Header.test.jsx @@ -1,6 +1,7 @@ import React from 'react' import { shallow } from 'enzyme' import { shallowToJson } from 'enzyme-to-json' +import { RaisedButton } from 'material-ui' import { DumbHeader } from './Header' describe('', () => { @@ -13,4 +14,11 @@ describe('', () => { const wrapper = shallow() expect(shallowToJson(wrapper)).toMatchSnapshot() }) + + it('navigates to the login page when clicking the login button', () => { + const push = jest.fn() + const wrapper = shallow() + wrapper.find(RaisedButton).simulate('click') + expect(push).toBeCalledWith('/login') + }) }) diff --git a/src/client/components/header/components/__snapshots__/Header.test.jsx.snap b/src/client/components/header/components/__snapshots__/Header.test.jsx.snap index ff54379..50e8242 100644 --- a/src/client/components/header/components/__snapshots__/Header.test.jsx.snap +++ b/src/client/components/header/components/__snapshots__/Header.test.jsx.snap @@ -10,8 +10,21 @@ exports[` renders with a profile widget when we are authenticated exports[` renders with login button when we are not authenticated 1`] = ` - `;