major refactor to introduce project id state machine
This commit is contained in:
1 parent
a7c5d60afb
commit
676b3fc278
11 files changed
+522
-179
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
Reference in new issue
Block a user