From a9ee70e033f1dd50ba44659c560fb2cfe3eb39b7 Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 24 Jan 2017 15:29:25 -0500 Subject: [PATCH] #254 show error when user attempts to save stale version of project, refactor error modals to one component --- client/components/Nav.jsx | 4 +- client/constants.js | 6 +- client/modules/IDE/actions/ide.js | 21 ++---- client/modules/IDE/actions/project.js | 19 +++-- client/modules/IDE/actions/projects.js | 4 +- .../IDE/components/AuthenticationError.jsx | 24 ------- client/modules/IDE/components/ErrorModal.jsx | 70 +++++++++++++++++++ .../IDE/components/ForceAuthentication.jsx | 36 ---------- client/modules/IDE/pages/IDEView.jsx | 33 +++------ client/modules/IDE/reducers/ide.js | 15 ++-- client/modules/IDE/reducers/project.js | 6 +- client/modules/User/actions.js | 6 +- .../components/_authentication-error.scss | 13 ---- ...-authentication.scss => _error-modal.scss} | 12 ++-- client/styles/main.scss | 4 +- server/controllers/project.controller.js | 6 +- 16 files changed, 123 insertions(+), 156 deletions(-) delete mode 100644 client/modules/IDE/components/AuthenticationError.jsx create mode 100644 client/modules/IDE/components/ErrorModal.jsx delete mode 100644 client/modules/IDE/components/ForceAuthentication.jsx delete mode 100644 client/styles/components/_authentication-error.scss rename client/styles/components/{_force-authentication.scss => _error-modal.scss} (65%) diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 563a8832..6bfcbae7 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -29,7 +29,7 @@ function Nav(props) { if (props.user.authenticated) { props.saveProject(); } else { - props.openForceAuthentication(); + props.showErrorModal('forceAuthentication'); } }} > @@ -168,7 +168,7 @@ Nav.propTypes = { logoutUser: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, showShareModal: PropTypes.func.isRequired, - openForceAuthentication: PropTypes.func.isRequired + showErrorModal: PropTypes.func.isRequired }; export default Nav; diff --git a/client/constants.js b/client/constants.js index 32f24968..baa61b84 100644 --- a/client/constants.js +++ b/client/constants.js @@ -106,7 +106,5 @@ export const RESET_JUST_OPENED_PROJECT = 'RESET_JUST_OPENED_PROJECT'; export const SET_PROJECT_SAVED_TIME = 'SET_PROJECT_SAVED_TIME'; export const RESET_PROJECT_SAVED_TIME = 'RESET_PROJECT_SAVED_TIME'; export const SET_PREVIOUS_PATH = 'SET_PREVIOUS_PATH'; -export const OPEN_FORCE_AUTHENTICATION = 'OPEN_FORCE_AUTHENTICATION'; -export const CLOSE_FORCE_AUTHENTICATION = 'CLOSE_FORCE_AUTHENTICATION'; -export const SHOW_AUTHENTICATION_ERROR = 'SHOW_AUTHENTICATION_ERROR'; -export const HIDE_AUTHENTICATION_ERROR = 'HIDE_AUTHENTICATION_ERROR'; +export const SHOW_ERROR_MODAL = 'SHOW_ERROR_MODAL'; +export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL'; diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index 487ac392..0d1b0f4c 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -221,26 +221,15 @@ export function setPreviousPath(path) { }; } -export function openForceAuthentication() { +export function showErrorModal(modalType) { return { - type: ActionTypes.OPEN_FORCE_AUTHENTICATION + type: ActionTypes.SHOW_ERROR_MODAL, + modalType }; } -export function closeForceAuthentication() { +export function hideErrorModal() { return { - type: ActionTypes.CLOSE_FORCE_AUTHENTICATION - }; -} - -export function showAuthenticationError() { - return { - type: ActionTypes.SHOW_AUTHENTICATION_ERROR - }; -} - -export function hideAuthenticationError() { - return { - type: ActionTypes.HIDE_AUTHENTICATION_ERROR + type: ActionTypes.HIDE_ERROR_MODAL }; } diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 385e55c8..0ced170a 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -7,7 +7,7 @@ import { setUnsavedChanges, resetJustOpenedProject, setProjectSavedTime, resetProjectSavedTime, - showAuthenticationError } from './ide'; + showErrorModal } from './ide'; import moment from 'moment'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -21,7 +21,6 @@ export function getProject(id) { } axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true }) .then(response => { - // browserHistory.push(`/projects/${id}`); dispatch({ type: ActionTypes.SET_PROJECT, project: response.data, @@ -74,7 +73,9 @@ export function saveProject(autosave = false) { }) .catch((response) => { if (response.status === 403) { - dispatch(showAuthenticationError()); + dispatch(showErrorModal('staleSession')); + } else if (response.status === 409) { + dispatch(showErrorModal('staleProject')); } else { dispatch({ type: ActionTypes.PROJECT_SAVE_FAIL, @@ -90,8 +91,7 @@ export function saveProject(autosave = false) { browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); dispatch({ type: ActionTypes.NEW_PROJECT, - name: response.data.name, - id: response.data.id, + project: response.data, owner: response.data.user, files: response.data.files }); @@ -109,7 +109,7 @@ export function saveProject(autosave = false) { }) .catch(response => { if (response.status === 403) { - dispatch(showAuthenticationError()); + dispatch(showErrorModal('staleSession')); } else { dispatch({ type: ActionTypes.PROJECT_SAVE_FAIL, @@ -134,8 +134,7 @@ export function createProject() { browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); dispatch({ type: ActionTypes.NEW_PROJECT, - name: response.data.name, - id: response.data.id, + project: response.data, owner: response.data.user, files: response.data.files }); @@ -176,10 +175,8 @@ export function cloneProject() { browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); dispatch({ type: ActionTypes.NEW_PROJECT, - name: response.data.name, - id: response.data.id, + project: response.data, owner: response.data.user, - selectedFile: response.data.selectedFile, files: response.data.files }); }) diff --git a/client/modules/IDE/actions/projects.js b/client/modules/IDE/actions/projects.js index 4c100e35..2561f7c9 100644 --- a/client/modules/IDE/actions/projects.js +++ b/client/modules/IDE/actions/projects.js @@ -1,6 +1,6 @@ import * as ActionTypes from '../../../constants'; import axios from 'axios'; -import { showAuthenticationError, setPreviousPath } from './ide'; +import { showErrorModal, setPreviousPath } from './ide'; import { resetProject } from './project'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -43,7 +43,7 @@ export function deleteProject(id) { }) .catch(response => { if (response.status === 403) { - dispatch(showAuthenticationError()); + dispatch(showErrorModal('staleSession')); } else { dispatch({ type: ActionTypes.ERROR, diff --git a/client/modules/IDE/components/AuthenticationError.jsx b/client/modules/IDE/components/AuthenticationError.jsx deleted file mode 100644 index 6e11a9f0..00000000 --- a/client/modules/IDE/components/AuthenticationError.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React, { PropTypes } from 'react'; -import { Link } from 'react-router'; - -function AuthenticationError(props) { - return ( -
-
-

Error

-
-
-

- It looks like you've been logged out. Please  - log in. -

-
-
- ); -} - -AuthenticationError.propTypes = { - closeModal: PropTypes.func.isRequired -}; - -export default AuthenticationError; diff --git a/client/modules/IDE/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx new file mode 100644 index 00000000..7cd91f60 --- /dev/null +++ b/client/modules/IDE/components/ErrorModal.jsx @@ -0,0 +1,70 @@ +import React, { PropTypes } from 'react'; +import InlineSVG from 'react-inlinesvg'; +const exitUrl = require('../../../images/exit.svg'); +import { Link } from 'react-router'; + +class ErrorModal extends React.Component { + componentDidMount() { + this.refs.modal.focus(); + } + + + forceAuthentication() { + return ( +

+ In order to save sketches, you must be logged in. Please  + Login +  or  + Sign Up. +

+ ); + } + + staleSession() { + return ( +

+ It looks like you've been logged out. Please  + log in. +

+ ); + } + + staleProject() { + return ( +

+ The project you have attempted to save is out of date. Please refresh the page. +

+ ); + } + + render() { + return ( +
+
+

Error

+ +
+
+ {(() => { // eslint-disable-line + if (this.props.type === 'forceAuthentication') { + return this.forceAuthentication(); + } else if (this.props.type === 'staleSession') { + return this.staleSession(); + } else if (this.props.type === 'staleProject') { + return this.staleProject(); + } + })()} +
+
+ ); + } +} + +ErrorModal.propTypes = { + type: PropTypes.string, + closeModal: PropTypes.func.isRequired +}; + +export default ErrorModal; diff --git a/client/modules/IDE/components/ForceAuthentication.jsx b/client/modules/IDE/components/ForceAuthentication.jsx deleted file mode 100644 index 531c0e70..00000000 --- a/client/modules/IDE/components/ForceAuthentication.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { PropTypes } from 'react'; -import InlineSVG from 'react-inlinesvg'; -const exitUrl = require('../../../images/exit.svg'); -import { Link } from 'react-router'; - -class ForceAuthentication extends React.Component { - componentDidMount() { - this.refs.forceAuthentication.focus(); - } - - render() { - return ( -
-
- -
-
-

- In order to save sketches, you must be logged in. Please  - Login -  or  - Sign Up. -

-
-
- ); - } -} - -ForceAuthentication.propTypes = { - closeModal: PropTypes.func.isRequired -}; - -export default ForceAuthentication; diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index ad209a40..5845be81 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -9,8 +9,7 @@ import NewFileModal from '../components/NewFileModal'; import NewFolderModal from '../components/NewFolderModal'; import ShareModal from '../components/ShareModal'; import KeyboardShortcutModal from '../components/KeyboardShortcutModal'; -import ForceAuthentication from '../components/ForceAuthentication'; -import AuthenticationError from '../components/AuthenticationError'; +import ErrorModal from '../components/ErrorModal'; import Nav from '../../../components/Nav'; import Console from '../components/Console'; import Toast from '../components/Toast'; @@ -195,7 +194,7 @@ class IDEView extends React.Component { logoutUser={this.props.logoutUser} stopSketch={this.props.stopSketch} showShareModal={this.props.showShareModal} - openForceAuthentication={this.props.openForceAuthentication} + showErrorModal={this.props.showErrorModal} unsavedChanges={this.props.ide.unsavedChanges} warnIfUnsavedChanges={this.warnIfUnsavedChanges} /> @@ -425,22 +424,12 @@ class IDEView extends React.Component { } })()} {(() => { // eslint-disable-line - if (this.props.ide.forceAuthenticationVisible) { + if (this.props.ide.errorType) { return ( - - - ); - } - })()} - {(() => { // eslint-disable-line - if (this.props.ide.authenticationError) { - return ( - - ); @@ -488,9 +477,8 @@ IDEView.propTypes = { infiniteLoopMessage: PropTypes.string.isRequired, projectSavedTime: PropTypes.string.isRequired, previousPath: PropTypes.string.isRequired, - forceAuthenticationVisible: PropTypes.bool.isRequired, - authenticationError: PropTypes.bool.isRequired, - justOpenedProject: PropTypes.bool.isRequired + justOpenedProject: PropTypes.bool.isRequired, + errorType: PropTypes.string }).isRequired, startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, @@ -590,11 +578,10 @@ IDEView.propTypes = { setBlobUrl: PropTypes.func.isRequired, setPreviousPath: PropTypes.func.isRequired, resetProject: PropTypes.func.isRequired, - closeForceAuthentication: PropTypes.func.isRequired, - openForceAuthentication: PropTypes.func.isRequired, console: PropTypes.array.isRequired, clearConsole: PropTypes.func.isRequired, - hideAuthenticationError: PropTypes.func.isRequired + showErrorModal: PropTypes.func.isRequired, + hideErrorModal: PropTypes.func.isRequired }; function mapStateToProps(state) { diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 5c01c142..f4908f2a 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -19,8 +19,7 @@ const initialState = { justOpenedProject: false, projectSavedTime: '', previousPath: '/', - forceAuthenticationVisible: false, - authenticationError: false + errorType: undefined }; const ide = (state = initialState, action) => { @@ -93,14 +92,10 @@ const ide = (state = initialState, action) => { return Object.assign({}, state, { projectSavedTime: '' }); case ActionTypes.SET_PREVIOUS_PATH: return Object.assign({}, state, { previousPath: action.path }); - case ActionTypes.OPEN_FORCE_AUTHENTICATION: - return Object.assign({}, state, { forceAuthenticationVisible: true }); - case ActionTypes.CLOSE_FORCE_AUTHENTICATION: - return Object.assign({}, state, { forceAuthenticationVisible: false }); - case ActionTypes.SHOW_AUTHENTICATION_ERROR: - return Object.assign({}, state, { authenticationError: true }); - case ActionTypes.HIDE_AUTHENTICATION_ERROR: - return Object.assign({}, state, { authenticationError: false }); + case ActionTypes.SHOW_ERROR_MODAL: + return Object.assign({}, state, { errorType: action.modalType }); + case ActionTypes.HIDE_ERROR_MODAL: + return Object.assign({}, state, { errorType: undefined }); default: return state; } diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index 7d4df05e..2a89d519 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -18,14 +18,16 @@ const project = (state, action) => { return Object.assign({}, { ...state }, { name: action.name }); case ActionTypes.NEW_PROJECT: return { - id: action.id, - name: action.name, + id: action.project.id, + name: action.project.name, + updatedAt: action.project.updatedAt, owner: action.owner }; case ActionTypes.SET_PROJECT: return { id: action.project.id, name: action.project.name, + updatedAt: action.project.updatedAt, owner: action.owner }; case ActionTypes.RESET_PROJECT: diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 3eaeb0fd..21617ac5 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -1,7 +1,7 @@ import * as ActionTypes from '../../constants'; import { browserHistory } from 'react-router'; import axios from 'axios'; -import { showAuthenticationError, justOpenedProject } from '../IDE/actions/ide'; +import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -91,12 +91,12 @@ export function validateSession() { .then(response => { const state = getState(); if (state.user.username !== response.data.username) { - dispatch(showAuthenticationError()); + dispatch(showErrorModal('staleSession')); } }) .catch(response => { if (response.status === 404) { - dispatch(showAuthenticationError()); + dispatch(showErrorModal('staleSession')); } }); }; diff --git a/client/styles/components/_authentication-error.scss b/client/styles/components/_authentication-error.scss deleted file mode 100644 index 5b45e14a..00000000 --- a/client/styles/components/_authentication-error.scss +++ /dev/null @@ -1,13 +0,0 @@ -.authentication-error { - @extend %modal; -} - -.authentication-error__header { - padding: #{20 / $base-font-size}rem; -} - -.authentication-error__copy { - padding: #{20 / $base-font-size}rem; - padding-top: 0; - padding-bottom: #{60 / $base-font-size}rem; -} \ No newline at end of file diff --git a/client/styles/components/_force-authentication.scss b/client/styles/components/_error-modal.scss similarity index 65% rename from client/styles/components/_force-authentication.scss rename to client/styles/components/_error-modal.scss index b75d87eb..35eb940f 100644 --- a/client/styles/components/_force-authentication.scss +++ b/client/styles/components/_error-modal.scss @@ -1,24 +1,24 @@ -.force-authentication { +.error-modal { @extend %modal; display: flex; flex-wrap: wrap; flex-flow: column; } -.force-authentication__header { +.error-modal__header { display: flex; - justify-content: flex-end; + justify-content: space-between; padding: #{20 / $base-font-size}rem; } -.force-authentication__exit-button { +.error-modal__exit-button { @include themify() { @extend %icon; } } -.force-authentication__copy { +.error-modal__content { padding: #{20 / $base-font-size}rem; padding-top: 0; padding-bottom: #{60 / $base-font-size}rem; -} \ No newline at end of file +} diff --git a/client/styles/main.scss b/client/styles/main.scss index 823c0e89..d5a507a6 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -30,10 +30,8 @@ @import 'components/forms'; @import 'components/toast'; @import 'components/timer'; -@import 'components/force-authentication'; @import 'components/form-container'; -@import 'components/uploader'; -@import 'components/authentication-error'; +@import 'components/error-modal'; @import 'layout/ide'; @import 'layout/fullscreen'; diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index db401e41..0d3c9e9e 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -2,6 +2,7 @@ import Project from '../models/project'; import User from '../models/user'; import archiver from 'archiver'; import request from 'request'; +import moment from 'moment'; export function createProject(req, res) { @@ -29,7 +30,10 @@ export function createProject(req, res) { export function updateProject(req, res) { Project.findById(req.params.project_id, (err, project) => { if (!req.user || !project.user.equals(req.user._id)) { - return res.status(403).send({ success: false, message: 'Session does not match owner of project.'}); + return res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); + } + if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) { + return res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' }) } Project.findByIdAndUpdate(req.params.project_id, {