Skip to content
Snippets Groups Projects
Unverified Commit bcca5bf1 authored by Benjamin Wheeler's avatar Benjamin Wheeler Committed by GitHub
Browse files

Merge pull request #3856 from benjiwheeler/autosave-draft

autosave periodically when project is dirty
parents ede6d2fe 5d36b584
No related branches found
No related tags found
No related merge requests found
import bindAll from 'lodash.bindall';
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
......@@ -9,6 +10,7 @@ import {
showAlertWithTimeout,
showStandardAlert
} from '../reducers/alerts';
import {setAutoSaveTimeoutId} from '../reducers/timeout';
import {setProjectUnchanged} from '../reducers/project-changed';
import {
LoadingStates,
......@@ -37,7 +39,16 @@ import {
*/
const ProjectSaverHOC = function (WrappedComponent) {
class ProjectSaverComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'tryToAutoSave'
]);
}
componentDidUpdate (prevProps) {
if (this.props.projectChanged && !prevProps.projectChanged) {
this.scheduleAutoSave();
}
if (this.props.isUpdating && !prevProps.isUpdating) {
this.updateProjectToStorage();
}
......@@ -65,10 +76,30 @@ const ProjectSaverHOC = function (WrappedComponent) {
// don't try to save immediately after trying to save
if (prevProps.isUpdating) return;
// if we're newly able to save this project, save it!
const showingSaveable = this.props.canSave && this.props.isShowingWithId;
const becameAbleToSave = this.props.canSave && !prevProps.canSave;
const becameShared = this.props.isShared && !prevProps.isShared;
if (showingSaveable && (becameAbleToSave || becameShared)) {
if (this.props.isShowingSaveable && (becameAbleToSave || becameShared)) {
this.props.onAutoUpdateProject();
}
}
componentWillUnmount () {
this.clearAutoSaveTimeout();
}
clearAutoSaveTimeout () {
if (this.props.autoSaveTimeoutId !== null) {
clearTimeout(this.props.autoSaveTimeoutId);
this.props.setAutoSaveTimeoutId(null);
}
}
scheduleAutoSave () {
if (this.props.isShowingSaveable && this.props.autoSaveTimeoutId === null) {
const timeoutId = setTimeout(this.tryToAutoSave,
this.props.autosaveIntervalSecs * 1000);
this.props.setAutoSaveTimeoutId(timeoutId);
}
}
tryToAutoSave () {
if (this.props.projectChanged && this.props.isShowingSaveable) {
this.props.onAutoUpdateProject();
}
}
......@@ -143,6 +174,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
*/
storeProject (projectId, requestParams) {
requestParams = requestParams || {};
this.clearAutoSaveTimeout();
return Promise.all(this.props.vm.assets
.filter(asset => !asset.clean)
.map(
......@@ -180,10 +212,13 @@ const ProjectSaverHOC = function (WrappedComponent) {
render () {
const {
/* eslint-disable no-unused-vars */
autosaveIntervalSecs,
isCreatingCopy,
isCreatingNew,
projectChanged,
isManualUpdating,
isRemixing,
isShowingSaveable,
isShowingWithId,
isShowingWithoutId,
isUpdating,
......@@ -200,6 +235,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
onUpdatedProject,
reduxProjectId,
reduxProjectTitle,
setAutoSaveTimeoutId: setAutoSaveTimeoutIdProp,
/* eslint-enable no-unused-vars */
...componentProps
} = this.props;
......@@ -212,6 +248,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
}
ProjectSaverComponent.propTypes = {
autoSaveTimeoutId: PropTypes.number,
canCreateNew: PropTypes.bool,
canSave: PropTypes.bool,
isCreatingCopy: PropTypes.bool,
......@@ -219,6 +256,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
isManualUpdating: PropTypes.bool,
isRemixing: PropTypes.bool,
isShared: PropTypes.bool,
isShowingSaveable: PropTypes.bool,
isShowingWithId: PropTypes.bool,
isShowingWithoutId: PropTypes.bool,
isUpdating: PropTypes.bool,
......@@ -233,21 +271,29 @@ const ProjectSaverHOC = function (WrappedComponent) {
onShowSaveSuccessAlert: PropTypes.func,
onShowSavingAlert: PropTypes.func,
onUpdatedProject: PropTypes.func,
projectChanged: PropTypes.bool,
reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
reduxProjectTitle: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
};
const mapStateToProps = state => {
ProjectSaverComponent.defaultProps = {
autosaveIntervalSecs: 120
};
const mapStateToProps = (state, ownProps) => {
const loadingState = state.scratchGui.projectState.loadingState;
const isShowingWithId = getIsShowingWithId(loadingState);
return {
autoSaveTimeoutId: state.scratchGui.timeout.autoSaveTimeoutId,
isCreatingCopy: getIsCreatingCopy(loadingState),
isCreatingNew: getIsCreatingNew(loadingState),
isRemixing: getIsRemixing(loadingState),
isShowingWithId: getIsShowingWithId(loadingState),
isShowingSaveable: ownProps.canSave && isShowingWithId,
isShowingWithId: isShowingWithId,
isShowingWithoutId: getIsShowingWithoutId(loadingState),
isUpdating: getIsUpdating(loadingState),
isManualUpdating: getIsManualUpdating(loadingState),
loadingState: loadingState,
projectChanged: state.scratchGui.projectChanged,
reduxProjectId: state.scratchGui.projectState.projectId,
reduxProjectTitle: state.scratchGui.projectTitle,
vm: state.scratchGui.vm
......@@ -264,7 +310,8 @@ const ProjectSaverHOC = function (WrappedComponent) {
onShowCreatingAlert: () => showAlertWithTimeout(dispatch, 'creating'),
onShowSaveSuccessAlert: () => showAlertWithTimeout(dispatch, 'saveSuccess'),
onShowSavingAlert: () => showAlertWithTimeout(dispatch, 'saving'),
onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState))
onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState)),
setAutoSaveTimeoutId: id => dispatch(setAutoSaveTimeoutId(id))
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
......
......@@ -20,6 +20,7 @@ import projectTitleReducer, {projectTitleInitialState} from './project-title';
import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion';
import stageSizeReducer, {stageSizeInitialState} from './stage-size';
import targetReducer, {targetsInitialState} from './targets';
import timeoutReducer, {timeoutInitialState} from './timeout';
import toolboxReducer, {toolboxInitialState} from './toolbox';
import vmReducer, {vmInitialState} from './vm';
import vmStatusReducer, {vmStatusInitialState} from './vm-status';
......@@ -51,6 +52,7 @@ const guiInitialState = {
projectTitle: projectTitleInitialState,
restoreDeletion: restoreDeletionInitialState,
targets: targetsInitialState,
timeout: timeoutInitialState,
toolbox: toolboxInitialState,
vm: vmInitialState,
vmStatus: vmStatusInitialState
......@@ -133,6 +135,7 @@ const guiReducer = combineReducers({
projectTitle: projectTitleReducer,
restoreDeletion: restoreDeletionReducer,
targets: targetReducer,
timeout: timeoutReducer,
toolbox: toolboxReducer,
vm: vmReducer,
vmStatus: vmStatusReducer
......
const SET_AUTOSAVE_TIMEOUT_ID = 'timeout/SET_AUTOSAVE_TIMEOUT_ID';
const initialState = {
autoSaveTimeoutId: null
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case SET_AUTOSAVE_TIMEOUT_ID:
return Object.assign({}, state, {
autoSaveTimeoutId: action.id
});
default:
return state;
}
};
const setAutoSaveTimeoutId = id => ({
type: SET_AUTOSAVE_TIMEOUT_ID,
id
});
export {
reducer as default,
initialState as timeoutInitialState,
setAutoSaveTimeoutId
};
......@@ -16,10 +16,16 @@ describe('projectSaverHOC', () => {
beforeEach(() => {
store = mockStore({
scratchGui: {
projectState: {}
projectChanged: false,
projectState: {},
projectTitle: 'Scratch Project',
timeout: {
autoSaveTimeoutId: null
}
}
});
vm = new VM();
jest.useFakeTimers();
});
test('if canSave becomes true when showing a project with an id, project will be saved', () => {
......@@ -31,6 +37,7 @@ describe('projectSaverHOC', () => {
isShowingWithId
canSave={false}
isCreatingNew={false}
isShowingSaveable={false} // set explicitly because it relies on ownProps.canSave
isShowingWithoutId={false}
isUpdating={false}
loadingState={LoadingState.SHOWING_WITH_ID}
......@@ -40,7 +47,8 @@ describe('projectSaverHOC', () => {
/>
);
mounted.setProps({
canSave: true
canSave: true,
isShowingSaveable: true
});
expect(mockedUpdateProject).toHaveBeenCalled();
});
......@@ -313,4 +321,82 @@ describe('projectSaverHOC', () => {
});
expect(mockedShowSavingAlert).toHaveBeenCalled();
});
test('if project is changed, it should autosave after interval', () => {
const Component = () => <div />;
const WrappedComponent = projectSaverHOC(Component);
const mockedAutoUpdate = jest.fn(() => Promise.resolve());
const mounted = mount(
<WrappedComponent
canSave
isShowingSaveable
isShowingWithId
loadingState={LoadingState.SHOWING_WITH_ID}
store={store}
vm={vm}
onAutoUpdateProject={mockedAutoUpdate}
/>
);
mounted.setProps({
projectChanged: true
});
// Fast-forward until all timers have been executed
jest.runAllTimers();
expect(mockedAutoUpdate).toHaveBeenCalled();
});
test('if project is changed several times in a row, it should only autosave once', () => {
const Component = () => <div />;
const WrappedComponent = projectSaverHOC(Component);
const mockedAutoUpdate = jest.fn(() => Promise.resolve());
const mounted = mount(
<WrappedComponent
canSave
isShowingSaveable
isShowingWithId
loadingState={LoadingState.SHOWING_WITH_ID}
store={store}
vm={vm}
onAutoUpdateProject={mockedAutoUpdate}
/>
);
mounted.setProps({
projectChanged: true,
reduxProjectTitle: 'a'
});
mounted.setProps({
projectChanged: true,
reduxProjectTitle: 'b'
});
mounted.setProps({
projectChanged: true,
reduxProjectTitle: 'c'
});
// Fast-forward until all timers have been executed
jest.runAllTimers();
expect(mockedAutoUpdate).toHaveBeenCalledTimes(1);
});
test('if project is not changed, it should not autosave after interval', () => {
const Component = () => <div />;
const WrappedComponent = projectSaverHOC(Component);
const mockedAutoUpdate = jest.fn(() => Promise.resolve());
const mounted = mount(
<WrappedComponent
canSave
isShowingSaveable
isShowingWithId
loadingState={LoadingState.SHOWING_WITH_ID}
store={store}
vm={vm}
onAutoUpdateProject={mockedAutoUpdate}
/>
);
mounted.setProps({
projectChanged: false
});
// Fast-forward until all timers have been executed
jest.runAllTimers();
expect(mockedAutoUpdate).not.toHaveBeenCalled();
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment