#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) {
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;

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 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';

View file

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

View file

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

View file

@ -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,

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 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 (
<Overlay>
<ForceAuthentication
closeModal={this.props.closeForceAuthentication}
/>
</Overlay>
);
}
})()}
{(() => { // eslint-disable-line
if (this.props.ide.authenticationError) {
return (
<Overlay>
<AuthenticationError
closeModal={this.props.hideAuthenticationError}
<ErrorModal
type={this.props.ide.errorType}
closeModal={this.props.hideErrorModal}
/>
</Overlay>
);
@ -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) {

View file

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

View file

@ -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:

View file

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

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

View file

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

View file

@ -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) {
@ -31,6 +32,9 @@ export function updateProject(req, res) {
if (!req.user || !project.user.equals(req.user._id)) {
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,
{
$set: req.body