diff --git a/.eslintrc.js b/.eslintrc.js index 61c66f6..ac090cc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,7 +8,7 @@ module.exports = { semi: ["error", "never"], "arrow-parens": ["error", "as-needed"], "react/jsx-filename-extension": ["warn", { "extensions": [".js"] }], - "react/prop-types": "warn", + "react/prop-types": "error", "jsx-a11y/label-has-for": ["error", { "components": ["Label"], "required": { diff --git a/package.json b/package.json index d4acb7c..cbaa096 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "homepage": "https://tatomyr.github.io/estimate-it/", "scripts": { "start": "react-scripts start", - "build": "react-scripts build && npm run analyze", + "build": "react-scripts build", "lint": "eslint src/", "test": "react-scripts test --env=jsdom --coverage", - "precommit": "npm run lint && npm run test", - "predeploy": "npm run build", + "precommit": "git status && npm run lint && npm run test", + "predeploy": "npm run build && npm run analyze", "deploy": "gh-pages -d build", "analyze": "source-map-explorer build/static/js/main.*", "eject": "react-scripts eject" diff --git a/src/components/App.js b/src/components/App.js new file mode 100644 index 0000000..842882f --- /dev/null +++ b/src/components/App.js @@ -0,0 +1,32 @@ +import React from 'react' +import { Switch } from 'react-router-dom' +import Route from './ProtectedRoute' +import Home from './Home' +import Private from './Private' +import Redirector from './Redirector' +import Estimate from './Estimate' +import Dashboard from './Dashboard' +import AuthScreen from './AuthScreen' +import Spinner from './Spinner' +import Toastr from './Toastr' + +const App = () => ( +
+ + + + + + + + + + + + + + +
+) + +export default App diff --git a/src/components/App/App.js b/src/components/App/App.js deleted file mode 100644 index f7e4893..0000000 --- a/src/components/App/App.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Switch, Route } from 'react-router-dom' -import Toastr from '../Toastr' -import Redirector from '../Redirector' -import Spinner from '../Spinner' -import Private from '../Private' -import Estimate from '../Estimate' -import Dashboard from '../Dashboard' -import AuthScreen from '../AuthScreen' - -class App extends React.Component { - componentDidMount = () => { - const { checkCreds, username } = this.props - if (!username) checkCreds() - } - - render = () => ( -
- - - - - - - - - {/* eslint-disable-next-line react/destructuring-assignment */} - {this.props.showAuthScreen && } - - -
- ) -} - -App.propTypes = { - checkCreds: PropTypes.func.isRequired, - showAuthScreen: PropTypes.bool.isRequired, - username: PropTypes.string.isRequired, -} - -export default App diff --git a/src/components/App/index.js b/src/components/App/index.js deleted file mode 100644 index 03a0fc5..0000000 --- a/src/components/App/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux' -import App from './App' -import './styles.css' -import { - checkCreds, -} from '../../redux/actions/async' - -const mapStateToProps = ({ - visualEffects: { showAuthScreen }, - creds: { username }, -}) => ({ - showAuthScreen, - username, -}) - -const mapDispatchToProps = ({ - checkCreds, -}) - -export default connect(mapStateToProps, mapDispatchToProps)(App) diff --git a/src/components/App/styles.css b/src/components/App/styles.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/AuthScreen/Anonymous.js b/src/components/AuthScreen/Anonymous.js index a4befa9..d6dc3f0 100644 --- a/src/components/AuthScreen/Anonymous.js +++ b/src/components/AuthScreen/Anonymous.js @@ -1,4 +1,5 @@ import React, { Fragment } from 'react' +import { Link } from 'react-router-dom' import PropTypes from 'prop-types' import { Button, @@ -9,18 +10,30 @@ import { } from 'reactstrap' import FA from 'react-fontawesome' import * as api from '../../helpers/api' +import { locationType } from '../../helpers/propTypes' + +const AlertAccessingEstimate = ({ from }) => ((from + && from.pathname + && from.pathname.startsWith('/estimate/') + && from.pathname !== '/estimate/new' +) + ? ( + + Please enter a valid credentials to get access to this estimate + + ) + : null) + +AlertAccessingEstimate.propTypes = { + from: locationType.isRequired, +} const Anonymous = ({ - match: { url, params }, checkCreds, - openGuestSession, + from, }) => ( - {(url.startsWith('/estimate/') && params.estimateId !== 'new') ? ( - - Please enter a valid credentials to get access to this estimate - - ) : null} +
{ e.preventDefault() @@ -67,25 +80,17 @@ const Anonymous = ({
Or
- + + +
) Anonymous.propTypes = { - match: PropTypes.shape({ - url: PropTypes.string.isRequired, - params: PropTypes.shape({ - estimateId: PropTypes.string, - }).isRequired, - }).isRequired, checkCreds: PropTypes.func.isRequired, - openGuestSession: PropTypes.func.isRequired, + from: locationType.isRequired, } export default Anonymous diff --git a/src/components/AuthScreen/AuthScreen.js b/src/components/AuthScreen/AuthScreen.js index 63e6a8b..cf4ef46 100644 --- a/src/components/AuthScreen/AuthScreen.js +++ b/src/components/AuthScreen/AuthScreen.js @@ -1,27 +1,37 @@ import React from 'react' -import PropTypes from 'prop-types' import Header from '../Header' import Authorized from './Authorized' import Anonymous from './Anonymous' +import CredsCheckingScreen from './CredsCheckingScreen' +import { locationType, credsType } from '../../helpers/propTypes' const AuthScreen = ({ - username, - checkingCreds, + creds: { + haveBeenChecked, + username, + }, + location: { state }, ...rest }) => (
- {(checkingCreds && 'Checking permissions. Please wait…') - || (username && ) - || } + {(!haveBeenChecked && ) + || (username && ( + )) + || }
) AuthScreen.propTypes = { - username: PropTypes.string.isRequired, - checkingCreds: PropTypes.bool.isRequired, + creds: credsType.isRequired, + location: locationType.isRequired, } export default AuthScreen diff --git a/src/components/AuthScreen/AuthScreen.test.js b/src/components/AuthScreen/AuthScreen.test.js index edb4628..c13e539 100644 --- a/src/components/AuthScreen/AuthScreen.test.js +++ b/src/components/AuthScreen/AuthScreen.test.js @@ -1,51 +1,74 @@ /* globals describe, it, expect */ import React from 'react' +import { HashRouter as Router } from 'react-router-dom' import renderer from 'react-test-renderer' import AuthScreen from './AuthScreen' -const mockedFunctions = { +const commons = { + location: { pathname: '/', state: { from: '/' } }, + match: { url: '/estimate/new', params: { estimateId: 'new' } }, checkCreds: () => null, resetCreds: () => null, - cleanEstimate: () => null, - closeAuthScreen: () => null, - openGuestSession: () => null, + cleanAllEstimates: () => null, redirect: () => null, + history: { goBack: () => null }, } describe('AuthScreen', () => { it('renders correctly for authorized user', () => { const tree = renderer - .create() + .create( + + + // eslint-disable-line comma-dangle + ) .toJSON() expect(tree).toMatchSnapshot() }) it('renders correctly for anonymous user', () => { const tree = renderer - .create() + .create( + + + // eslint-disable-line comma-dangle + ) .toJSON() expect(tree).toMatchSnapshot() }) it('renders correctly when checking credentials', () => { const tree = renderer - .create() + .create( + + + // eslint-disable-line comma-dangle + ) .toJSON() expect(tree).toMatchSnapshot() }) diff --git a/src/components/AuthScreen/Authorized.js b/src/components/AuthScreen/Authorized.js index 2b07fe0..ddfaed3 100644 --- a/src/components/AuthScreen/Authorized.js +++ b/src/components/AuthScreen/Authorized.js @@ -1,17 +1,20 @@ import React, { Fragment } from 'react' +import { Redirect } from 'react-router-dom' import PropTypes from 'prop-types' import { Button } from 'reactstrap' import * as api from '../../helpers/api' +import { locationType } from '../../helpers/propTypes' const Authorized = ({ - match: { url, params }, username, resetCreds, - cleanEstimate, - closeAuthScreen, - redirect, + cleanAllEstimates, + from, + redirectToReferrer, + history, }) => ( + {redirectToReferrer && }
{/* eslint-disable-next-line react/jsx-one-expression-per-line */} Welcome, {username}! @@ -19,7 +22,7 @@ const Authorized = ({ @@ -29,7 +32,7 @@ const Authorized = ({ onClick={() => { api.removeCreds() resetCreds() - cleanEstimate(params) + cleanAllEstimates() }} > Log Out @@ -38,17 +41,16 @@ const Authorized = ({ ) Authorized.propTypes = { - match: PropTypes.shape({ - url: PropTypes.string.isRequired, - params: PropTypes.shape({ - estimateId: PropTypes.string, - }).isRequired, - }).isRequired, username: PropTypes.string.isRequired, resetCreds: PropTypes.func.isRequired, - cleanEstimate: PropTypes.func.isRequired, - closeAuthScreen: PropTypes.func.isRequired, - redirect: PropTypes.func.isRequired, + cleanAllEstimates: PropTypes.func.isRequired, + from: locationType.isRequired, + redirectToReferrer: PropTypes.bool, + history: PropTypes.objectOf(PropTypes.any).isRequired, +} + +Authorized.defaultProps = { + redirectToReferrer: false, } export default Authorized diff --git a/src/components/AuthScreen/CredsCheckingScreen.js b/src/components/AuthScreen/CredsCheckingScreen.js new file mode 100644 index 0000000..a54408d --- /dev/null +++ b/src/components/AuthScreen/CredsCheckingScreen.js @@ -0,0 +1 @@ +export default () => 'Checking permissions. Please wait…' diff --git a/src/components/AuthScreen/__snapshots__/AuthScreen.test.js.snap b/src/components/AuthScreen/__snapshots__/AuthScreen.test.js.snap index cf41e44..c4564d9 100644 --- a/src/components/AuthScreen/__snapshots__/AuthScreen.test.js.snap +++ b/src/components/AuthScreen/__snapshots__/AuthScreen.test.js.snap @@ -102,13 +102,18 @@ exports[`AuthScreen renders correctly for anonymous user 1`] = ` Or
- + +
`; diff --git a/src/components/AuthScreen/index.js b/src/components/AuthScreen/index.js index 5e77f70..7be10e6 100644 --- a/src/components/AuthScreen/index.js +++ b/src/components/AuthScreen/index.js @@ -3,33 +3,19 @@ import { withRouter } from 'react-router-dom' import AuthScreen from './AuthScreen' import './styles.css' import { - closeAuthScreen, - cleanEstimate, + cleanAllEstimates, resetCreds, } from '../../redux/actions' import { checkCreds, - openGuestSession, - redirect, } from '../../redux/actions/async' -const mapStateToProps = ({ - creds: { - username, - checkingCreds, - }, -}) => ({ - username, - checkingCreds, -}) +const mapStateToProps = ({ creds }) => ({ creds }) const mapDispatchToProps = ({ checkCreds, - closeAuthScreen, - cleanEstimate, - openGuestSession, resetCreds, - redirect, + cleanAllEstimates, }) export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AuthScreen)) diff --git a/src/components/Dashboard/Dashboard.js b/src/components/Dashboard/Dashboard.js index bae542c..1df6456 100644 --- a/src/components/Dashboard/Dashboard.js +++ b/src/components/Dashboard/Dashboard.js @@ -8,7 +8,7 @@ import { } from 'reactstrap' import Header from '../Header' import Project from './Project' -import { estimateType } from '../Estimate/propTypes' +import { estimateType } from '../../helpers/propTypes' const Dashboard = ({ estimates }) => (
diff --git a/src/components/Dashboard/Project.js b/src/components/Dashboard/Project.js index 4ebf333..36039b2 100644 --- a/src/components/Dashboard/Project.js +++ b/src/components/Dashboard/Project.js @@ -1,4 +1,4 @@ -// TODO: implement delete project +// TODO: implement deleting project import React from 'react' import { Link } from 'react-router-dom' @@ -11,7 +11,7 @@ import { Button, } from 'reactstrap' import FA from 'react-fontawesome' -import { estimateType } from '../Estimate/propTypes' +import { estimateType } from '../../helpers/propTypes' const Project = ({ estimate: { diff --git a/src/components/Estimate/Editor.js b/src/components/Estimate/Editor.js index 5210e17..31079e9 100644 --- a/src/components/Estimate/Editor.js +++ b/src/components/Estimate/Editor.js @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import MonacoEditor from 'react-monaco-editor' import { options, languageDef, configuration } from './editor-config' -import { estimateType } from './propTypes' +import { estimateType } from '../../helpers/propTypes' const editorWillMount = monaco => { this.editor = monaco diff --git a/src/components/Estimate/Estimate.js b/src/components/Estimate/Estimate.js index b7c8ded..fb91fa1 100644 --- a/src/components/Estimate/Estimate.js +++ b/src/components/Estimate/Estimate.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import Editor from './Editor' import Graph from './Graph' import Sidebar from '../Sidebar' -import { estimateType } from './propTypes' +import { matchType, estimateType } from '../../helpers/propTypes' class Estimate extends React.Component { componentDidMount = () => { @@ -18,6 +18,7 @@ class Estimate extends React.Component { .values(estimates) .some(({ saved }) => !saved) // Setting up hook to prevent of accidental window closing/refreshing + // FIXME: after clearing all estimates this doesn't change window.onbeforeunload = thereIsUnsavedEstimate ? () => true : null @@ -26,10 +27,10 @@ class Estimate extends React.Component { fetchHelper = () => { const { getEstimate, - match: { params: { estimateId } }, + match: { params: { estimateId = 'new' } }, estimates, } = this.props - // Fetching an appropriate estimate on route change. + // Fetching an appropriate estimate if such doesn't exist const estimate = estimates[estimateId] const estimateIsntLoaded = !estimate || estimate.text === undefined if (estimateIsntLoaded) { @@ -39,7 +40,7 @@ class Estimate extends React.Component { render = () => { const { - match: { params: { estimateId } }, + match: { params: { estimateId = 'new' } }, estimates, updateEstimate, graphView, @@ -68,11 +69,7 @@ class Estimate extends React.Component { } Estimate.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape({ - estimateId: PropTypes.string, - }).isRequired, - }).isRequired, + match: matchType.isRequired, estimates: PropTypes.objectOf(estimateType).isRequired, getEstimate: PropTypes.func.isRequired, updateEstimate: PropTypes.func.isRequired, diff --git a/src/components/Estimate/propTypes.js b/src/components/Estimate/propTypes.js deleted file mode 100644 index 536b05b..0000000 --- a/src/components/Estimate/propTypes.js +++ /dev/null @@ -1,13 +0,0 @@ -import PropTypes from 'prop-types' - -export const estimateType = PropTypes.shape({ - _id: PropTypes.string.isRequired, - _changed: PropTypes.string, - project: PropTypes.string, - participants: PropTypes.arrayOf(PropTypes.string), - modifiedBy: PropTypes.string, - text: PropTypes.string, - graphData: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)), - calculated: PropTypes.bool, - saved: PropTypes.bool.isRequired, -}) diff --git a/src/components/Home/Home.js b/src/components/Home/Home.js index d9e9f81..4e0a021 100644 --- a/src/components/Home/Home.js +++ b/src/components/Home/Home.js @@ -2,44 +2,36 @@ import React from 'react' import { Link } from 'react-router-dom' import { Container, - Row, - Col, Button, } from 'reactstrap' import Header from '../Header' import example from '../../helpers/example' const Home = () => ( -
+

Get started!

- - - - - - - - - - - - - - - - - +
+ + + + + + + + + +
diff --git a/src/components/Home/styles.css b/src/components/Home/styles.css index 66bd484..5982d84 100644 --- a/src/components/Home/styles.css +++ b/src/components/Home/styles.css @@ -1,4 +1,8 @@ -article pre { +.home .options button { + margin: 10px; +} + +.home article pre { max-width: 300px; margin: 0 auto; padding: 10px 30px; @@ -6,3 +10,4 @@ article pre { color: wheat; border-radius: 3px; } + diff --git a/src/components/Private.js b/src/components/Private.js deleted file mode 100644 index 5646e24..0000000 --- a/src/components/Private.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, { Fragment } from 'react' -import PropTypes from 'prop-types' -import { withRouter } from 'react-router-dom' -import { connect } from 'react-redux' -import AuthScreen from './AuthScreen' - -const Private = ({ - children, - username, - match: { params: { estimateId = '' } }, -}) => ( - - {username || estimateId === 'new' - ? children - : } - -) - -Private.propTypes = { - children: PropTypes.node.isRequired, - username: PropTypes.string.isRequired, - match: PropTypes.shape({ - params: PropTypes.shape({ - estimateId: PropTypes.string, - }).isRequired, - }).isRequired, -} - -const mapStateToProps = ({ creds: { username } }) => ({ - username, -}) - -export default withRouter(connect(mapStateToProps, null)(Private)) diff --git a/src/components/Private/Private.js b/src/components/Private/Private.js new file mode 100644 index 0000000..ace51d4 --- /dev/null +++ b/src/components/Private/Private.js @@ -0,0 +1,30 @@ +import React, { Component, Fragment } from 'react' +import PropTypes from 'prop-types' +import { credsType } from '../../helpers/propTypes' + +class Private extends Component { + componentDidMount = () => { + const { + creds, + checkCreds, + } = this.props + if (!creds.haveBeenChecked) { + checkCreds() + } + } + + render = () => ( + + {/* eslint-disable-next-line react/destructuring-assignment */} + {this.props.children} + + ) +} + +Private.propTypes = { + creds: credsType.isRequired, + checkCreds: PropTypes.func.isRequired, + children: PropTypes.node.isRequired, +} + +export default Private diff --git a/src/components/Private/index.js b/src/components/Private/index.js new file mode 100644 index 0000000..5492122 --- /dev/null +++ b/src/components/Private/index.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux' +import Private from './Private' +import { checkCreds } from '../../redux/actions/async' + +const mapStateToProps = ({ creds }) => ({ creds }) + +const mapDispatchToProps = ({ checkCreds }) + +export default connect(mapStateToProps, mapDispatchToProps)(Private) diff --git a/src/components/ProtectedRoute/ProtectedRoute.js b/src/components/ProtectedRoute/ProtectedRoute.js new file mode 100644 index 0000000..44ffe4e --- /dev/null +++ b/src/components/ProtectedRoute/ProtectedRoute.js @@ -0,0 +1,39 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Route, Redirect } from 'react-router-dom' +import { locationType } from '../../helpers/propTypes' + +const ProtectedRoute = ({ + component: Component, + isProtected, + username, + ...rest +}) => ( + (isProtected && !username + ? ( + + ) + : + )} + /> +) + +ProtectedRoute.propTypes = { + component: PropTypes.func.isRequired, + isProtected: PropTypes.bool, + username: PropTypes.string.isRequired, + location: locationType.isRequired, +} + +ProtectedRoute.defaultProps = { + isProtected: false, +} + +export default ProtectedRoute diff --git a/src/components/ProtectedRoute/index.js b/src/components/ProtectedRoute/index.js new file mode 100644 index 0000000..c84af54 --- /dev/null +++ b/src/components/ProtectedRoute/index.js @@ -0,0 +1,9 @@ +import { withRouter } from 'react-router-dom' +import { connect } from 'react-redux' +import ProtectedRoute from './ProtectedRoute' + +const mapStateToProps = ({ creds: { username } }) => ({ + username, +}) + +export default withRouter(connect(mapStateToProps, null)(ProtectedRoute)) diff --git a/src/components/Redirector/Redirector.js b/src/components/Redirector/Redirector.js index 1fc8acd..8409c4c 100644 --- a/src/components/Redirector/Redirector.js +++ b/src/components/Redirector/Redirector.js @@ -1,14 +1,15 @@ import React from 'react' import PropTypes from 'prop-types' import { Redirect } from 'react-router-dom' +import { locationType } from '../../helpers/propTypes' -const Redirector = ({ redirector: { href } }) => href && ( - +const Redirector = ({ redirector: { location } }) => location && ( + ) Redirector.propTypes = { redirector: PropTypes.shape({ - href: PropTypes.string.isRequired, + location: locationType, }).isRequired, } diff --git a/src/components/Redirector/Redirector.test.js b/src/components/Redirector/Redirector.test.js index d14f0a1..9489eda 100644 --- a/src/components/Redirector/Redirector.test.js +++ b/src/components/Redirector/Redirector.test.js @@ -9,7 +9,7 @@ describe('Redirector', () => { it('renders correctly', () => { const tree = renderer .create() .toJSON() expect(tree).toMatchSnapshot() @@ -20,7 +20,7 @@ describe('Redirector', () => { .create( /* eslint-disable-line comma-dangle */ ) diff --git a/src/components/Root.js b/src/components/Root.js index cc6cc95..7847406 100644 --- a/src/components/Root.js +++ b/src/components/Root.js @@ -1,19 +1,13 @@ import React from 'react' import PropTypes from 'prop-types' import { Provider } from 'react-redux' -import { HashRouter as Router, Route, Switch } from 'react-router-dom' +import { HashRouter as Router } from 'react-router-dom' import App from './App' -import Home from './Home' const Root = ({ store }) => ( - - - - - - + ) diff --git a/src/components/SideButton/SideButton.js b/src/components/SideButton/SideButton.js index f170ab7..1e45670 100644 --- a/src/components/SideButton/SideButton.js +++ b/src/components/SideButton/SideButton.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import FA from 'react-fontawesome' import { Button } from 'reactstrap' +import { locationType } from '../../helpers/propTypes' const SideButton = ({ title, @@ -33,7 +34,7 @@ SideButton.propTypes = { name: PropTypes.string.isRequired, color: PropTypes.string, onClick: PropTypes.func, - link: PropTypes.string, + link: locationType, disabled: PropTypes.bool, redirect: PropTypes.func.isRequired, } diff --git a/src/components/Sidebar/Sidebar.js b/src/components/Sidebar/Sidebar.js index 3fcd38d..4c46e6e 100644 --- a/src/components/Sidebar/Sidebar.js +++ b/src/components/Sidebar/Sidebar.js @@ -1,13 +1,12 @@ import React from 'react' import PropTypes from 'prop-types' import SideButton from '../SideButton' -import { estimateType } from '../Estimate/propTypes' +import { matchType, estimateType } from '../../helpers/propTypes' const Sidebar = ({ - match: { params: { estimateId } }, + match: { params: { estimateId = 'new' } }, recalc, saveEstimate, - openAuthScreen, estimates, username, graphView, @@ -23,11 +22,6 @@ const Sidebar = ({ disabled={estimateId === 'new'} link="/estimate/new" /> - )} +
-
{ configure({ adapter: new Adapter() }) const tree = shallow( null} saveEstimate={() => null} - openAuthScreen={() => null} estimates={{ new: emptyEstimate }} username="Test User" graphView="minified" diff --git a/src/components/Sidebar/__snapshots__/Sidebar.test.js.snap b/src/components/Sidebar/__snapshots__/Sidebar.test.js.snap index 780a0ae..88221c7 100644 --- a/src/components/Sidebar/__snapshots__/Sidebar.test.js.snap +++ b/src/components/Sidebar/__snapshots__/Sidebar.test.js.snap @@ -19,6 +19,11 @@ ShallowWrapper { } } graphView="minified" + location={ + Object { + "pathname": "/", + } + } match={ Object { "params": Object { @@ -27,7 +32,6 @@ ShallowWrapper { } } minifyGraph={[Function]} - openAuthScreen={[Function]} recalc={[Function]} saveEstimate={[Function]} username="Test User" @@ -50,11 +54,6 @@ ShallowWrapper { link="/estimate/new" name="file" title="New" -/>, - , , +
, , -
, , , - , , +
, , -
, , dispatch => { +export const redirect = location => dispatch => { dispatch({ type: '__ASYNC__REDIRECT' }) - dispatch(setHref(href)) - setTimeout(() => dispatch(resetHref())) + dispatch(setLocation(location)) + setTimeout(() => dispatch(resetLocation())) } export const saveEstimate = ({ estimateId }) => (dispatch, getState) => { @@ -69,6 +68,9 @@ export const saveEstimate = ({ estimateId }) => (dispatch, getState) => { .then(savedEstimate => { dispatch(updateEstimate(savedEstimate)) dispatch(markEstimateSaved(savedEstimate._id)) + if (estimateId === 'new') { + dispatch(cleanEstimate('new')) + } dispatch(redirect(`/estimate/${savedEstimate._id}`)) toastr.success(...saved(savedEstimate)) }) @@ -107,7 +109,6 @@ export const getEstimate = ({ estimateId }) => dispatch => { export const checkCreds = () => dispatch => { dispatch({ type: '__ASYNC__CHECK_CREDS' }) - dispatch(setCredsChecking()) dispatch(addSpinner()) return api.fetchTitles() .then(titles => { @@ -115,14 +116,7 @@ export const checkCreds = () => dispatch => { console.info(`${titles.length} record(s) found in the DB.`) dispatch(setTitles(titles)) dispatch(setCreds()) - dispatch(closeAuthScreen()) }) .catch(() => { dispatch(resetCreds()) }) .finally(() => { dispatch(delSpinner()) }) } - -export const openGuestSession = () => dispatch => { - dispatch({ type: '__ASYNC__OPEN_GUEST_SESSION' }) - dispatch(closeAuthScreen()) - dispatch(redirect('/estimate/new')) -} diff --git a/src/redux/actions/index.js b/src/redux/actions/index.js index 80e2e1e..fc74595 100644 --- a/src/redux/actions/index.js +++ b/src/redux/actions/index.js @@ -1,20 +1,18 @@ import { UPDATE_ESTIMATE, CLEAN_ESTIMATE, + CLEAN_ALL_ESTIMATES, RECALCULATE, - SET_HREF, - RESET_HREF, + SET_LOCATION, + RESET_LOCATION, ADD_SPINNER, DEL_SPINNER, SET_CREDS, RESET_CREDS, - OPEN_AUTH_SCREEN, - CLOSE_AUTH_SCREEN, SET_TITLES, MARK_ESTIMATE_SAVED, ENLARGE_GRAPH, MINIFY_GRAPH, - SET_CREDS_CHECKING, } from './types' import * as api from '../../helpers/api' @@ -23,23 +21,27 @@ export const updateEstimate = estimate => ({ payload: estimate, }) -export const cleanEstimate = ({ estimateId }) => ({ +export const cleanEstimate = _id => ({ type: CLEAN_ESTIMATE, - payload: estimateId, + payload: { _id }, +}) + +export const cleanAllEstimates = () => ({ + type: CLEAN_ALL_ESTIMATES, }) export const recalc = _id => ({ type: RECALCULATE, - payload: _id, + payload: { _id }, }) -export const setHref = href => ({ - type: SET_HREF, - payload: href, +export const setLocation = location => ({ + type: SET_LOCATION, + payload: location, }) -export const resetHref = () => ({ - type: RESET_HREF, +export const resetLocation = () => ({ + type: RESET_LOCATION, }) export const addSpinner = () => ({ @@ -50,18 +52,6 @@ export const delSpinner = () => ({ type: DEL_SPINNER, }) -export const openAuthScreen = () => ({ - type: OPEN_AUTH_SCREEN, -}) - -export const closeAuthScreen = () => ({ - type: CLOSE_AUTH_SCREEN, -}) - -export const setCredsChecking = () => ({ - type: SET_CREDS_CHECKING, -}) - export const setCreds = () => ({ type: SET_CREDS, payload: { ...api.getCreds() }, @@ -71,14 +61,14 @@ export const resetCreds = () => ({ type: RESET_CREDS, }) -export const setTitles = payload => ({ +export const setTitles = projects => ({ type: SET_TITLES, - payload, + payload: { projects }, }) -export const markEstimateSaved = payload => ({ +export const markEstimateSaved = _id => ({ type: MARK_ESTIMATE_SAVED, - payload, + payload: { _id }, }) export const enlargeGraph = () => ({ diff --git a/src/redux/actions/types.js b/src/redux/actions/types.js index f8aa38f..4a1c1fd 100644 --- a/src/redux/actions/types.js +++ b/src/redux/actions/types.js @@ -1,15 +1,13 @@ export const UPDATE_ESTIMATE = 'UPDATE_ESTIMATE' export const CLEAN_ESTIMATE = 'CLEAN_ESTIMATE' +export const CLEAN_ALL_ESTIMATES = 'CLEAN_ALL_ESTIMATES' export const RECALCULATE = 'RECALCULATE' export const ADD_SPINNER = 'ADD_SPINNER' export const DEL_SPINNER = 'DEL_SPINNER' -export const SET_HREF = 'SET_HREF' -export const RESET_HREF = 'RESET_HREF' -export const SET_CREDS_CHECKING = 'SET_CREDS_CHECKING' +export const SET_LOCATION = 'SET_LOCATION' +export const RESET_LOCATION = 'RESET_LOCATION' export const SET_CREDS = 'SET_CREDS' export const RESET_CREDS = 'RESET_CREDS' -export const OPEN_AUTH_SCREEN = 'OPEN_AUTH_SCREEN' -export const CLOSE_AUTH_SCREEN = 'CLOSE_AUTH_SCREEN' export const SET_TITLES = 'SET_TITLES' export const MARK_ESTIMATE_SAVED = 'MARK_ESTIMATE_SAVED' export const ENLARGE_GRAPH = 'ENLARGE_GRAPH' diff --git a/src/redux/reducers/creds.js b/src/redux/reducers/creds.js index 228f885..8bd3311 100644 --- a/src/redux/reducers/creds.js +++ b/src/redux/reducers/creds.js @@ -1,29 +1,25 @@ -// TODO: Implement login route: .../login/:dbName/:apiKey -import { SET_CREDS_CHECKING, SET_CREDS, RESET_CREDS } from '../actions/types' +import { SET_CREDS, RESET_CREDS } from '../actions/types' export const emptyCreds = ({ + haveBeenChecked: false, + username: '', dbName: '', apiKey: '', - username: '', - checkingCreds: false, }) export default (state = emptyCreds, { type, payload }) => { switch (type) { - case SET_CREDS_CHECKING: - return ({ - ...state, - checkingCreds: true, - }) - case SET_CREDS: return ({ ...payload, - checkingCreds: false, + haveBeenChecked: true, }) case RESET_CREDS: - return emptyCreds + return ({ + ...emptyCreds, + haveBeenChecked: true, + }) default: return state diff --git a/src/redux/reducers/creds.test.js b/src/redux/reducers/creds.test.js index e07276c..41d7ae2 100644 --- a/src/redux/reducers/creds.test.js +++ b/src/redux/reducers/creds.test.js @@ -1,24 +1,11 @@ /* globals describe, it, expect */ -import { SET_CREDS_CHECKING, SET_CREDS, RESET_CREDS } from '../actions/types' +import { SET_CREDS, RESET_CREDS } from '../actions/types' import creds, { emptyCreds } from './creds' describe('authentication', () => { it('returns initial state', () => { - expect(creds(undefined, {})).toEqual({ ...emptyCreds }) - }) - - it('sets checking creds status correctly', () => { - // Given - const beforeState = emptyCreds - const action = { type: SET_CREDS_CHECKING } - // When - const afterState = creds(beforeState, action) - // Then - expect(afterState).toEqual({ - ...emptyCreds, - checkingCreds: true, - }) + expect(creds(undefined, {})).toEqual(emptyCreds) }) it('sets creds correctly', () => { @@ -27,7 +14,7 @@ describe('authentication', () => { const action = { type: SET_CREDS, payload: { - user: 'Test User', + username: 'Test User', dbName: 'dbName', apiKey: 'apiKey', }, @@ -36,25 +23,25 @@ describe('authentication', () => { const afterState = creds(beforeState, action) // Then expect(afterState).toEqual({ - user: 'Test User', + haveBeenChecked: true, + username: 'Test User', dbName: 'dbName', apiKey: 'apiKey', - checkingCreds: false, }) }) it('resets creds correctly', () => { // Given const beforeState = { - user: 'Test User', + haveBeenChecked: false, + username: 'Test User', dbName: 'dbName', apiKey: 'apiKey', - checkingCreds: true, } const action = { type: RESET_CREDS } // When const afterState = creds(beforeState, action) // Then - expect(afterState).toEqual(emptyCreds) + expect(afterState).toEqual({ ...emptyCreds, haveBeenChecked: true }) }) }) diff --git a/src/redux/reducers/estimates.js b/src/redux/reducers/estimates.js index 4786fc7..04ab003 100644 --- a/src/redux/reducers/estimates.js +++ b/src/redux/reducers/estimates.js @@ -3,6 +3,7 @@ import { UPDATE_ESTIMATE, RECALCULATE, CLEAN_ESTIMATE, + CLEAN_ALL_ESTIMATES, SET_TITLES, MARK_ESTIMATE_SAVED, } from '../actions/types' @@ -43,30 +44,46 @@ export default (state = ({ new: emptyEstimate }), { type, payload }) => { } case CLEAN_ESTIMATE: + { + // Putting 'new' here to avoid creating an estimate with an undefined `_id` + // ...while logging out on different routes + // ...(that may not contain an `:estimateId` param) + const { _id = 'new' } = payload return ({ ...state, - // Put 'new' here to avoid creating an estimate with an undefined `_id` - // ...while logging out on different routes - // ...(that may not contain an `:estimateId` param) - [payload || 'new']: emptyEstimate, + [_id]: { + ...emptyEstimate, + _id, + }, + }) + } + + case CLEAN_ALL_ESTIMATES: + return ({ + new: emptyEstimate, }) case RECALCULATE: + { + const { _id } = payload return ({ ...state, - [payload]: { - ...state[payload], - payload, - ...handleText(state[payload].text), + [_id]: { + ...state[_id], + _id, + ...handleText(state[_id].text), calculated: true, saved: false, }, }) + } case SET_TITLES: + { + const { projects } = payload return ({ ...state, - ...payload.reduce(($, project) => ({ + ...projects.reduce(($, project) => ({ ...$, [project._id]: { ...project, @@ -74,15 +91,19 @@ export default (state = ({ new: emptyEstimate }), { type, payload }) => { }, }), {}), }) + } case MARK_ESTIMATE_SAVED: + { + const { _id } = payload return ({ ...state, - [payload]: { - ...state[payload], + [_id]: { + ...state[_id], saved: true, }, }) + } default: return state diff --git a/src/redux/reducers/estimates.test.js b/src/redux/reducers/estimates.test.js new file mode 100644 index 0000000..3e25a6e --- /dev/null +++ b/src/redux/reducers/estimates.test.js @@ -0,0 +1,247 @@ +/* globals describe, it, expect */ + +import { + UPDATE_ESTIMATE, + RECALCULATE, + CLEAN_ESTIMATE, + CLEAN_ALL_ESTIMATES, + SET_TITLES, + MARK_ESTIMATE_SAVED, +} from '../actions/types' +import estimates, { emptyEstimate } from './estimates' + +describe('estimates reducer', () => { + it('returns initial state', () => { + expect(estimates(undefined, {})).toEqual({ new: emptyEstimate }) + }) + + it('updates estimate', () => { + // Given + const beforeState = { new: emptyEstimate } + const action = { + type: UPDATE_ESTIMATE, + payload: { + _id: 'new', + text: ` + @project Updated estimate + @participants Test User + `, + }, + } + // When + const afterState = estimates(beforeState, action) + // Then + expect(afterState).toEqual({ + new: { + _id: 'new', + text: ` + @project Updated estimate + @participants Test User + `, + graphData: [], + project: 'Updated estimate', + participants: ['Test User'], + calculated: false, + saved: false, + }, + }) + }) + + it('recalculates estimate', () => { + // Mock + console.table = console.log + // Given + const beforeState = { + new: { + _id: 'new', + text: ` + @project Updated estimate + @participants Test User + + Main task + Subtask A = 1 2 + Subtask B = 10 20 + # Commented task = 42 + + @summary + `, + graphData: [], + project: 'Updated estimate', + participants: ['Test User'], + calculated: false, + saved: false, + }, + } + const action = { + type: RECALCULATE, + payload: { _id: 'new' }, + } + // When + const afterState = estimates(beforeState, action) + // Then + expect(afterState).toEqual({ + new: { + _id: 'new', + text: ` + @project Updated estimate + @participants Test User + + Main task = 11 12 21 22 + Subtask A = 1 2 + Subtask B = 10 20 + # Commented task = 42 + +@summary = 11 12 21 22 + `, + graphData: [[11, 25], [12, 50], [21, 75], [22, 100]], + project: 'Updated estimate', + participants: ['Test User'], + calculated: true, + saved: false, + }, + }) + }) + + it('cleans estimate', () => { + // Given + const beforeState = { + new: { + _id: 'new', + text: ` + @project Updated estimate + @participants Test User + `, + graphData: [], + project: 'Updated estimate', + participants: ['Test User'], + calculated: false, + saved: false, + }, + } + const action = { + type: CLEAN_ESTIMATE, + payload: {}, + } + // When + const afterState = estimates(beforeState, action) + // Then + expect(afterState).toEqual({ new: emptyEstimate }) + }) + + + it('cleans all estimates', () => { + // Given + const beforeState = { + new: { + _id: 'new', + text: ` + New Project + `, + graphData: [], + project: '', + participants: [], + calculated: false, + saved: false, + }, + saved_project_id: { + id: 'saved_project_id', + text: ` + @project Updated estimate + @participants Test User + + Main task = 11 12 21 22 + Subtask A = 1 2 + Subtask B = 10 20 + # Commented task = 42 + +@summary = 11 12 21 22 + `, + graphData: [[11, 25], [12, 50], [21, 75], [22, 100]], + project: 'Updated estimate', + participants: ['Test User'], + calculated: true, + saved: true, + }, + } + const action = { type: CLEAN_ALL_ESTIMATES } + // When + const afterState = estimates(beforeState, action) + // Then + expect(afterState).toEqual({ new: emptyEstimate }) + }) + + it('sets titles of projects', () => { + // Given + const beforeState = { new: emptyEstimate } + const action = { + type: SET_TITLES, + payload: { + projects: [ + { _id: 'foo' }, + { _id: 'bar' }, + ], + }, + } + // When + const afterState = estimates(beforeState, action) + // Then + expect(afterState).toEqual({ + new: emptyEstimate, + foo: { _id: 'foo', saved: true }, + bar: { _id: 'bar', saved: true }, + }) + }) + + it('marks estimate as saved', () => { + // Given + const beforeState = { + saved_estimate_id: { + _id: 'saved_estimate_id', + text: ` + @project Updated estimate + @participants Test User + + Main task = 11 12 21 22 + Subtask A = 1 2 + Subtask B = 10 20 + # Commented task = 42 + + @summary = 11 12 21 22 + `, + graphData: [[11, 25], [12, 50], [21, 75], [22, 100]], + project: 'Updated estimate', + participants: ['Test User'], + calculated: true, + saved: false, + }, + } + const action = { + type: MARK_ESTIMATE_SAVED, + payload: { _id: 'saved_estimate_id' }, + } + // When + const afterState = estimates(beforeState, action) + // Then + expect(afterState).toEqual({ + saved_estimate_id: { + _id: 'saved_estimate_id', + text: ` + @project Updated estimate + @participants Test User + + Main task = 11 12 21 22 + Subtask A = 1 2 + Subtask B = 10 20 + # Commented task = 42 + + @summary = 11 12 21 22 + `, + graphData: [[11, 25], [12, 50], [21, 75], [22, 100]], + project: 'Updated estimate', + participants: ['Test User'], + calculated: true, + saved: true, + }, + }) + }) +}) diff --git a/src/redux/reducers/redirector.js b/src/redux/reducers/redirector.js index e8a0309..fa9df0a 100644 --- a/src/redux/reducers/redirector.js +++ b/src/redux/reducers/redirector.js @@ -1,17 +1,17 @@ -import { SET_HREF, RESET_HREF } from '../actions/types' +import { SET_LOCATION, RESET_LOCATION } from '../actions/types' export default (state = ({ - href: '', + location: '', }), { type, payload }) => { switch (type) { - case SET_HREF: + case SET_LOCATION: return ({ - href: payload, + location: payload, }) - case RESET_HREF: + case RESET_LOCATION: return ({ - href: '', + location: '', }) default: diff --git a/src/redux/reducers/redirector.test.js b/src/redux/reducers/redirector.test.js index 67e3923..b0f51e5 100644 --- a/src/redux/reducers/redirector.test.js +++ b/src/redux/reducers/redirector.test.js @@ -1,29 +1,50 @@ /* globals describe, it, expect */ -import { SET_HREF, RESET_HREF } from '../actions/types' +import { SET_LOCATION, RESET_LOCATION } from '../actions/types' import redirector from './redirector' describe("redirector's behavior", () => { it('returns initial state', () => { - expect(redirector(undefined, {})).toEqual({ href: '' }) + expect(redirector(undefined, {})).toEqual({ location: '' }) }) - it('sets up correct path', () => { + it('sets up correct path (string)', () => { // Given - const beforeState = { href: '' } - const action = { type: SET_HREF, payload: '/estimate/new' } + const beforeState = { location: '' } + const action = { type: SET_LOCATION, payload: '/estimate/new' } // When const afterState = redirector(beforeState, action) // Then - expect(afterState).toEqual({ href: '/estimate/new' }) + expect(afterState).toEqual({ location: '/estimate/new' }) + }) + + it('sets up correct path (object)', () => { + // Given + const beforeState = { location: '' } + const action = { + type: SET_LOCATION, + payload: { + pathname: '/auth', + state: { from: '/estimate/new' }, + }, + } + // When + const afterState = redirector(beforeState, action) + // Then + expect(afterState).toEqual({ + location: { + pathname: '/auth', + state: { from: '/estimate/new' }, + }, + }) }) it('resets path', () => { // Given - const beforeState = { href: '/estimate/new' } - const action = { type: RESET_HREF } + const beforeState = { location: '/estimate/new' } + const action = { type: RESET_LOCATION } // When const afterState = redirector(beforeState, action) - expect(afterState).toEqual({ href: '' }) + expect(afterState).toEqual({ location: '' }) }) }) diff --git a/src/redux/reducers/visualEffects.js b/src/redux/reducers/visualEffects.js index 0bccd68..f4cda99 100644 --- a/src/redux/reducers/visualEffects.js +++ b/src/redux/reducers/visualEffects.js @@ -1,15 +1,12 @@ import { ADD_SPINNER, DEL_SPINNER, - OPEN_AUTH_SCREEN, - CLOSE_AUTH_SCREEN, ENLARGE_GRAPH, MINIFY_GRAPH, } from '../actions/types' export default (state = { spinnersCount: false, - showAuthScreen: false, graphView: 'minified', }, { type }) => { switch (type) { @@ -25,18 +22,6 @@ export default (state = { spinnersCount: state.spinnersCount - 1, }) - case OPEN_AUTH_SCREEN: - return ({ - ...state, - showAuthScreen: true, - }) - - case CLOSE_AUTH_SCREEN: - return ({ - ...state, - showAuthScreen: false, - }) - case ENLARGE_GRAPH: return ({ ...state,