add unsavedChanges to redux, handle unsaved changes
This commit is contained in:
parent
bd71041794
commit
f48b872500
6 changed files with 59 additions and 13 deletions
|
@ -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';
|
||||||
|
|
|
@ -163,3 +163,10 @@ export function closeKeyboardShortcutModal() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setUnsavedChanges(value) {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.SET_UNSAVED_CHANGES,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue