From 095924256ac6ecd537e8715ba5ed9ca03a4a9d77 Mon Sep 17 00:00:00 2001 From: Ray Schamp <ray@scratch.mit.edu> Date: Mon, 26 Nov 2018 19:21:15 -0500 Subject: [PATCH] Track "project changed" state in Redux Listen to the new project changed event from the VM and keep track of the state in Redux. Update it as appropriate. Known issue is that the project is marked as "changed" immediately after it's loaded. This is due to asynchronous event emitting while the project data is loaded, and needs an update to the VM to fix. --- src/containers/blocks.jsx | 5 ++++- src/lib/project-fetcher-hoc.jsx | 9 ++++++++- src/lib/project-saver-hoc.jsx | 7 ++++++- src/lib/vm-listener-hoc.jsx | 8 ++++++++ src/lib/vm-manager-hoc.jsx | 5 ++++- src/reducers/gui.js | 3 +++ src/reducers/project-changed.js | 28 ++++++++++++++++++++++++++++ 7 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/reducers/project-changed.js diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index e038790e4..7c757f392 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -328,7 +328,10 @@ class Blocks extends React.Component { error.message = `Workspace Update Error: ${error.message}`; log.error(error); } - this.workspace.addChangeListener(this.props.vm.blockListener); + // All of the changes that happened during the load above are queued with + // timeouts, so re-enable the listener in the next tick, so it happens after + // the events are already fired. + setTimeout(() => this.workspace.addChangeListener(this.props.vm.blockListener)); if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) { const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id]; diff --git a/src/lib/project-fetcher-hoc.jsx b/src/lib/project-fetcher-hoc.jsx index ca847fe3f..1879d69e5 100644 --- a/src/lib/project-fetcher-hoc.jsx +++ b/src/lib/project-fetcher-hoc.jsx @@ -4,10 +4,12 @@ import {intlShape, injectIntl} from 'react-intl'; import bindAll from 'lodash.bindall'; import {connect} from 'react-redux'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, defaultProjectId, getIsFetchingWithId, + getIsShowingProject, onFetchedProjectData, projectError, setProjectId @@ -54,6 +56,9 @@ const ProjectFetcherHOC = function (WrappedComponent) { if (this.props.isFetchingWithId && !prevProps.isFetchingWithId) { this.fetchProject(this.props.reduxProjectId, this.props.loadingState); } + if (this.props.isShowingProject && !prevProps.isShowingProject) { + this.props.onProjectLoaded(); + } } fetchProject (projectId, loadingState) { return storage @@ -123,6 +128,7 @@ const ProjectFetcherHOC = function (WrappedComponent) { const mapStateToProps = state => ({ isFetchingWithId: getIsFetchingWithId(state.scratchGui.projectState.loadingState), + isShowingProject: getIsShowingProject(state.scratchGui.projectState.loadingState), loadingState: state.scratchGui.projectState.loadingState, reduxProjectId: state.scratchGui.projectState.projectId }); @@ -130,7 +136,8 @@ const ProjectFetcherHOC = function (WrappedComponent) { onError: error => dispatch(projectError(error)), onFetchedProjectData: (projectData, loadingState) => dispatch(onFetchedProjectData(projectData, loadingState)), - setProjectId: projectId => dispatch(setProjectId(projectId)) + setProjectId: projectId => dispatch(setProjectId(projectId)), + onProjectLoaded: () => dispatch(setProjectUnchanged()) }); // Allow incoming props to override redux-provided props. Used to mock in tests. const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( diff --git a/src/lib/project-saver-hoc.jsx b/src/lib/project-saver-hoc.jsx index 3fbbad361..df0d6fa6f 100644 --- a/src/lib/project-saver-hoc.jsx +++ b/src/lib/project-saver-hoc.jsx @@ -9,6 +9,7 @@ import { showAlertWithTimeout, showStandardAlert } from '../reducers/alerts'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, autoUpdateProject, @@ -167,7 +168,10 @@ const ProjectSaverHOC = function (WrappedComponent) { storage.DataFormat.JSON, body, projectId - ); + ).then(response => { + this.props.onSetProjectUnchanged(); + return response; + }); }) .catch(err => { log.error(err); @@ -255,6 +259,7 @@ const ProjectSaverHOC = function (WrappedComponent) { onCreatedProject: (projectId, loadingState) => dispatch(doneCreatingProject(projectId, loadingState)), onCreateProject: () => dispatch(createProject()), onProjectError: error => dispatch(projectError(error)), + onSetProjectUnchanged: () => dispatch(setProjectUnchanged()), onShowAlert: alertType => dispatch(showStandardAlert(alertType)), onShowCreateSuccessAlert: () => showAlertWithTimeout(dispatch, 'createSuccess'), onShowCreatingAlert: () => showAlertWithTimeout(dispatch, 'creating'), diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx index 675deb04c..3e3686571 100644 --- a/src/lib/vm-listener-hoc.jsx +++ b/src/lib/vm-listener-hoc.jsx @@ -8,6 +8,7 @@ import {connect} from 'react-redux'; import {updateTargets} from '../reducers/targets'; import {updateBlockDrag} from '../reducers/block-drag'; import {updateMonitors} from '../reducers/monitors'; +import {setProjectChanged, setProjectUnchanged} from '../reducers/project-changed'; import {setRunningState, setTurboState, setStartedState} from '../reducers/vm-status'; import {showExtensionAlert} from '../reducers/alerts'; import {updateMicIndicator} from '../reducers/mic-indicator'; @@ -24,6 +25,7 @@ const vmListenerHOC = function (WrappedComponent) { bindAll(this, [ 'handleKeyDown', 'handleKeyUp', + 'handleProjectChanged', 'handleTargetsUpdate' ]); // We have to start listening to the vm here rather than in @@ -39,6 +41,7 @@ const vmListenerHOC = function (WrappedComponent) { this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff); this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart); this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop); + this.props.vm.on('PROJECT_CHANGED', this.handleProjectChanged); this.props.vm.on('RUNTIME_STARTED', this.props.onRuntimeStarted); this.props.vm.on('PERIPHERAL_DISCONNECT_ERROR', this.props.onShowExtensionAlert); this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate); @@ -69,6 +72,9 @@ const vmListenerHOC = function (WrappedComponent) { document.removeEventListener('keyup', this.handleKeyUp); } } + handleProjectChanged () { + this.props.onProjectChanged(); + } handleTargetsUpdate (data) { if (this.props.shouldEmitTargetsUpdate) { this.props.onTargetsUpdate(data); @@ -167,6 +173,8 @@ const vmListenerHOC = function (WrappedComponent) { }, onProjectRunStart: () => dispatch(setRunningState(true)), onProjectRunStop: () => dispatch(setRunningState(false)), + onProjectChanged: () => dispatch(setProjectChanged()), + onProjectSaved: () => dispatch(setProjectUnchanged()), onRuntimeStarted: () => dispatch(setStartedState(true)), onTurboModeOn: () => dispatch(setTurboState(true)), onTurboModeOff: () => dispatch(setTurboState(false)), diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx index 981fe8326..d9efcd4de 100644 --- a/src/lib/vm-manager-hoc.jsx +++ b/src/lib/vm-manager-hoc.jsx @@ -6,6 +6,7 @@ import {connect} from 'react-redux'; import VM from 'scratch-vm'; import AudioEngine from 'scratch-audio'; +import {setProjectUnchanged} from '../reducers/project-changed'; import { LoadingStates, getIsLoadingWithId, @@ -53,6 +54,7 @@ const vmManagerHOC = function (WrappedComponent) { return this.props.vm.loadProject(this.props.projectData) .then(() => { this.props.onLoadedProject(this.props.loadingState, this.props.canSave); + this.props.onSetProjectUnchanged(); // If the vm is not running, call draw on the renderer manually // This draws the state of the loaded project with no blocks running @@ -123,7 +125,8 @@ const vmManagerHOC = function (WrappedComponent) { const mapDispatchToProps = dispatch => ({ onError: error => dispatch(projectError(error)), onLoadedProject: (loadingState, canSave) => - dispatch(onLoadedProject(loadingState, canSave)) + dispatch(onLoadedProject(loadingState, canSave)), + onSetProjectUnchanged: () => dispatch(setProjectUnchanged()) }); // Allow incoming props to override redux-provided props. Used to mock in tests. diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 6ae91b95a..edffa1cf3 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -14,6 +14,7 @@ import modalReducer, {modalsInitialState} from './modals'; import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout'; +import projectChangedReducer, {projectChangedInitialState} from './project-changed'; import projectStateReducer, {projectStateInitialState} from './project-state'; import projectTitleReducer, {projectTitleInitialState} from './project-title'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; @@ -45,6 +46,7 @@ const guiInitialState = { modals: modalsInitialState, monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, + projectChanged: projectChangedInitialState, projectState: projectStateInitialState, projectTitle: projectTitleInitialState, restoreDeletion: restoreDeletionInitialState, @@ -122,6 +124,7 @@ const guiReducer = combineReducers({ modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer, + projectChanged: projectChangedReducer, projectState: projectStateReducer, projectTitle: projectTitleReducer, restoreDeletion: restoreDeletionReducer, diff --git a/src/reducers/project-changed.js b/src/reducers/project-changed.js new file mode 100644 index 000000000..c59f6a107 --- /dev/null +++ b/src/reducers/project-changed.js @@ -0,0 +1,28 @@ +const SET_PROJECT_CHANGED = 'scratch-gui/project-changed/SET_PROJECT_CHANGED'; + +const initialState = false; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_PROJECT_CHANGED: + return action.changed; + default: + return state; + } +}; +const setProjectChanged = () => ({ + type: SET_PROJECT_CHANGED, + changed: true +}); +const setProjectUnchanged = () => ({ + type: SET_PROJECT_CHANGED, + changed: false +}); + +export { + reducer as default, + initialState as projectChangedInitialState, + setProjectChanged, + setProjectUnchanged +}; -- GitLab