major refactor to introduce project id state machine

This commit is contained in:
Ben Wheeler committed 2018-10-08 18:02:56 -04:00
1 parent a7c5d60afb
commit 676b3fc278
11 files changed
+522 -179

No files matched your search

+68 -35
View File
@@ -21,6 +21,7 @@ import DeletionRestorer from '../../containers/deletion-restorer.jsx';
import TurboMode from '../../containers/turbo-mode.jsx';
import {openTipsLibrary} from '../../reducers/modals';
import {ProjectState, stepTowardsNewProject} from '../../reducers/project-id';
import {setPlayer} from '../../reducers/mode';
import {
openAccountMenu,
@@ -131,6 +132,21 @@ class MenuBar extends React.Component {
]);
this.state = {projectSaveInProgress: false};
}
componentWillReceiveProps (newProps) {
// if we're no longer showing the project (loading, or whatever), close menus
if (!newProps.isShowingProject && this.props.isShowingProject) {
this.props.onRequestCloseFile();
this.props.onRequestCloseEdit();
}
// if we're now saving, show save in progress
if (newProps.isSavingWithId && !this.props.isSavingWithId) {
this.setState({projectSaveInProgress: true});
}
// if we're no longer saving, don't show save in progress
if (!newProps.isSavingWithId && this.props.isSavingWithId) {
this.setState({projectSaveInProgress: false});
}
}
handleLanguageMouseUp (e) {
if (!this.props.languageMenuOpen) {
this.props.onClickLanguage(e);
@@ -142,23 +158,27 @@ class MenuBar extends React.Component {
this.props.onRequestCloseEdit();
};
}
// NOTE: not using updateFun currently
// NOTE: need some sort of reducer or some place to do saves, besides a component.
// then it can listen for state changes into saving, and do them.
handleNewProject (updateFun) {
return () => {
this.props.onRequestCloseFile();
if (this.props.userOwnsProject) {
this.setState({projectSaveInProgress: true},
() => {
updateFun().then(() => {
this.props.onRequestNewProject(() => {
this.setState({projectSaveInProgress: false});
});
});
}
);
} else {
this.props.onRequestNewProject();
}
this.props.stepTowardsNewProject();
};
// if (this.props.userOwnsProject) {
// this.setState({projectSaveInProgress: true},
// () => {
// updateFun().then(() => {
// this.props.onRequestNewProject(() => {
// this.setState({projectSaveInProgress: false});
// });
// });
// }
// );
// } else {
// this.props.onRequestNewProject();
// }
// };
}
handleUpdateProject (updateFun) {
return () => {
@@ -284,11 +304,12 @@ class MenuBar extends React.Component {
place={this.props.isRtl ? 'left' : 'right'}
onRequestClose={this.props.onRequestCloseFile}
>
<ProjectSaver onSaveProject={this.handleProjectLoadFinished}>
{(saveProject, updateProject, createProject) => (
{/* <ProjectSaver onSaveProject={this.handleProjectLoadFinished}>
{(downloadProject, updateProject, createProject) => ( */}
<MenuItem
isRtl={this.props.isRtl}
onClick={this.handleNewProject(createProject)}
onClick={this.handleNewProject()}
// onClick={this.handleNewProject(createProject)}
>
<FormattedMessage
defaultMessage="New"
@@ -296,10 +317,10 @@ class MenuBar extends React.Component {
id="gui.menuBar.new"
/>
</MenuItem>
)}
</ProjectSaver>
{/* )}
</ProjectSaver> */}
<MenuSection>
<ProjectSaver>{(saveProject, updateProject) => (
<ProjectSaver>{(downloadProject, updateProject) => (
this.props.canUpdateProject && this.props.userOwnsProject ? (
<MenuItem onClick={this.handleUpdateProject(updateProject)}>
{saveNowMessage}
@@ -345,9 +366,9 @@ class MenuBar extends React.Component {
</MenuItem>
)}
</ProjectLoader>
<ProjectSaver>{saveProject => (
<ProjectSaver>{downloadProject => (
<MenuItem
onClick={this.handleCloseFileMenuAndThen(saveProject)}
onClick={this.handleCloseFileMenuAndThen(downloadProject)}
>
<FormattedMessage
defaultMessage="Save to your computer"
@@ -622,6 +643,8 @@ MenuBar.propTypes = {
fileMenuOpen: PropTypes.bool,
intl: intlShape,
isRtl: PropTypes.bool,
isSavingWithId: PropTypes.bool,
isShowingProject: PropTypes.bool,
languageMenuOpen: PropTypes.bool,
loginMenuOpen: PropTypes.bool,
onClickAccount: PropTypes.func,
@@ -643,22 +666,31 @@ MenuBar.propTypes = {
onUpdateProjectTitle: PropTypes.func,
renderLogin: PropTypes.func,
sessionExists: PropTypes.bool,
stepTowardsNewProject: PropTypes.func,
userOwnsProject: PropTypes.bool,
username: PropTypes.string
};
const mapStateToProps = state => ({
canUpdateProject: typeof (state.session && state.session.session && state.session.session.user) !== 'undefined',
accountMenuOpen: accountMenuOpen(state),
fileMenuOpen: fileMenuOpen(state),
editMenuOpen: editMenuOpen(state),
isRtl: state.locales.isRtl,
languageMenuOpen: languageMenuOpen(state),
loginMenuOpen: loginMenuOpen(state),
sessionExists: state.session && typeof state.session.session !== 'undefined',
username: state.session && state.session.session && state.session.session.user ?
state.session.session.user.username : null
});
const mapStateToProps = state => {
const projectState = state.scratchGui.projectId.projectState;
const user = state.session && state.session.session && state.session.session.user;
return {
canUpdateProject: typeof user !== 'undefined', // NOTE: this is wrong, correct??
accountMenuOpen: accountMenuOpen(state),
fileMenuOpen: fileMenuOpen(state),
editMenuOpen: editMenuOpen(state),
isRtl: state.locales.isRtl,
isSavingWithId: projectState === ProjectState.SAVING_WITH_ID ||
projectState === ProjectState.SAVING_WITH_ID_BEFORE_NEW,
isShowingProject: projectState === ProjectState.SHOWING_WITH_ID ||
projectState === ProjectState.SHOWING_FILE_UPLOAD ||
projectState === ProjectState.SHOWING_NEW_DEFAULT,
languageMenuOpen: languageMenuOpen(state),
loginMenuOpen: loginMenuOpen(state),
sessionExists: state.session && typeof state.session.session !== 'undefined',
username: user ? user.username : null
};
};
const mapDispatchToProps = dispatch => ({
onOpenTipLibrary: () => dispatch(openTipsLibrary()),
@@ -672,7 +704,8 @@ const mapDispatchToProps = dispatch => ({
onRequestCloseLanguage: () => dispatch(closeLanguageMenu()),
onClickLogin: () => dispatch(openLoginMenu()),
onRequestCloseLogin: () => dispatch(closeLoginMenu()),
onSeeCommunity: () => dispatch(setPlayer(true))
onSeeCommunity: () => dispatch(setPlayer(true)),
stepTowardsNewProject: () => dispatch(stepTowardsNewProject())
});
export default injectIntl(connect(
+6 -3
View File
@@ -20,15 +20,16 @@ import {
} from '../reducers/modals';
import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx';
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
import vmManagerHOC from '../lib/vm-manager-hoc.jsx';
import GUIComponent from '../components/gui/gui.jsx';
class GUI extends React.Component {
constructor (props) {
super(props);
}
// constructor (props) {
// super(props);
// }
componentDidMount () {
if (this.props.projectTitle) {
this.props.onUpdateReduxProjectTitle(this.props.projectTitle);
@@ -86,6 +87,7 @@ GUI.propTypes = {
onUpdateProjectTitle: PropTypes.func,
onUpdateReduxProjectTitle: PropTypes.func,
previewInfoVisible: PropTypes.bool,
projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
projectHost: PropTypes.string,
projectTitle: PropTypes.string
};
@@ -129,6 +131,7 @@ const ConnectedGUI = connect(
const WrappedGui = compose(
ErrorBoundaryHOC('Top Level App'),
ProjectLoaderHOC,
ProjectSaverHOC,
vmListenerHOC,
vmManagerHOC
)(ConnectedGUI);
+11 -2
View File
@@ -1,3 +1,5 @@
// NOTE: rename this or project-loader-hoc to make clear they describe mutually exclusive functionality and states?
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
@@ -7,6 +9,7 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl';
import analytics from '../lib/analytics';
import log from '../lib/log';
import {setProjectTitle} from '../reducers/project-title';
import {startLoadingFileUpload, doneLoadingFileUpload} from '../reducers/project-id';
import {
openLoadingProject,
@@ -134,9 +137,15 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
closeLoadingState: () => dispatch(closeLoadingProject()),
closeLoadingState: () => {
dispatch(doneLoadingFileUpload());
dispatch(closeLoadingProject());
},
onSetReduxProjectTitle: title => dispatch(setProjectTitle(title)),
openLoadingState: () => dispatch(openLoadingProject())
openLoadingState: () => {
dispatch(openLoadingProject());
dispatch(startLoadingFileUpload());
}
});
export default connect(
+52 -40
View File
@@ -2,20 +2,22 @@ import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import storage from '../lib/storage';
// import storage from '../lib/storage';
import {projectTitleInitialState} from '../reducers/project-title';
import {ProjectState} from '../reducers/project-id';
import {doStoreProject} from '../reducers/vm';
/**
* Project saver component passes a saveProject function to its child.
* Project saver component passes a downloadProject function to its child.
* It expects this child to be a function with the signature
* function (saveProject, props) {}
* function (downloadProject, props) {}
* The component can then be used to attach project saving functionality
* to any other component:
*
* <ProjectSaver>{(saveProject, props) => (
* <ProjectSaver>{(downloadProject, props) => (
* <MyCoolComponent
* onClick={saveProject}
* onClick={downloadProject}
* {...props}
* />
* )}</ProjectSaver>
@@ -26,13 +28,12 @@ class ProjectSaver extends React.Component {
bindAll(this, [
'createProject',
'updateProject',
'saveProject',
'doStoreProject'
'downloadProject'
]);
}
saveProject () {
const saveLink = document.createElement('a');
document.body.appendChild(saveLink);
downloadProject () {
const downloadLink = document.createElement('a');
document.body.appendChild(downloadLink);
this.props.saveProjectSb3().then(content => {
if (this.props.onSaveFinished) {
@@ -45,43 +46,49 @@ class ProjectSaver extends React.Component {
}
const url = window.URL.createObjectURL(content);
saveLink.href = url;
saveLink.download = this.props.projectFilename;
saveLink.click();
downloadLink.href = url;
downloadLink.download = this.props.projectFilename;
downloadLink.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(saveLink);
document.body.removeChild(downloadLink);
});
}
doStoreProject (id) {
return this.props.saveProjectSb3()
.then(content => {
if (this.props.onSaveFinished) {
this.props.onSaveFinished();
}
const assetType = storage.AssetType.Project;
const dataFormat = storage.DataFormat.SB3;
const body = new FormData();
body.append('sb3_file', content, 'sb3_file');
return storage.store(
assetType,
dataFormat,
body,
id
);
});
}
// doStoreProject (id) {
// return this.props.saveProjectSb3()
// .then(content => {
// if (this.props.onSaveFinished) {
// this.props.onSaveFinished();
// }
// const assetType = storage.AssetType.Project;
// const dataFormat = storage.DataFormat.SB3;
// const body = new FormData();
// body.append('sb3_file', content, 'sb3_file');
// return storage.store(
// assetType,
// dataFormat,
// body,
// id
// );
// });
// }
createProject () {
return this.doStoreProject();
if (this.props.isShowingWithId) {
return doStoreProject(null, this.props.onSaveFinished);
}
return Promise.reject();
}
updateProject () {
return this.doStoreProject(this.props.projectId);
if (this.props.isShowingWithId) {
return doStoreProject(this.props.projectId, this.props.onSaveFinished);
}
return Promise.reject();
}
render () {
const {
children
} = this.props;
return children(
this.saveProject,
this.downloadProject,
this.updateProject,
this.createProject
);
@@ -98,17 +105,22 @@ const getProjectFilename = (curTitle, defaultTitle) => {
ProjectSaver.propTypes = {
children: PropTypes.func,
isShowingWithId: PropTypes.bool,
onSaveFinished: PropTypes.func,
projectFilename: PropTypes.string,
projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
saveProjectSb3: PropTypes.func
};
const mapStateToProps = state => ({
saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm),
projectFilename: getProjectFilename(state.scratchGui.projectTitle, projectTitleInitialState),
projectId: state.scratchGui.projectId
});
const mapStateToProps = state => {
const projectState = state.scratchGui.projectId.projectState;
return {
isShowingWithId: projectState === ProjectState.SHOWING_WITH_ID,
saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm),
projectFilename: getProjectFilename(state.scratchGui.projectTitle, projectTitleInitialState),
projectId: state.scratchGui.projectId.projectId
};
};
export default connect(
mapStateToProps,
+45 -20
View File
@@ -1,7 +1,9 @@
import React from 'react';
import bindAll from 'lodash.bindall';
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {defaultProjectId} from '../reducers/project-id';
import {ProjectState, defaultProjectId, setHashProjectId} from '../reducers/project-id';
/* Higher Order Component to get the project id from location.hash
* @param {React.Component} WrappedComponent component to receive projectData prop
@@ -15,46 +17,69 @@ const HashParserHOC = function (WrappedComponent) {
'handleHashChange',
'handleRequestNewProject'
]);
this.state = {
projectId: null
};
}
componentDidMount () {
window.addEventListener('hashchange', this.handleHashChange);
this.handleHashChange();
}
componentWillReceiveProps (nextProps) {
// if we are newly loading a non-hash project...
if (nextProps.isLoadingNonHashProject && !this.props.isLoadingNonHashProject) {
// ...clear the hash from the url
history.pushState('new-project', 'new-project',
window.location.pathname + window.location.search);
}
}
componentWillUnmount () {
window.removeEventListener('hashchange', this.handleHashChange);
}
handleHashChange () {
const hashMatch = window.location.hash.match(/#(\d+)/);
const projectId = hashMatch === null ? defaultProjectId : hashMatch[1];
if (projectId !== this.state.projectId) {
this.setState({projectId: projectId});
const hashProjectId = hashMatch === null ? defaultProjectId : hashMatch[1];
if (hashProjectId !== this.props.reduxProjectId) {
this.props.setHashProjectId(hashProjectId);
}
}
handleRequestNewProject (callback) {
// unset the projectId
this.setState({projectId: defaultProjectId}, () => {
// strip the hash tag from the url
history.pushState('new-project', 'new-project',
window.location.pathname + window.location.search);
if (callback) callback(defaultProjectId);
});
}
// handleRequestNewProject (callback) {
// // unset the projectId
// this.setState({projectId: defaultProjectId}, () => {
// // strip the hash tag from the url
// // history.pushState('new-project', 'new-project',
// // window.location.pathname + window.location.search);
// if (callback) callback(defaultProjectId);
// });
// }
render () {
return (
<WrappedComponent
hideIntro={this.state.projectId && this.state.projectId !== 0}
projectId={this.state.projectId}
// projectId={this.state.projectId}
onRequestNewProject={this.handleRequestNewProject}
{...this.props}
/>
);
}
}
return HashParserComponent;
HashParserComponent.propTypes = {
isLoadingNonHashProject: PropTypes.bool,
reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
setHashProjectId: PropTypes.func
};
const mapStateToProps = state => {
const projectState = state.scratchGui.projectId.projectState;
return {
isLoadingNonHashProject: projectState === ProjectState.LOADING_FILE_UPLOAD ||
projectState === ProjectState.LOADING_NEW_DEFAULT,
reduxProjectId: state.scratchGui.projectId.projectId
};
};
const mapDispatchToProps = dispatch => ({
setHashProjectId: projectId => dispatch(setHashProjectId(projectId))
});
return connect(
mapStateToProps,
mapDispatchToProps
)(HashParserComponent);
};
export {
+47 -21
View File
@@ -2,8 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import {intlShape, injectIntl} from 'react-intl';
import bindAll from 'lodash.bindall';
import {connect} from 'react-redux';
import {defaultProjectId} from '../reducers/project-id';
import {ProjectState, defaultProjectId, setInitialProjectId} from '../reducers/project-id';
import analytics from './analytics';
import log from './log';
@@ -39,9 +40,12 @@ const ProjectLoaderHOC = function (WrappedComponent) {
props.projectId !== null &&
typeof props.projectId !== 'undefined'
) {
this.updateProject(props.projectId);
this.props.setInitialProjectId(props.projectId);
// NOTE: may need to reenable this
// this.updateProject(props.projectId);
}
}
// NOTE: try componentWillReceiveProps instead?
componentWillUpdate (nextProps) {
if (this.props.projectHost !== nextProps.projectHost) {
storage.setProjectHost(nextProps.projectHost);
@@ -49,23 +53,26 @@ const ProjectLoaderHOC = function (WrappedComponent) {
if (this.props.assetHost !== nextProps.assetHost) {
storage.setAssetHost(nextProps.assetHost);
}
if (this.props.projectId !== nextProps.projectId) {
this.setState({fetchingProject: true}, () => {
this.updateProject(nextProps.projectId);
});
if (nextProps.isLoadingProjectWithId && !this.props.isLoadingProjectWithId) {
this.updateProject(nextProps.reduxProjectId);
}
// if (this.props.projectId !== nextProps.projectId) {
// this.setState({fetchingProject: true}, () => {
// this.updateProject(nextProps.projectId);
// });
// }
}
handleRequestNewProject (callback) {
// pass the request up the chain
this.props.onRequestNewProject(newProjectId => {
// now that parents have had chance to act and change the projectId,
// update the metadata using the projectId -- even if it is the
// same projectId as before.
this.updateProject(newProjectId).then(() => {
if (callback) callback(newProjectId); // pass the callback down the chain
});
});
}
// handleRequestNewProject (callback) {
// // pass the request up the chain
// this.props.onRequestNewProject(newProjectId => {
// // now that parents have had chance to act and change the projectId,
// // update the metadata using the projectId -- even if it is the
// // same projectId as before.
// // this.updateProject(newProjectId).then(() => {
// if (callback) callback(newProjectId); // pass the callback down the chain
// // });
// });
// }
// NOTE: should we instesad have updateProject return a promise, and resolve those
// in the functions that call updatePromise to improve the execution sequence?
updateProject (projectId) {
@@ -116,17 +123,36 @@ const ProjectLoaderHOC = function (WrappedComponent) {
ProjectLoaderComponent.propTypes = {
assetHost: PropTypes.string,
intl: intlShape.isRequired,
isLoadingProjectWithId: PropTypes.bool,
onRequestNewProject: PropTypes.func,
projectHost: PropTypes.string,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
setInitialProjectId: PropTypes.func
};
ProjectLoaderComponent.defaultProps = {
// NOTE: shouldn't these settings be moved into webpack, like API_HOST?
assetHost: 'https://assets.scratch.mit.edu',
projectHost: 'https://projects.scratch.mit.edu',
projectId: defaultProjectId
projectHost: 'https://projects.scratch.mit.edu'
// NOTE: disabled because either embed or hash parser should provide val
// projectId: defaultProjectId
};
return injectIntl(ProjectLoaderComponent);
const mapStateToProps = state => {
const projectState = state.scratchGui.projectId.projectState;
return {
isLoadingProjectWithId: projectState === ProjectState.LOADING_WITH_ID ||
projectState === ProjectState.LOADING_NEW_DEFAULT,
reduxProjectId: state.scratchGui.projectId.projectId
};
};
const mapDispatchToProps = dispatch => ({
setInitialProjectId: projectId => dispatch(setInitialProjectId(projectId))
});
return injectIntl(connect(
mapStateToProps,
mapDispatchToProps
)(ProjectLoaderComponent));
};
export {
+41 -32
View File
@@ -2,11 +2,11 @@ import React from 'react';
import defaultsdeep from 'lodash.defaultsdeep';
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
// import {connect} from 'react-redux';
import {connect} from 'react-redux';
import {intlShape, injectIntl} from 'react-intl';
import xhr from 'xhr';
import {defaultProjectId} from '../reducers/project-id';
import {ProjectState, defaultProjectId} from '../reducers/project-id';
import {defaultProjectTitleMessages} from '../reducers/project-title';
const api = (options, token, callback) => {
@@ -50,29 +50,32 @@ const ProjectMetaDataHOC = function (WrappedComponent) {
]);
this.updateProjectMetaData(props.projectId);
}
componentWillUpdate (nextProps) {
// if projectId has changed, and not just from one default value to
// another equivalent value, look up project metadata
if (this.props.projectId !== nextProps.projectId &&
((this.props.projectId !== defaultProjectId &&
this.props.projectId !== null &&
typeof this.props.projectId !== 'undefined') ||
(nextProps.projectId !== defaultProjectId &&
nextProps.projectId !== null &&
typeof nextProps.projectId !== 'undefined'))) {
componentWillReceiveProps (nextProps) {
if (nextProps.isLoadingProjectWithId && !this.props.isLoadingProjectWithId) {
this.updateProjectMetaData(nextProps.projectId);
}
// // if projectId has changed, and not just from one default value to
// // another equivalent value, look up project metadata
// if (this.props.projectId !== nextProps.projectId &&
// ((this.props.projectId !== defaultProjectId &&
// this.props.projectId !== null &&
// typeof this.props.projectId !== 'undefined') ||
// (nextProps.projectId !== defaultProjectId &&
// nextProps.projectId !== null &&
// typeof nextProps.projectId !== 'undefined'))) {
// this.updateProjectMetaData(nextProps.projectId);
// }
}
handleRequestNewProject (callback) {
// pass the request up the chain
this.props.onRequestNewProject(newProjectId => {
// now that parents have had chance to act and change the projectId,
// update the metadata using the projectId -- even if it is the
// same projectId as before.
this.updateProjectMetaData(newProjectId);
if (callback) callback(newProjectId); // pass the callback down the chain
});
}
// handleRequestNewProject (callback) {
// // pass the request up the chain
// this.props.onRequestNewProject(newProjectId => {
// // now that parents have had chance to act and change the projectId,
// // update the metadata using the projectId -- even if it is the
// // same projectId as before.
// // this.updateProjectMetaData(newProjectId);
// if (callback) callback(newProjectId); // pass the callback down the chain
// });
// }
updateProjectMetaData (projectId) {
if (projectId === defaultProjectId || projectId === null || typeof projectId === 'undefined') {
// if project is default, use static data
@@ -108,20 +111,26 @@ const ProjectMetaDataHOC = function (WrappedComponent) {
ProjectMetaDataComponent.propTypes = {
intl: intlShape.isRequired,
isLoadingProjectWithId: PropTypes.bool,
onRequestNewProject: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
token: PropTypes.string
projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
// token: PropTypes.string
};
// const mapStateToProps = state => ({
// token: state.session.session && state.session.session.user && state.session.session.user.token
// });
//
// const mapDispatchToProps = () => ({});
//
// return injectIntl(connect(mapStateToProps, mapDispatchToProps)(ProjectMetaDataComponent));
return injectIntl(ProjectMetaDataComponent);
const mapStateToProps = state => {
const projectState = state.scratchGui.projectId.projectState;
return {
isLoadingProjectWithId: projectState === ProjectState.LOADING_WITH_ID ||
projectState === ProjectState.LOADING_NEW_DEFAULT,
projectId: state.scratchGui.projectId.projectId
// token: state.session.session && state.session.session.user && state.session.session.user.token
};
};
const mapDispatchToProps = () => ({});
return injectIntl(connect(mapStateToProps, mapDispatchToProps)(ProjectMetaDataComponent));
};
export {
+70
View File
@@ -0,0 +1,70 @@
// NOTE: maybe this whole HOC can be moved inside project-id reducer?
import React from 'react';
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
import {connect} from 'react-redux';
import {ProjectState, doneSavingWithId} from '../reducers/project-id';
import {doStoreProject} from '../reducers/vm';
// import storage from './storage';
/* Higher Order Component to provide behavior for saving projects.
*/
const ProjectSaverHOC = function (WrappedComponent) {
class ProjectSaverComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
]);
this.state = {
};
}
componentWillReceiveProps (nextProps) {
if (nextProps.isSavingWithId && !this.props.isSavingWithId) {
this.doStoreProject(nextProps.reduxProjectId).then(() => {
this.doneSavingWithId();
});
}
}
render () {
const {
/* eslint-disable no-unused-vars */
/* eslint-enable no-unused-vars */
...componentProps
} = this.props;
return (
<WrappedComponent
{...componentProps}
/>
);
}
}
ProjectSaverComponent.propTypes = {
doStoreProject: PropTypes.func,
doneSavingWithId: PropTypes.func,
isSavingWithId: PropTypes.bool,
reduxProjectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
const mapStateToProps = state => {
const projectState = state.scratchGui.projectId.projectState;
return {
isSavingWithId: projectState === ProjectState.SAVING_WITH_ID ||
projectState === ProjectState.SAVING_WITH_ID_BEFORE_NEW,
reduxProjectId: state.scratchGui.projectId.projectId
};
};
const mapDispatchToProps = dispatch => ({
doStoreProject: projectId => dispatch(doStoreProject(projectId)),
doneSavingWithId: projectId => dispatch(doneSavingWithId(projectId))
});
return connect(
mapStateToProps,
mapDispatchToProps
)(ProjectSaverComponent);
};
export {
ProjectSaverHOC as default
};
+33 -21
View File
@@ -1,10 +1,12 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import VM from 'scratch-vm';
import AudioEngine from 'scratch-audio';
import {connect} from 'react-redux';
import {ProjectState} from '../reducers/project-id';
/*
* Higher Order Component to manage events emitted by the VM
@@ -34,11 +36,14 @@ const vmManagerHOC = function (WrappedComponent) {
});
}
componentWillReceiveProps (nextProps) {
if (this.props.projectData !== nextProps.projectData) {
this.setState({isLoading: true}, () => {
this.loadProject(false);
});
if (nextProps.isLoadingProjectWithId && !this.props.isLoadingProjectWithId) {
this.loadProject(false);
}
// if (this.props.projectData !== nextProps.projectData) {
// this.setState({isLoading: true}, () => {
// this.loadProject(false);
// });
// }
}
// NOTE: keep working here. problem is that we ONLY want to load the project
// here when the old project id was also default... otherwise, we load twice!!!
@@ -58,17 +63,17 @@ const vmManagerHOC = function (WrappedComponent) {
this.setState({loadingError: true, errorMessage: e});
});
}
handleRequestNewProject (callback) {
// pass the request up the chain
this.props.onRequestNewProject(newProjectId => {
// now that parents have had chance to act and change the projectId,
this.setState({isLoading: true}, () => {
this.loadProject(false).then(() => {
if (callback) callback(newProjectId); // pass the callback down the chain
});
});
});
}
// handleRequestNewProject (callback) {
// // pass the request up the chain
// this.props.onRequestNewProject(newProjectId => {
// // now that parents have had chance to act and change the projectId,
// this.setState({isLoading: true}, () => {
// // this.loadProject(false).then(() => {
// if (callback) callback(newProjectId); // pass the callback down the chain
// // });
// });
// });
// }
render () {
const {
/* eslint-disable no-unused-vars */
@@ -92,17 +97,24 @@ const vmManagerHOC = function (WrappedComponent) {
}
VMManager.propTypes = {
isLoadingProjectWithId: PropTypes.bool,
onRequestNewProject: PropTypes.func,
projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
vm: PropTypes.instanceOf(VM).isRequired
};
const mapStateToProps = state => ({
vm: state.scratchGui.vm
});
const mapStateToProps = state => {
const projectState = state.scratchGui.projectId.projectState;
return {
isLoadingProjectWithId: projectState === ProjectState.LOADING_WITH_ID ||
projectState === ProjectState.LOADING_NEW_DEFAULT,
projectId: state.scratchGui.projectId.projectId,
vm: state.scratchGui.vm
};
};
const mapDispatchToProps = dispatch => ({
});
const mapDispatchToProps = () => ({});
return connect(
mapStateToProps,
+127 -5
View File
@@ -1,16 +1,67 @@
const SET_PROJECT_ID = 'scratch-gui/project-id/SET_PROJECT_ID';
import keyMirror from 'keymirror';
const initialState = null;
const SET_PROJECT_ID = 'scratch-gui/project-id/SET_PROJECT_ID';
const TRANSITION_STATE = 'scratch-gui/project-id/TRANSITION_STATE';
const defaultProjectId = 0; // hardcoded id of default project
const ProjectState = keyMirror({
NOT_LOADED: null,
ERROR: null,
LOADING_WITH_ID: null,
LOADING_FILE_UPLOAD: null,
LOADING_NEW_DEFAULT: null,
SHOWING_WITH_ID: null,
SHOWING_FILE_UPLOAD: null,
SHOWING_NEW_DEFAULT: null,
SAVING_WITH_ID: null,
SAVING_WITH_ID_BEFORE_NEW: null
});
const initialState = {
projectId: null,
projectState: ProjectState.NOT_LOADED
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
// NOTE: delete this action entirely?
case SET_PROJECT_ID:
// debugger;
return action.id;
return Object.assign({}, state, {projectId: action.id});
case TRANSITION_STATE:
// "from" state must be either an array that includes current project state,
// or null/undefined
if (state.projectState in action.transitions) {
const resultState = action.transitions[state.projectState];
switch (resultState) {
case ProjectState.LOADING_WITH_ID:
return Object.assign({}, state, {
projectId: action.id,
projectState: resultState
});
case ProjectState.LOADING_FILE_UPLOAD:
return Object.assign({}, state, {
projectId: null,
projectState: resultState
});
case ProjectState.LOADING_NEW_DEFAULT:
return Object.assign({}, state, {
projectId: defaultProjectId,
projectState: resultState
});
default:
return Object.assign({}, state, {projectState: resultState});
}
}
// default to requiring transitions to successfully match current state
if (action.require || typeof action.require === 'undefined') {
// NOTE: how to throw error right here?
console.log(`Error: tried to transition from state ${state.projectState} to states...`);
console.log(action.transitions);
}
break;
default:
return state;
}
@@ -23,9 +74,80 @@ const setProjectId = function (id) {
};
};
// "initial" here refers to being invoked, usually embedded in another app, with a projectId property
const setInitialProjectId = id => ({
type: TRANSITION_STATE,
transitions: {
[ProjectState.NOT_LOADED]: ProjectState.LOADING_WITH_ID
},
id: id
});
const setHashProjectId = id => {
if (id === defaultProjectId || id === null || typeof id === 'undefined') {
// it's ok if nothing matches, we may have just stripped the hash.
// only transition based on seeing no hash if this is the initial load.
return {
type: TRANSITION_STATE,
transitions: {
[ProjectState.NOT_LOADED]: ProjectState.LOADING_NEW_DEFAULT
},
require: false
};
}
return {
type: TRANSITION_STATE,
transitions: {
[ProjectState.NOT_LOADED]: ProjectState.LOADING_WITH_ID
},
id: id
};
};
const startLoadingFileUpload = () => ({
type: TRANSITION_STATE,
transitions: {
[ProjectState.NOT_LOADED]: ProjectState.LOADING_FILE_UPLOAD,
[ProjectState.SHOWING_WITH_ID]: ProjectState.LOADING_FILE_UPLOAD,
[ProjectState.SHOWING_FILE_UPLOAD]: ProjectState.LOADING_FILE_UPLOAD,
[ProjectState.SHOWING_NEW_DEFAULT]: ProjectState.LOADING_FILE_UPLOAD
}
});
const doneLoadingFileUpload = () => ({
type: TRANSITION_STATE,
transitions: {
[ProjectState.LOADING_FILE_UPLOAD]: ProjectState.SHOWING_FILE_UPLOAD
}
});
const stepTowardsNewProject = () => ({
type: TRANSITION_STATE,
transitions: {
[ProjectState.SHOWING_WITH_ID]: ProjectState.SAVING_WITH_ID_BEFORE_NEW,
[ProjectState.SHOWING_FILE_UPLOAD]: ProjectState.LOADING_NEW_DEFAULT,
[ProjectState.SHOWING_NEW_DEFAULT]: ProjectState.LOADING_NEW_DEFAULT
}
});
const doneSavingWithId = () => ({
type: TRANSITION_STATE,
transitions: {
[ProjectState.SAVING_WITH_ID]: ProjectState.SHOWING_WITH_ID,
[ProjectState.SAVING_WITH_ID_BEFORE_NEW]: ProjectState.LOADING_NEW_DEFAULT
}
});
export {
reducer as default,
initialState as projectIdInitialState,
ProjectState,
defaultProjectId,
setProjectId
doneLoadingFileUpload,
doneSavingWithId,
setHashProjectId,
setInitialProjectId,
setProjectId,
startLoadingFileUpload,
stepTowardsNewProject
};
+22
View File
@@ -21,8 +21,30 @@ const setVM = function (vm) {
vm: vm
};
};
const doStoreProject = (id, onSaveFinished) => (
reducer.saveProjectSb3()
.then(content => {
if (onSaveFinished) {
onSaveFinished();
}
const assetType = storage.AssetType.Project;
const dataFormat = storage.DataFormat.SB3;
const body = new FormData();
body.append('sb3_file', content, 'sb3_file');
return storage.store(
assetType,
dataFormat,
body,
id
);
})
);
export {
reducer as default,
initialState as vmInitialState,
doStoreProject,
setVM
};