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 SET_TOAST_TEXT = 'SET_TOAST_TEXT';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
// eventually, handle errors more specifically and better
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 { getBlobUrl } from './files';
import { showToast, setToastText } from './toast';
import { setUnsavedChanges } from './ide';
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
});
getProjectBlobUrls()(dispatch, getState);
dispatch(setUnsavedChanges(false));
})
.catch(response => dispatch({
type: ActionTypes.ERROR,
@ -58,6 +60,7 @@ export function saveProject(autosave) {
if (state.project.id) {
axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
.then(() => {
dispatch(setUnsavedChanges(false));
dispatch({
type: ActionTypes.PROJECT_SAVE_SUCCESS
});
@ -73,6 +76,7 @@ export function saveProject(autosave) {
} else {
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then(response => {
dispatch(setUnsavedChanges(false));
browserHistory.push(`/projects/${response.data.id}`);
dispatch({
type: ActionTypes.NEW_PROJECT,
@ -112,6 +116,7 @@ export function createProject() {
owner: response.data.user,
files: response.data.files
});
dispatch(setUnsavedChanges(false));
})
.catch(response => dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL,

View file

@ -60,33 +60,33 @@ class Editor extends React.Component {
})
}
});
this._cm.on('change', debounce(200, () => {
this.props.setUnsavedChanges(true);
this.props.updateFileContent(this.props.file.name, this._cm.getValue());
}));
this._cm.on('keyup', () => {
const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`;
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) => {
if (e.key === 'Tab' && e.shiftKey) {
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) {
if (this.props.file.content !== prevProps.file.content &&
this.props.file.content !== this._cm.getValue()) {
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) {
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
@ -190,7 +190,8 @@ Editor.propTypes = {
editorOptionsVisible: PropTypes.bool.isRequired,
showEditorOptions: PropTypes.func.isRequired,
closeEditorOptions: PropTypes.func.isRequired,
showKeyboardShortcutModal: PropTypes.func.isRequired
showKeyboardShortcutModal: PropTypes.func.isRequired,
setUnsavedChanges: PropTypes.func.isRequired
};
export default Editor;

View file

@ -14,6 +14,7 @@ import Console from '../components/Console';
import Toast from '../components/Toast';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project';
@ -33,6 +34,7 @@ class IDEView extends React.Component {
this._handleConsolePaneOnDragFinished = this._handleConsolePaneOnDragFinished.bind(this);
this._handleSidebarPaneOnDragFinished = this._handleSidebarPaneOnDragFinished.bind(this);
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
}
componentDidMount() {
@ -55,6 +57,10 @@ class IDEView extends React.Component {
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
document.addEventListener('keydown', this.handleGlobalKeydown, false);
this.props.router.setRouteLeaveHook(this.props.route, () => this.warnIfUnsavedChanges());
window.onbeforeunload = () => this.warnIfUnsavedChanges();
}
componentWillUpdate(nextProps) {
@ -91,6 +97,10 @@ class IDEView extends React.Component {
clearInterval(this.autosaveInterval);
this.autosaveInterval = null;
}
if (this.props.route.path !== prevProps.route.path) {
this.props.router.setRouteLeaveHook(this.props.route, () => this.warnIfUnsavedChanges());
}
}
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() {
return (
<div className="ide">
@ -243,6 +264,7 @@ class IDEView extends React.Component {
showEditorOptions={this.props.showEditorOptions}
closeEditorOptions={this.props.closeEditorOptions}
showKeyboardShortcutModal={this.props.showKeyboardShortcutModal}
setUnsavedChanges={this.props.setUnsavedChanges}
/>
<Console
consoleEvent={this.props.ide.consoleEvent}
@ -380,7 +402,8 @@ IDEView.propTypes = {
newFolderModalVisible: PropTypes.bool.isRequired,
shareModalVisible: PropTypes.bool.isRequired,
editorOptionsVisible: PropTypes.bool.isRequired,
keyboardShortcutVisible: PropTypes.bool.isRequired
keyboardShortcutVisible: PropTypes.bool.isRequired,
unsavedChanges: PropTypes.bool.isRequired
}).isRequired,
startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
@ -464,7 +487,12 @@ IDEView.propTypes = {
}).isRequired,
showToast: 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) {
@ -495,4 +523,4 @@ function mapDispatchToProps(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,
shareModalVisible: false,
editorOptionsVisible: false,
keyboardShortcutVisible: false
keyboardShortcutVisible: false,
unsavedChanges: false
};
const ide = (state = initialState, action) => {
@ -70,6 +71,8 @@ const ide = (state = initialState, action) => {
return Object.assign({}, state, { keyboardShortcutVisible: true });
case ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL:
return Object.assign({}, state, { keyboardShortcutVisible: false });
case ActionTypes.SET_UNSAVED_CHANGES:
return Object.assign({}, state, { unsavedChanges: action.value });
default:
return state;
}