#254 show error when user attempts to save stale version of project, refactor error modals to one component
This commit is contained in:
parent
c8253dd923
commit
a9ee70e033
16 changed files with 123 additions and 156 deletions
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
<Link to="/login" onClick={props.closeModal}>log in</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationError.propTypes = {
|
||||
closeModal: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationError;
|
70
client/modules/IDE/components/ErrorModal.jsx
Normal file
70
client/modules/IDE/components/ErrorModal.jsx
Normal 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
|
||||
<Link to="/login" onClick={this.props.closeModal}>Login</Link>
|
||||
or
|
||||
<Link to="/signup" onClick={this.props.closeModal}>Sign Up</Link>.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
staleSession() {
|
||||
return (
|
||||
<p>
|
||||
It looks like you've been logged out. Please
|
||||
<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;
|
|
@ -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
|
||||
<Link to="/login" onClick={this.props.closeModal}>Login</Link>
|
||||
or
|
||||
<Link to="/signup" onClick={this.props.closeModal}>Sign Up</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ForceAuthentication.propTypes = {
|
||||
closeModal: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ForceAuthentication;
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue