#254 show error when user attempts to save stale version of project, refactor error modals to one component

This commit is contained in:
Cassie Tarakajian 2017-01-24 15:29:25 -05:00
parent c8253dd923
commit a9ee70e033
16 changed files with 123 additions and 156 deletions

View file

@ -29,7 +29,7 @@ function Nav(props) {
if (props.user.authenticated) { if (props.user.authenticated) {
props.saveProject(); props.saveProject();
} else { } else {
props.openForceAuthentication(); props.showErrorModal('forceAuthentication');
} }
}} }}
> >
@ -168,7 +168,7 @@ Nav.propTypes = {
logoutUser: PropTypes.func.isRequired, logoutUser: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,
showShareModal: PropTypes.func.isRequired, showShareModal: PropTypes.func.isRequired,
openForceAuthentication: PropTypes.func.isRequired showErrorModal: PropTypes.func.isRequired
}; };
export default Nav; export default Nav;

View file

@ -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 SET_PROJECT_SAVED_TIME = 'SET_PROJECT_SAVED_TIME';
export const RESET_PROJECT_SAVED_TIME = 'RESET_PROJECT_SAVED_TIME'; export const RESET_PROJECT_SAVED_TIME = 'RESET_PROJECT_SAVED_TIME';
export const SET_PREVIOUS_PATH = 'SET_PREVIOUS_PATH'; export const SET_PREVIOUS_PATH = 'SET_PREVIOUS_PATH';
export const OPEN_FORCE_AUTHENTICATION = 'OPEN_FORCE_AUTHENTICATION'; export const SHOW_ERROR_MODAL = 'SHOW_ERROR_MODAL';
export const CLOSE_FORCE_AUTHENTICATION = 'CLOSE_FORCE_AUTHENTICATION'; export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL';
export const SHOW_AUTHENTICATION_ERROR = 'SHOW_AUTHENTICATION_ERROR';
export const HIDE_AUTHENTICATION_ERROR = 'HIDE_AUTHENTICATION_ERROR';

View file

@ -221,26 +221,15 @@ export function setPreviousPath(path) {
}; };
} }
export function openForceAuthentication() { export function showErrorModal(modalType) {
return { return {
type: ActionTypes.OPEN_FORCE_AUTHENTICATION type: ActionTypes.SHOW_ERROR_MODAL,
modalType
}; };
} }
export function closeForceAuthentication() { export function hideErrorModal() {
return { return {
type: ActionTypes.CLOSE_FORCE_AUTHENTICATION type: ActionTypes.HIDE_ERROR_MODAL
};
}
export function showAuthenticationError() {
return {
type: ActionTypes.SHOW_AUTHENTICATION_ERROR
};
}
export function hideAuthenticationError() {
return {
type: ActionTypes.HIDE_AUTHENTICATION_ERROR
}; };
} }

View file

@ -7,7 +7,7 @@ import { setUnsavedChanges,
resetJustOpenedProject, resetJustOpenedProject,
setProjectSavedTime, setProjectSavedTime,
resetProjectSavedTime, resetProjectSavedTime,
showAuthenticationError } from './ide'; showErrorModal } from './ide';
import moment from 'moment'; import moment from 'moment';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; 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 }) axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
.then(response => { .then(response => {
// browserHistory.push(`/projects/${id}`);
dispatch({ dispatch({
type: ActionTypes.SET_PROJECT, type: ActionTypes.SET_PROJECT,
project: response.data, project: response.data,
@ -74,7 +73,9 @@ export function saveProject(autosave = false) {
}) })
.catch((response) => { .catch((response) => {
if (response.status === 403) { if (response.status === 403) {
dispatch(showAuthenticationError()); dispatch(showErrorModal('staleSession'));
} else if (response.status === 409) {
dispatch(showErrorModal('staleProject'));
} else { } else {
dispatch({ dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL, type: ActionTypes.PROJECT_SAVE_FAIL,
@ -90,8 +91,7 @@ export function saveProject(autosave = false) {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({ dispatch({
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
name: response.data.name, project: response.data,
id: response.data.id,
owner: response.data.user, owner: response.data.user,
files: response.data.files files: response.data.files
}); });
@ -109,7 +109,7 @@ export function saveProject(autosave = false) {
}) })
.catch(response => { .catch(response => {
if (response.status === 403) { if (response.status === 403) {
dispatch(showAuthenticationError()); dispatch(showErrorModal('staleSession'));
} else { } else {
dispatch({ dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL, type: ActionTypes.PROJECT_SAVE_FAIL,
@ -134,8 +134,7 @@ export function createProject() {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({ dispatch({
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
name: response.data.name, project: response.data,
id: response.data.id,
owner: response.data.user, owner: response.data.user,
files: response.data.files files: response.data.files
}); });
@ -176,10 +175,8 @@ export function cloneProject() {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({ dispatch({
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
name: response.data.name, project: response.data,
id: response.data.id,
owner: response.data.user, owner: response.data.user,
selectedFile: response.data.selectedFile,
files: response.data.files files: response.data.files
}); });
}) })

View file

@ -1,6 +1,6 @@
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import axios from 'axios'; import axios from 'axios';
import { showAuthenticationError, setPreviousPath } from './ide'; import { showErrorModal, setPreviousPath } from './ide';
import { resetProject } from './project'; import { resetProject } from './project';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@ -43,7 +43,7 @@ export function deleteProject(id) {
}) })
.catch(response => { .catch(response => {
if (response.status === 403) { if (response.status === 403) {
dispatch(showAuthenticationError()); dispatch(showErrorModal('staleSession'));
} else { } else {
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,

View file

@ -1,24 +0,0 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
function AuthenticationError(props) {
return (
<section className="authentication-error" tabIndex="0">
<header className="authentication-error__header">
<h2 className="authentication-error__title">Error</h2>
</header>
<div className="authentication-error__copy">
<p>
It looks like you've been logged out. Please&nbsp;
<Link to="/login" onClick={props.closeModal}>log in</Link>.
</p>
</div>
</section>
);
}
AuthenticationError.propTypes = {
closeModal: PropTypes.func.isRequired
};
export default AuthenticationError;

View file

@ -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 (
<p>
In order to save sketches, you must be logged in. Please&nbsp;
<Link to="/login" onClick={this.props.closeModal}>Login</Link>
&nbsp;or&nbsp;
<Link to="/signup" onClick={this.props.closeModal}>Sign Up</Link>.
</p>
);
}
staleSession() {
return (
<p>
It looks like you've been logged out. Please&nbsp;
<Link to="/login" onClick={this.props.closeModal}>log in</Link>.
</p>
);
}
staleProject() {
return (
<p>
The project you have attempted to save is out of date. Please refresh the page.
</p>
);
}
render() {
return (
<section className="error-modal" ref="modal" tabIndex="0">
<header className="error-modal__header">
<h2 className="error-modal__title">Error</h2>
<button className="error-modal__exit-button" onClick={this.props.closeModal}>
<InlineSVG src={exitUrl} alt="Close Error Modal" />
</button>
</header>
<div className="error-modal__content">
{(() => { // 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();
}
})()}
</div>
</section>
);
}
}
ErrorModal.propTypes = {
type: PropTypes.string,
closeModal: PropTypes.func.isRequired
};
export default ErrorModal;

View file

@ -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 (
<section className="force-authentication" ref="forceAuthentication" tabIndex="0">
<header className="force-authentication__header">
<button className="force-authentication__exit-button" onClick={this.props.closeModal}>
<InlineSVG src={exitUrl} alt="Close About Overlay" />
</button>
</header>
<div className="force-authentication__copy">
<p>
In order to save sketches, you must be logged in. Please&nbsp;
<Link to="/login" onClick={this.props.closeModal}>Login</Link>
&nbsp;or&nbsp;
<Link to="/signup" onClick={this.props.closeModal}>Sign Up</Link>.
</p>
</div>
</section>
);
}
}
ForceAuthentication.propTypes = {
closeModal: PropTypes.func.isRequired
};
export default ForceAuthentication;

View file

@ -9,8 +9,7 @@ import NewFileModal from '../components/NewFileModal';
import NewFolderModal from '../components/NewFolderModal'; import NewFolderModal from '../components/NewFolderModal';
import ShareModal from '../components/ShareModal'; import ShareModal from '../components/ShareModal';
import KeyboardShortcutModal from '../components/KeyboardShortcutModal'; import KeyboardShortcutModal from '../components/KeyboardShortcutModal';
import ForceAuthentication from '../components/ForceAuthentication'; import ErrorModal from '../components/ErrorModal';
import AuthenticationError from '../components/AuthenticationError';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
import Console from '../components/Console'; import Console from '../components/Console';
import Toast from '../components/Toast'; import Toast from '../components/Toast';
@ -195,7 +194,7 @@ class IDEView extends React.Component {
logoutUser={this.props.logoutUser} logoutUser={this.props.logoutUser}
stopSketch={this.props.stopSketch} stopSketch={this.props.stopSketch}
showShareModal={this.props.showShareModal} showShareModal={this.props.showShareModal}
openForceAuthentication={this.props.openForceAuthentication} showErrorModal={this.props.showErrorModal}
unsavedChanges={this.props.ide.unsavedChanges} unsavedChanges={this.props.ide.unsavedChanges}
warnIfUnsavedChanges={this.warnIfUnsavedChanges} warnIfUnsavedChanges={this.warnIfUnsavedChanges}
/> />
@ -425,22 +424,12 @@ class IDEView extends React.Component {
} }
})()} })()}
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.ide.forceAuthenticationVisible) { if (this.props.ide.errorType) {
return ( return (
<Overlay> <Overlay>
<ForceAuthentication <ErrorModal
closeModal={this.props.closeForceAuthentication} type={this.props.ide.errorType}
/> closeModal={this.props.hideErrorModal}
</Overlay>
);
}
})()}
{(() => { // eslint-disable-line
if (this.props.ide.authenticationError) {
return (
<Overlay>
<AuthenticationError
closeModal={this.props.hideAuthenticationError}
/> />
</Overlay> </Overlay>
); );
@ -488,9 +477,8 @@ IDEView.propTypes = {
infiniteLoopMessage: PropTypes.string.isRequired, infiniteLoopMessage: PropTypes.string.isRequired,
projectSavedTime: PropTypes.string.isRequired, projectSavedTime: PropTypes.string.isRequired,
previousPath: PropTypes.string.isRequired, previousPath: PropTypes.string.isRequired,
forceAuthenticationVisible: PropTypes.bool.isRequired, justOpenedProject: PropTypes.bool.isRequired,
authenticationError: PropTypes.bool.isRequired, errorType: PropTypes.string
justOpenedProject: PropTypes.bool.isRequired
}).isRequired, }).isRequired,
startSketch: PropTypes.func.isRequired, startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,
@ -590,11 +578,10 @@ IDEView.propTypes = {
setBlobUrl: PropTypes.func.isRequired, setBlobUrl: PropTypes.func.isRequired,
setPreviousPath: PropTypes.func.isRequired, setPreviousPath: PropTypes.func.isRequired,
resetProject: PropTypes.func.isRequired, resetProject: PropTypes.func.isRequired,
closeForceAuthentication: PropTypes.func.isRequired,
openForceAuthentication: PropTypes.func.isRequired,
console: PropTypes.array.isRequired, console: PropTypes.array.isRequired,
clearConsole: PropTypes.func.isRequired, clearConsole: PropTypes.func.isRequired,
hideAuthenticationError: PropTypes.func.isRequired showErrorModal: PropTypes.func.isRequired,
hideErrorModal: PropTypes.func.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {

View file

@ -19,8 +19,7 @@ const initialState = {
justOpenedProject: false, justOpenedProject: false,
projectSavedTime: '', projectSavedTime: '',
previousPath: '/', previousPath: '/',
forceAuthenticationVisible: false, errorType: undefined
authenticationError: false
}; };
const ide = (state = initialState, action) => { const ide = (state = initialState, action) => {
@ -93,14 +92,10 @@ const ide = (state = initialState, action) => {
return Object.assign({}, state, { projectSavedTime: '' }); return Object.assign({}, state, { projectSavedTime: '' });
case ActionTypes.SET_PREVIOUS_PATH: case ActionTypes.SET_PREVIOUS_PATH:
return Object.assign({}, state, { previousPath: action.path }); return Object.assign({}, state, { previousPath: action.path });
case ActionTypes.OPEN_FORCE_AUTHENTICATION: case ActionTypes.SHOW_ERROR_MODAL:
return Object.assign({}, state, { forceAuthenticationVisible: true }); return Object.assign({}, state, { errorType: action.modalType });
case ActionTypes.CLOSE_FORCE_AUTHENTICATION: case ActionTypes.HIDE_ERROR_MODAL:
return Object.assign({}, state, { forceAuthenticationVisible: false }); return Object.assign({}, state, { errorType: undefined });
case ActionTypes.SHOW_AUTHENTICATION_ERROR:
return Object.assign({}, state, { authenticationError: true });
case ActionTypes.HIDE_AUTHENTICATION_ERROR:
return Object.assign({}, state, { authenticationError: false });
default: default:
return state; return state;
} }

View file

@ -18,14 +18,16 @@ const project = (state, action) => {
return Object.assign({}, { ...state }, { name: action.name }); return Object.assign({}, { ...state }, { name: action.name });
case ActionTypes.NEW_PROJECT: case ActionTypes.NEW_PROJECT:
return { return {
id: action.id, id: action.project.id,
name: action.name, name: action.project.name,
updatedAt: action.project.updatedAt,
owner: action.owner owner: action.owner
}; };
case ActionTypes.SET_PROJECT: case ActionTypes.SET_PROJECT:
return { return {
id: action.project.id, id: action.project.id,
name: action.project.name, name: action.project.name,
updatedAt: action.project.updatedAt,
owner: action.owner owner: action.owner
}; };
case ActionTypes.RESET_PROJECT: case ActionTypes.RESET_PROJECT:

View file

@ -1,7 +1,7 @@
import * as ActionTypes from '../../constants'; import * as ActionTypes from '../../constants';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import axios from 'axios'; 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'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@ -91,12 +91,12 @@ export function validateSession() {
.then(response => { .then(response => {
const state = getState(); const state = getState();
if (state.user.username !== response.data.username) { if (state.user.username !== response.data.username) {
dispatch(showAuthenticationError()); dispatch(showErrorModal('staleSession'));
} }
}) })
.catch(response => { .catch(response => {
if (response.status === 404) { if (response.status === 404) {
dispatch(showAuthenticationError()); dispatch(showErrorModal('staleSession'));
} }
}); });
}; };

View file

@ -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;
}

View file

@ -1,23 +1,23 @@
.force-authentication { .error-modal {
@extend %modal; @extend %modal;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex-flow: column; flex-flow: column;
} }
.force-authentication__header { .error-modal__header {
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
padding: #{20 / $base-font-size}rem; padding: #{20 / $base-font-size}rem;
} }
.force-authentication__exit-button { .error-modal__exit-button {
@include themify() { @include themify() {
@extend %icon; @extend %icon;
} }
} }
.force-authentication__copy { .error-modal__content {
padding: #{20 / $base-font-size}rem; padding: #{20 / $base-font-size}rem;
padding-top: 0; padding-top: 0;
padding-bottom: #{60 / $base-font-size}rem; padding-bottom: #{60 / $base-font-size}rem;

View file

@ -30,10 +30,8 @@
@import 'components/forms'; @import 'components/forms';
@import 'components/toast'; @import 'components/toast';
@import 'components/timer'; @import 'components/timer';
@import 'components/force-authentication';
@import 'components/form-container'; @import 'components/form-container';
@import 'components/uploader'; @import 'components/error-modal';
@import 'components/authentication-error';
@import 'layout/ide'; @import 'layout/ide';
@import 'layout/fullscreen'; @import 'layout/fullscreen';

View file

@ -2,6 +2,7 @@ import Project from '../models/project';
import User from '../models/user'; import User from '../models/user';
import archiver from 'archiver'; import archiver from 'archiver';
import request from 'request'; import request from 'request';
import moment from 'moment';
export function createProject(req, res) { export function createProject(req, res) {
@ -29,7 +30,10 @@ export function createProject(req, res) {
export function updateProject(req, res) { export function updateProject(req, res) {
Project.findById(req.params.project_id, (err, project) => { Project.findById(req.params.project_id, (err, project) => {
if (!req.user || !project.user.equals(req.user._id)) { 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, Project.findByIdAndUpdate(req.params.project_id,
{ {