add unsavedChanges to redux, handle unsaved changes

This commit is contained in:
Cassie Tarakajian 2016-09-20 18:27:10 -04:00
parent bd71041794
commit f48b872500
6 changed files with 59 additions and 13 deletions

View file

@ -82,5 +82,7 @@ export const SHOW_TOAST = 'SHOW_TOAST';
export const HIDE_TOAST = 'HIDE_TOAST'; export const HIDE_TOAST = 'HIDE_TOAST';
export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; export const SET_TOAST_TEXT = 'SET_TOAST_TEXT';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
// eventually, handle errors more specifically and better // eventually, handle errors more specifically and better
export const ERROR = 'ERROR'; export const ERROR = 'ERROR';

View file

@ -163,3 +163,10 @@ export function closeKeyboardShortcutModal() {
}; };
} }
export function setUnsavedChanges(value) {
return {
type: ActionTypes.SET_UNSAVED_CHANGES,
value
};
}

View file

@ -6,6 +6,7 @@ import JSZipUtils from 'jszip-utils';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { getBlobUrl } from './files'; import { getBlobUrl } from './files';
import { showToast, setToastText } from './toast'; import { showToast, setToastText } from './toast';
import { setUnsavedChanges } from './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';
@ -32,6 +33,7 @@ export function getProject(id) {
owner: response.data.user owner: response.data.user
}); });
getProjectBlobUrls()(dispatch, getState); getProjectBlobUrls()(dispatch, getState);
dispatch(setUnsavedChanges(false));
}) })
.catch(response => dispatch({ .catch(response => dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
@ -58,6 +60,7 @@ export function saveProject(autosave) {
if (state.project.id) { if (state.project.id) {
axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true }) axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
.then(() => { .then(() => {
dispatch(setUnsavedChanges(false));
dispatch({ dispatch({
type: ActionTypes.PROJECT_SAVE_SUCCESS type: ActionTypes.PROJECT_SAVE_SUCCESS
}); });
@ -73,6 +76,7 @@ export function saveProject(autosave) {
} else { } else {
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then(response => { .then(response => {
dispatch(setUnsavedChanges(false));
browserHistory.push(`/projects/${response.data.id}`); browserHistory.push(`/projects/${response.data.id}`);
dispatch({ dispatch({
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
@ -112,6 +116,7 @@ export function createProject() {
owner: response.data.user, owner: response.data.user,
files: response.data.files files: response.data.files
}); });
dispatch(setUnsavedChanges(false));
}) })
.catch(response => dispatch({ .catch(response => dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL, type: ActionTypes.PROJECT_SAVE_FAIL,

View file

@ -60,33 +60,33 @@ class Editor extends React.Component {
}) })
} }
}); });
this._cm.on('change', debounce(200, () => { this._cm.on('change', debounce(200, () => {
this.props.setUnsavedChanges(true);
this.props.updateFileContent(this.props.file.name, this._cm.getValue()); this.props.updateFileContent(this.props.file.name, this._cm.getValue());
})); }));
this._cm.on('keyup', () => { this._cm.on('keyup', () => {
const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`; const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`;
document.getElementById('current-line').innerHTML = temp; document.getElementById('current-line').innerHTML = temp;
}); });
// this._cm.on('change', () => { // eslint-disable-line
// // this.props.updateFileContent('sketch.js', this._cm.getValue());
// throttle(1000, () => console.log('debounce is working!'));
// this.props.updateFileContent(this.props.file.name, this._cm.getValue());
// });
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
this._cm.setOption('indentWithTabs', this.props.isTabIndent);
this._cm.setOption('tabSize', this.props.indentationAmount);
this._cm.on('keydown', (_cm, e) => { this._cm.on('keydown', (_cm, e) => {
if (e.key === 'Tab' && e.shiftKey) { if (e.key === 'Tab' && e.shiftKey) {
this.tidyCode(); this.tidyCode();
} }
}); });
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
this._cm.setOption('indentWithTabs', this.props.isTabIndent);
this._cm.setOption('tabSize', this.props.indentationAmount);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.file.content !== prevProps.file.content && if (this.props.file.content !== prevProps.file.content &&
this.props.file.content !== this._cm.getValue()) { this.props.file.content !== this._cm.getValue()) {
this._cm.setValue(this.props.file.content); // eslint-disable-line no-underscore-dangle this._cm.setValue(this.props.file.content); // eslint-disable-line no-underscore-dangle
setTimeout(() => this.props.setUnsavedChanges(false), 500);
} }
if (this.props.fontSize !== prevProps.fontSize) { if (this.props.fontSize !== prevProps.fontSize) {
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
@ -190,7 +190,8 @@ Editor.propTypes = {
editorOptionsVisible: PropTypes.bool.isRequired, editorOptionsVisible: PropTypes.bool.isRequired,
showEditorOptions: PropTypes.func.isRequired, showEditorOptions: PropTypes.func.isRequired,
closeEditorOptions: PropTypes.func.isRequired, closeEditorOptions: PropTypes.func.isRequired,
showKeyboardShortcutModal: PropTypes.func.isRequired showKeyboardShortcutModal: PropTypes.func.isRequired,
setUnsavedChanges: PropTypes.func.isRequired
}; };
export default Editor; export default Editor;

View file

@ -14,6 +14,7 @@ import Console from '../components/Console';
import Toast from '../components/Toast'; import Toast from '../components/Toast';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import * as FileActions from '../actions/files'; import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide'; import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
@ -33,6 +34,7 @@ class IDEView extends React.Component {
this._handleConsolePaneOnDragFinished = this._handleConsolePaneOnDragFinished.bind(this); this._handleConsolePaneOnDragFinished = this._handleConsolePaneOnDragFinished.bind(this);
this._handleSidebarPaneOnDragFinished = this._handleSidebarPaneOnDragFinished.bind(this); this._handleSidebarPaneOnDragFinished = this._handleSidebarPaneOnDragFinished.bind(this);
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this); this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -55,6 +57,10 @@ class IDEView extends React.Component {
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
document.addEventListener('keydown', this.handleGlobalKeydown, false); document.addEventListener('keydown', this.handleGlobalKeydown, false);
this.props.router.setRouteLeaveHook(this.props.route, () => this.warnIfUnsavedChanges());
window.onbeforeunload = () => this.warnIfUnsavedChanges();
} }
componentWillUpdate(nextProps) { componentWillUpdate(nextProps) {
@ -91,6 +97,10 @@ class IDEView extends React.Component {
clearInterval(this.autosaveInterval); clearInterval(this.autosaveInterval);
this.autosaveInterval = null; this.autosaveInterval = null;
} }
if (this.props.route.path !== prevProps.route.path) {
this.props.router.setRouteLeaveHook(this.props.route, () => this.warnIfUnsavedChanges());
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -134,6 +144,17 @@ class IDEView extends React.Component {
} }
} }
warnIfUnsavedChanges() { // eslint-disable-line
if (this.props.ide.unsavedChanges
&& ((this.props.project.owner && this.props.project.owner.id === this.props.user.id)
|| (!this.props.project.owner))) {
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
return false;
}
this.props.setUnsavedChanges(false);
}
}
render() { render() {
return ( return (
<div className="ide"> <div className="ide">
@ -243,6 +264,7 @@ class IDEView extends React.Component {
showEditorOptions={this.props.showEditorOptions} showEditorOptions={this.props.showEditorOptions}
closeEditorOptions={this.props.closeEditorOptions} closeEditorOptions={this.props.closeEditorOptions}
showKeyboardShortcutModal={this.props.showKeyboardShortcutModal} showKeyboardShortcutModal={this.props.showKeyboardShortcutModal}
setUnsavedChanges={this.props.setUnsavedChanges}
/> />
<Console <Console
consoleEvent={this.props.ide.consoleEvent} consoleEvent={this.props.ide.consoleEvent}
@ -380,7 +402,8 @@ IDEView.propTypes = {
newFolderModalVisible: PropTypes.bool.isRequired, newFolderModalVisible: PropTypes.bool.isRequired,
shareModalVisible: PropTypes.bool.isRequired, shareModalVisible: PropTypes.bool.isRequired,
editorOptionsVisible: PropTypes.bool.isRequired, editorOptionsVisible: PropTypes.bool.isRequired,
keyboardShortcutVisible: PropTypes.bool.isRequired keyboardShortcutVisible: PropTypes.bool.isRequired,
unsavedChanges: PropTypes.bool.isRequired
}).isRequired, }).isRequired,
startSketch: PropTypes.func.isRequired, startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,
@ -464,7 +487,12 @@ IDEView.propTypes = {
}).isRequired, }).isRequired,
showToast: PropTypes.func.isRequired, showToast: PropTypes.func.isRequired,
setToastText: PropTypes.func.isRequired, setToastText: PropTypes.func.isRequired,
autosaveProject: PropTypes.func.isRequired autosaveProject: PropTypes.func.isRequired,
router: PropTypes.shape({
setRouteLeaveHook: PropTypes.func
}).isRequired,
route: PropTypes.object.isRequired,
setUnsavedChanges: PropTypes.func.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
@ -495,4 +523,4 @@ function mapDispatchToProps(dispatch) {
dispatch); dispatch);
} }
export default connect(mapStateToProps, mapDispatchToProps)(IDEView); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView));

View file

@ -15,7 +15,8 @@ const initialState = {
newFolderModalVisible: false, newFolderModalVisible: false,
shareModalVisible: false, shareModalVisible: false,
editorOptionsVisible: false, editorOptionsVisible: false,
keyboardShortcutVisible: false keyboardShortcutVisible: false,
unsavedChanges: false
}; };
const ide = (state = initialState, action) => { const ide = (state = initialState, action) => {
@ -70,6 +71,8 @@ const ide = (state = initialState, action) => {
return Object.assign({}, state, { keyboardShortcutVisible: true }); return Object.assign({}, state, { keyboardShortcutVisible: true });
case ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL: case ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL:
return Object.assign({}, state, { keyboardShortcutVisible: false }); return Object.assign({}, state, { keyboardShortcutVisible: false });
case ActionTypes.SET_UNSAVED_CHANGES:
return Object.assign({}, state, { unsavedChanges: action.value });
default: default:
return state; return state;
} }