Fix routing issues. Fix panel layout.

This commit is contained in:
John Lyon-Smith
2018-03-23 13:49:41 -07:00
parent 54365d3566
commit ce25d56dfe
17 changed files with 240 additions and 98 deletions

View File

@@ -1 +1,2 @@
REACT_APP_TITLE=Deighton AR System REACT_APP_TITLE=Deighton AR System
REACT_APP_SUPPORT_EMAIL=support@kss.us.com

View File

@@ -1,5 +1,6 @@
import EventEmitter from 'eventemitter3' import EventEmitter from 'eventemitter3'
import io from 'socket.io-client' import io from 'socket.io-client'
import autobind from 'autobind-decorator'
const authTokenName = 'AuthToken' const authTokenName = 'AuthToken'
@@ -28,6 +29,7 @@ class APIError extends Error {
} }
} }
@autobind
class API extends EventEmitter { class API extends EventEmitter {
constructor() { constructor() {
super() super()

View File

@@ -1,5 +1,5 @@
import React, { Component, Fragment } from 'react' import React, { Component, Fragment } from 'react'
import { Login, Logout, ResetPassword, ForgotPassword, ConfirmEmail, ProtectedRoute } from './Auth' import { Login, Logout, ResetPassword, ForgotPassword, ConfirmEmail, ProtectedRoute, DefaultRoute } from './Auth'
import { Home } from './Home' import { Home } from './Home'
import { Profile } from './Profile' import { Profile } from './Profile'
import { Users } from './Users' import { Users } from './Users'
@@ -92,16 +92,17 @@ export class App extends Component {
</Box> </Box>
</Column.Item> </Column.Item>
<Switch> <Switch>
<Route path='/login' component={Login} /> <Route exact path='/login' component={Login} />
<Route path='/confirm-email' component={ConfirmEmail} /> <Route exact path='/logout' component={Logout} />
<Route path='/reset-password' component={ResetPassword} /> <Route exact path='/confirm-email' component={ConfirmEmail} />
<Route path='/forgot-password' component={ForgotPassword} /> <Route exact path='/reset-password' component={ResetPassword} />
<ProtectedRoute path='/profile' component={Profile} /> <Route exact path='/forgot-password' component={ForgotPassword} />
<ProtectedRoute path='/users' render={props => (<Users {...props} onChangeTitle={this.handleChangeTitle} />)} /> <ProtectedRoute exact path='/profile' component={Profile} />
<ProtectedRoute path='/teams' component={Users} /> <ProtectedRoute exact admin path='/users' render={props => (<Users {...props} onChangeTitle={this.handleChangeTitle} />)} />
<ProtectedRoute path='/system' component={Users} /> <ProtectedRoute exact admin path='/teams' component={Users} />
<ProtectedRoute path='/logout' component={Logout} /> <ProtectedRoute exact admin path='/system' component={Users} />
<ProtectedRoute path='/' render={props => (<Home {...props} onChangeTitle={this.handleChangeTitle} />)} /> <ProtectedRoute exact admin path='/home' render={props => (<Home {...props} onChangeTitle={this.handleChangeTitle} />)} />
<DefaultRoute />
</Switch> </Switch>
<Column.Item> <Column.Item>
<Box background={colorInfo.headerButtonBackground} borderTop={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }}> <Box background={colorInfo.headerButtonBackground} borderTop={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }}>

View File

@@ -2,6 +2,7 @@ import React from 'react'
import { api } from 'src/API' import { api } from 'src/API'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { MessageModal, WaitModal } from '../Modal' import { MessageModal, WaitModal } from '../Modal'
import { Logout } from '.'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
export class ConfirmEmail extends React.Component { export class ConfirmEmail extends React.Component {
@@ -17,7 +18,8 @@ export class ConfirmEmail extends React.Component {
} }
componentDidMount(props) { componentDidMount(props) {
let emailToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('email-token') const emailToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('email-token')
this.setState({ waitModal: { message: 'Validating Email...' } }) this.setState({ waitModal: { message: 'Validating Email...' } })
if (emailToken) { if (emailToken) {
api.confirmEmail(emailToken).then((response) => { api.confirmEmail(emailToken).then((response) => {
@@ -29,21 +31,17 @@ export class ConfirmEmail extends React.Component {
this.props.history.replace('/login') this.props.history.replace('/login')
} }
}).catch((err) => { }).catch((err) => {
console.error(err)
const supportEmail = 'support@kingstonsoftware.solutions' // TODO: From configuration
const message = err.message.includes('The token was not found')
? 'This email address may have already been confirmed.'
: `Please contact ${supportEmail} to request a new user invitation`
this.setState({ this.setState({
waitModal: null, waitModal: null,
messageModal: { messageModal: {
title: 'Error Verifying Email...', icon: 'hand',
message: `We couldn't complete that request. ${message}` message: `Please contact ${process.env.REACT_APP_SUPPORT_EMAIL} to request another confirmation email.`,
detail: err.message
} }
}) })
}) })
} else { } else {
this.props.history.replace('/login') this.props.history.replace('/')
} }
} }
@@ -54,14 +52,23 @@ export class ConfirmEmail extends React.Component {
} }
render() { render() {
const { messageModal, waitModal } = this.state
if (api.loggedInUser) {
return <Logout redirect={`${window.location.pathname}${window.location.search}`} />
}
return ( return (
<div> <div>
<WaitModal active={!!this.state.waitModal} <WaitModal
message={this.state.waitModal ? this.state.waitModal.message : ''} /> active={!!waitModal}
message={waitModal ? waitModal.message : ''} />
<MessageModal error open={!!this.state.messageModal} <MessageModal
title={this.state.messageModal ? this.state.messageModal.title : ''} open={!!messageModal}
message={this.state.messageModal ? this.state.messageModal.message : ''} icon={messageModal ? messageModal.icon : ''}
message={messageModal ? messageModal.message : ''}
detail={messageModal ? messageModal.title : ''}
onDismiss={this.handleMessageModalDismiss} /> onDismiss={this.handleMessageModalDismiss} />
</div> </div>
) )

View File

@@ -0,0 +1,47 @@
import React, { Fragment, Component } from 'react'
import { api } from 'src/API'
import { Route, Redirect } from 'react-router-dom'
import { Column } from 'ui'
import autobind from 'autobind-decorator'
export class DefaultRoute extends Component {
@autobind
updateComponent() {
this.forceUpdate()
}
componentDidMount() {
api.addListener('login', this.updateComponent)
}
componentWillUnmount() {
api.removeListener('login', this.updateComponent)
}
render() {
const user = api.loggedInUser
let redirect = null
if (user) {
if (!user.pending) {
redirect = <Redirect to={user.administrator ? '/home' : '/profile'} />
}
} else {
redirect = <Redirect to='/login' />
}
return (
<Route
path='/'
render={() => {
return (
<Fragment>
<Column.Item grow />
{redirect}
</Fragment>
)
}}
/>
)
}
}

View File

@@ -5,6 +5,7 @@ import { Image, Text, Column, Row, BoundInput, BoundButton, Box } from 'ui'
import { MessageModal, WaitModal } from '../Modal' import { MessageModal, WaitModal } from '../Modal'
import { api } from 'src/API' import { api } from 'src/API'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
import { Logout } from '.'
import headerLogo from 'images/deighton.png' import headerLogo from 'images/deighton.png'
import { sizeInfo, colorInfo } from 'ui/style' import { sizeInfo, colorInfo } from 'ui/style'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
@@ -65,6 +66,10 @@ export class ForgotPassword extends Component {
render() { render() {
const { binder, waitModal, messageModal } = this.state const { binder, waitModal, messageModal } = this.state
if (api.loggedInUser) {
return <Logout redirect={`${window.location.pathname}${window.location.search}`} />
}
return ( return (
<Fragment> <Fragment>
<Column.Item grow /> <Column.Item grow />

View File

@@ -1,21 +1,36 @@
import React from 'react' import React, { Component, Fragment } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { api } from 'src/API' import { api } from 'src/API'
import { Column } from 'ui'
export class Logout extends React.Component { export class Logout extends Component {
static propTypes = { static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
redirect: PropTypes.string,
} }
componentDidMount(event) { componentDidMount(event) {
api.logout().then(() => { const { history, redirect } = this.props
if (this.props.history) { const cb = () => {
this.props.history.replace('/login') if (history && redirect) {
try {
history.replace(redirect)
} catch (error) {
history.replace('/login')
} }
}) } else {
window.location.replace('/login')
}
}
api.logout().then(cb, cb)
} }
render() { render() {
return null return (
<Fragment>
<Column.Item grow />
</Fragment>
)
} }
} }

View File

@@ -6,14 +6,7 @@ import autobind from 'autobind-decorator'
export class ProtectedRoute extends React.Component { export class ProtectedRoute extends React.Component {
static propTypes = { static propTypes = {
location: PropTypes.shape({ location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string }),
pathname: PropTypes.string,
search: PropTypes.string,
}),
}
static defaultProps = {
roles: ['administrator']
} }
@autobind @autobind
@@ -30,7 +23,7 @@ export class ProtectedRoute extends React.Component {
} }
render(props) { render(props) {
let user = api.loggedInUser const user = api.loggedInUser
if (user) { if (user) {
if (user.pending) { if (user.pending) {
@@ -40,8 +33,8 @@ export class ProtectedRoute extends React.Component {
} else if (user.administrator) { } else if (user.administrator) {
return <Route {...this.props} /> return <Route {...this.props} />
} }
} } else {
return <Redirect to={`/login?redirect=${this.props.location.pathname}${this.props.location.search}`} /> return <Redirect to={`/login?redirect=${this.props.location.pathname}${this.props.location.search}`} />
} }
}
} }

View File

@@ -1,6 +1,7 @@
import React, { Component, Fragment } from 'react' import React, { Component, Fragment } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Box, Text, Image, Column, Row, BoundInput, BoundButton } from 'ui' import { Box, Text, Image, Column, Row, BoundInput, BoundButton } from 'ui'
import { Logout } from '.'
import { MessageModal, WaitModal } from '../Modal' import { MessageModal, WaitModal } from '../Modal'
import { api } from 'src/API' import { api } from 'src/API'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
@@ -32,7 +33,39 @@ export class ResetPassword extends Component {
this.state = { this.state = {
binder: new FormBinder({}, ResetPassword.bindings), binder: new FormBinder({}, ResetPassword.bindings),
messageModal: null, messageModal: null,
waitModal: null waitModal: null,
tokenConfirmed: false,
}
}
componentDidMount(props) {
if (this.state.tokenConfirmed) {
return
}
const passwordToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('password-token')
this.setState({ waitModal: { message: 'Confirming password reset...' } })
if (passwordToken) {
api.confirmResetPassword(passwordToken).then((response) => {
this.setState({ waitModal: null })
if (response && response.valid) {
this.setState({ tokenConfirmed: true })
} else {
this.props.history.replace('/')
}
}).catch((err) => {
this.setState({
waitModal: null,
messageModal: {
icon: 'hand',
message: `We were unable to confirm you requested a password reset. Please request another reset email.`,
detail: err.message
}
})
})
} else {
this.props.history.replace('/')
} }
} }
@@ -54,8 +87,9 @@ export class ResetPassword extends Component {
waitModal: null, waitModal: null,
messageModal: { messageModal: {
icon: 'hand', icon: 'hand',
title: 'There was a problem changing your password', message: 'There was a problem changing your password. Please request another reset email.',
detail: err.message, detail: err.message,
noRetry: true,
} }
}) })
}) })
@@ -63,12 +97,20 @@ export class ResetPassword extends Component {
@autobind @autobind
handleMessageModalDismiss() { handleMessageModalDismiss() {
if (this.state.messageModal.noRetry) {
this.props.history.replace('/login')
} else {
this.setState({ messageModal: null }) this.setState({ messageModal: null })
} }
}
render() { render() {
const { messageModal, waitModal, binder } = this.state const { messageModal, waitModal, binder } = this.state
if (api.loggedInUser) {
return <Logout redirect={`${window.location.pathname}${window.location.search}`} />
}
return ( return (
<Fragment> <Fragment>
<Column.Item grow /> <Column.Item grow />
@@ -132,8 +174,8 @@ export class ResetPassword extends Component {
<MessageModal <MessageModal
open={!!messageModal} open={!!messageModal}
icon={messageModal ? messageModal.icon : ''} icon={messageModal ? messageModal.icon : ''}
title={messageModal ? messageModal.title : ''}
message={messageModal ? messageModal.message : ''} message={messageModal ? messageModal.message : ''}
detail={messageModal ? messageModal.title : ''}
onDismiss={this.handleMessageModalDismiss} /> onDismiss={this.handleMessageModalDismiss} />
<WaitModal active={!!waitModal} <WaitModal active={!!waitModal}

View File

@@ -3,4 +3,5 @@ export { Logout } from './Logout'
export { ResetPassword } from './ResetPassword' export { ResetPassword } from './ResetPassword'
export { ForgotPassword } from './ForgotPassword' export { ForgotPassword } from './ForgotPassword'
export { ConfirmEmail } from './ConfirmEmail' export { ConfirmEmail } from './ConfirmEmail'
export { DefaultRoute } from './DefaultRoute'
export { ProtectedRoute } from './ProtectedRoute' export { ProtectedRoute } from './ProtectedRoute'

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
import { Modal, Button, Icon, Column, Row, Text, BoundInput, BoundButton } from 'ui' import { Modal, Button, Icon, Column, Row, Text, BoundInput, BoundButton } from 'ui'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
import { sizeInfo } from 'ui/style'
export class ChangePasswordModal extends React.Component { export class ChangePasswordModal extends React.Component {
static propTypes = { static propTypes = {
@@ -66,7 +67,7 @@ export class ChangePasswordModal extends React.Component {
render() { render() {
return ( return (
<Modal dimmer='inverted' open={this.props.open} onClose={this.handleClose} closeOnDimmerClick={false}> <Modal dimmer='inverted' open={this.props.open} width={sizeInfo.modalWidth}>
<form id='passwordForm' onSubmit={this.handleSubmit}> <form id='passwordForm' onSubmit={this.handleSubmit}>
<Column.Item color='black' icon='edit'> <Column.Item color='black' icon='edit'>
<Text size='large'>Change Password</Text> <Text size='large'>Change Password</Text>

View File

@@ -1,10 +1,12 @@
import React from 'react' import React, { Fragment, Component } from 'react'
import { ProfileForm } from './ProfileForm' import { ProfileForm } from './ProfileForm'
import { api } from 'src/API' import { api } from 'src/API'
import { WaitModal, MessageModal, ChangePasswordModal, ChangeEmailModal } from '../Modal' import { WaitModal, MessageModal, ChangePasswordModal, ChangeEmailModal } from '../Modal'
import { Column, Row } from 'ui'
import { sizeInfo } from 'ui/style'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
export class Profile extends React.Component { export class Profile extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
@@ -129,7 +131,12 @@ export class Profile extends React.Component {
render() { render() {
return ( return (
<div> <Fragment>
<Column.Item grow />
<Column.Item>
<Row>
<Row.Item grow />
<Row.Item width={sizeInfo.modalWidth}>
<ProfileForm <ProfileForm
user={this.state.user} user={this.state.user}
onSaved={this.handleSaved} onSaved={this.handleSaved}
@@ -137,7 +144,11 @@ export class Profile extends React.Component {
onChangePassword={this.handleChangePassword} onChangePassword={this.handleChangePassword}
onChangeEmail={this.handleChangeEmail} onChangeEmail={this.handleChangeEmail}
userImageUrl={this.state.userImageUrl} /> userImageUrl={this.state.userImageUrl} />
</Row.Item>
<Row.Item grow />
</Row>
</Column.Item>
<Column.Item>
<MessageModal error open={!!this.state.messageModal} <MessageModal error open={!!this.state.messageModal}
title={this.state.messageModal ? this.state.messageModal.title : ''} title={this.state.messageModal ? this.state.messageModal.title : ''}
message={this.state.messageModal ? this.state.messageModal.message : ''} message={this.state.messageModal ? this.state.messageModal.message : ''}
@@ -148,7 +159,9 @@ export class Profile extends React.Component {
<WaitModal active={!!this.state.waitModal} message={this.state.waitModal ? this.state.waitModal.message : ''} /> <WaitModal active={!!this.state.waitModal} message={this.state.waitModal ? this.state.waitModal.message : ''} />
<ChangePasswordModal open={!!this.state.changePasswordModal} onDismiss={this.handleChangePasswordDismiss} /> <ChangePasswordModal open={!!this.state.changePasswordModal} onDismiss={this.handleChangePasswordDismiss} />
</div> </Column.Item>
<Column.Item grow />
</Fragment>
) )
} }
} }

View File

@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Column, Button, BoundInput, BoundButton } from 'ui' import { Column, Row, Box, Button, BoundInput, BoundButton } from 'ui'
import { sizeInfo, colorInfo } from 'ui/style'
import { regExpPattern } from 'regexp-pattern' import { regExpPattern } from 'regexp-pattern'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
@@ -90,32 +91,39 @@ export class ProfileForm extends React.Component {
} }
render() { render() {
const { binder } = this.state
return ( return (
<form onSubmit={this.handleSubmit} id='profileForm'> <form onSubmit={this.handleSubmit} id='profileForm'>
<Box border={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }} radius={sizeInfo.formBoxRadius}>
<Row>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item>
<Column stackable> <Column stackable>
<Column.Item> <Column.Item>
<BoundInput label='First Name' name='firstName' <BoundInput label='First Name' name='firstName'
binder={this.state.binder} /> binder={binder} />
</Column.Item> </Column.Item>
<Column.Item> <Column.Item>
<BoundInput label='Last Name' name='lastName' <BoundInput label='Last Name' name='lastName'
binder={this.state.binder} /> binder={binder} />
</Column.Item> </Column.Item>
<Column.Item> <Column.Item>
<BoundInput label='Email' name='email' message='Required. Must be a valid email address.' <BoundInput label='Email' name='email' message='Required. Must be a valid email address.'
binder={this.state.binder} /> binder={binder} />
</Column.Item> </Column.Item>
<Column.Item> <Column.Item>
<Button fluid content={'Change Email'} label='&nbsp;' <Button text={'Change Email'} label='&nbsp;'
onClick={this.props.onChangeEmail} /> onClick={this.props.onChangeEmail} />
<Button fluid content={'Change Password'} label='&nbsp;' <Button text={'Change Password'} label='&nbsp;'
onClick={this.props.onChangePassword} /> onClick={this.props.onChangePassword} />
</Column.Item> <BoundButton submit size='medium' text='Save' label='&nbsp;' name='save'
<Column.Item> binder={binder} />
<BoundButton submit primary size='medium' content='Save' label='&nbsp;' name='save'
binder={this.state.binder} />
</Column.Item> </Column.Item>
</Column> </Column>
</Row.Item>
</Row>
</Box>
</form> </form>
) )
} }

View File

@@ -195,7 +195,6 @@ export class Users extends Component {
@autobind @autobind
handleRemoveModalDismiss(yes) { handleRemoveModalDismiss(yes) {
if (yes) { if (yes) {
// TODO: Pass the _id back from the dialog input data
const selectedUserId = this.state.selectedUser._id const selectedUserId = this.state.selectedUser._id
const selectedIndex = this.state.users.findIndex((user) => (user._id === selectedUserId)) const selectedIndex = this.state.users.findIndex((user) => (user._id === selectedUserId))

View File

@@ -1,14 +1,17 @@
import Radium from 'radium'
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { sizeInfo } from './style' import { sizeInfo } from './style'
// See https://www.flaticon.com/packs/free-basic-ui-elements // See https://www.flaticon.com/packs/free-basic-ui-elements
@Radium
export class Icon extends Component { export class Icon extends Component {
static propTypes = { static propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
size: PropTypes.number, size: PropTypes.number,
margin: PropTypes.number, margin: PropTypes.number,
style: PropTypes.object,
} }
static defaultProps = { static defaultProps = {
@@ -33,11 +36,11 @@ export class Icon extends Component {
} }
render() { render() {
let { size, name, margin } = this.props let { size, name, margin, style } = this.props
let source = Icon.svgs[name] || Icon.svgs['placeholder'] let source = Icon.svgs[name] || Icon.svgs['placeholder']
size -= margin * 2 size -= margin * 2
return <img style={{ width: size, height: size, margin }} src={source} /> return <img style={[{ width: size, height: size, margin }, style]} src={source} />
} }
} }

View File

@@ -43,7 +43,10 @@ export class PanelButton extends Component {
]} ]}
onClick={onClick}> onClick={onClick}>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Icon name={icon} size={sizeInfo.panelButtonIcon} margin={sizeInfo.panelButtonIconMargin} /> <Icon
name={icon} size={sizeInfo.panelButtonIcon}
margin={sizeInfo.panelButtonIconMargin}
style={{ position: 'relative', top: sizeInfo.panelButtonIconOffset }} />
<span style={{ <span style={{
position: 'absolute', position: 'absolute',
top: sizeInfo.panelButtonTextOffset, top: sizeInfo.panelButtonTextOffset,

View File

@@ -61,9 +61,10 @@ const sizeInfo = {
panelButton: 200, panelButton: 200,
panelButtonIcon: 170, panelButtonIcon: 170,
panelButtonIconMargin: 0, panelButtonIconMargin: 0,
panelButtonIconOffset: -10,
panelButtonBorderRadius: 25, panelButtonBorderRadius: 25,
panelButtonBorderWidth: 2, panelButtonBorderWidth: 2,
panelButtonTextOffset: 125, panelButtonTextOffset: 120,
panelButtonSpacing: 30, panelButtonSpacing: 30,
formBoxRadius: 5, formBoxRadius: 5,