From a70b7f5c9307447dd068b20776a0919c2360077c Mon Sep 17 00:00:00 2001 From: Ray Schamp <ray@scratch.mit.edu> Date: Fri, 24 Aug 2018 16:00:28 -0400 Subject: [PATCH] Enable "Save now" menu item when there's a session --- package.json | 2 +- src/.eslintrc.js | 5 ++- src/components/menu-bar/menu-bar.css | 13 +++++++ src/components/menu-bar/menu-bar.jsx | 57 ++++++++++++++++++++-------- src/containers/backpack.jsx | 2 +- src/containers/gui.jsx | 4 -- src/containers/project-saver.jsx | 49 ++++++++++++++++++------ src/lib/project-loader-hoc.jsx | 45 +++++++++++++++++----- src/lib/storage.js | 30 +++++++++++---- src/reducers/gui.js | 3 ++ src/reducers/project-id.js | 27 +++++++++++++ 11 files changed, 185 insertions(+), 52 deletions(-) create mode 100644 src/reducers/project-id.js diff --git a/package.json b/package.json index c43fb30c0..de36d129e 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "scratch-l10n": "3.0.20180824134256", "scratch-paint": "0.2.0-prerelease.20180823231354", "scratch-render": "0.1.0-prerelease.20180824141819", - "scratch-storage": "1.0.0", + "scratch-storage": "1.0.2", "scratch-svg-renderer": "0.2.0-prerelease.20180817005452", "scratch-vm": "0.2.0-prerelease.20180824135031", "selenium-webdriver": "3.6.0", diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 14b082cdf..4b05b2d27 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -12,7 +12,10 @@ module.exports = { 'import/no-commonjs': 'error', 'import/no-amd': 'error', 'import/no-nodejs-modules': 'error', - 'react/jsx-no-literals': 'error' + 'react/jsx-no-literals': 'error', + 'no-confusing-arrow': ['error', { + 'allowParens': true + }] }, settings: { react: { diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index de2854c50..662c0e1ae 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -189,3 +189,16 @@ .disabled { opacity: 0.5; } + +.save-in-progress { + animation: hue-rotate 3s linear infinite; +} + +@keyframes hue-rotate { + from { + filter: hue-rotate(); + } + to { + filter: hue-rotate(360deg); + } +} diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 5a9ca128b..d88e67f55 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -140,6 +140,7 @@ class MenuBar extends React.Component { 'handleRestoreOption', 'restoreOptionMessage' ]); + this.state = {projectSaveInProgress: false}; } handleLanguageMouseUp (e) { if (!this.props.languageMenuOpen) { @@ -152,6 +153,18 @@ class MenuBar extends React.Component { this.props.onRequestCloseEdit(); }; } + handleUpdateProject (updateFun) { + return () => { + this.props.onRequestCloseFile(); + this.setState({projectSaveInProgress: true}, + () => { + updateFun().then(() => { + this.setState({projectSaveInProgress: false}); + }); + } + ); + }; + } restoreOptionMessage (deletedItem) { switch (deletedItem) { case 'Sprite': @@ -182,8 +195,19 @@ class MenuBar extends React.Component { } } render () { + const saveNowMessage = ( + <FormattedMessage + defaultMessage="Save now" + description="Menu bar item for saving now" + id="gui.menuBar.saveNow" + /> + ); return ( - <Box className={styles.menuBar}> + <Box + className={classNames(styles.menuBar, { + [styles.saveInProgress]: this.state.projectSaveInProgress + })} + > <div className={styles.mainMenu}> <div className={styles.fileGroup}> <div className={classNames(styles.menuBarItem)}> @@ -246,18 +270,20 @@ class MenuBar extends React.Component { </MenuItem> </MenuItemTooltip> <MenuSection> - <MenuItemTooltip - id="save" - isRtl={this.props.isRtl} - > - <MenuItem> - <FormattedMessage - defaultMessage="Save now" - description="Menu bar item for saving now" - id="gui.menuBar.saveNow" - /> - </MenuItem> - </MenuItemTooltip> + <ProjectSaver>{(saveProject, updateProject) => ( + this.props.canUpdateProject ? ( + <MenuItem onClick={this.handleUpdateProject(updateProject)}> + {saveNowMessage} + </MenuItem> + ) : ( + <MenuItemTooltip + id="save" + isRtl={this.props.isRtl} + > + <MenuItem>{saveNowMessage}</MenuItem> + </MenuItemTooltip> + ) + )}</ProjectSaver> <MenuItemTooltip id="copy" isRtl={this.props.isRtl} @@ -284,10 +310,9 @@ class MenuBar extends React.Component { {renderFileInput()} </MenuItem> )}</ProjectLoader> - <ProjectSaver>{(saveProject, saveProps) => ( + <ProjectSaver>{saveProject => ( <MenuItem onClick={saveProject} - {...saveProps} > <FormattedMessage defaultMessage="Save to your computer" @@ -475,6 +500,7 @@ class MenuBar extends React.Component { } MenuBar.propTypes = { + canUpdateProject: PropTypes.bool, editMenuOpen: PropTypes.bool, enableCommunity: PropTypes.bool, fileMenuOpen: PropTypes.bool, @@ -492,6 +518,7 @@ MenuBar.propTypes = { }; const mapStateToProps = state => ({ + canUpdateProject: typeof (state.session && state.session.session && state.session.session.user) !== 'undefined', fileMenuOpen: fileMenuOpen(state), editMenuOpen: editMenuOpen(state), isRtl: state.locales.isRtl, diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index e90fe1dff..3f9b2dd59 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -134,7 +134,7 @@ Backpack.propTypes = { const getTokenAndUsername = state => { // Look for the session state provided by scratch-www - if (state.session && state.session.session) { + if (state.session && state.session.session && state.session.session.user) { return { token: state.session.session.user.token, username: state.session.session.user.username diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 33d8fe58b..6e5d96070 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -72,12 +72,10 @@ class GUI extends React.Component { `Failed to load project from server [id=${window.location.hash}]: ${this.state.errorMessage}`); } const { - assetHost, // eslint-disable-line no-unused-vars children, fetchingProject, loadingStateVisible, projectData, // eslint-disable-line no-unused-vars - projectHost, // eslint-disable-line no-unused-vars vm, ...componentProps } = this.props; @@ -94,7 +92,6 @@ class GUI extends React.Component { } GUI.propTypes = { - assetHost: PropTypes.string, children: PropTypes.node, fetchingProject: PropTypes.bool, importInfoVisible: PropTypes.bool, @@ -102,7 +99,6 @@ GUI.propTypes = { onSeeCommunity: PropTypes.func, previewInfoVisible: PropTypes.bool, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - projectHost: PropTypes.string, vm: PropTypes.instanceOf(VM) }; diff --git a/src/containers/project-saver.jsx b/src/containers/project-saver.jsx index c594421ec..13be33da8 100644 --- a/src/containers/project-saver.jsx +++ b/src/containers/project-saver.jsx @@ -2,6 +2,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; +import storage from '../lib/storage'; /** * Project saver component passes a saveProject function to its child. @@ -21,14 +22,17 @@ class ProjectSaver extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'saveProject' + 'createProject', + 'updateProject', + 'saveProject', + 'doStoreProject' ]); } saveProject () { const saveLink = document.createElement('a'); document.body.appendChild(saveLink); - this.props.vm.saveProjectSb3().then(content => { + this.props.saveProjectSb3().then(content => { // TODO user-friendly project name // File name: project-DATE-TIME const date = new Date(); @@ -49,27 +53,48 @@ class ProjectSaver extends React.Component { document.body.removeChild(saveLink); }); } + doStoreProject (id) { + return this.props.saveProjectSb3() + .then(content => { + 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(); + } + updateProject () { + return this.doStoreProject(this.props.projectId); + } render () { const { - /* eslint-disable no-unused-vars */ - children, - vm, - /* eslint-enable no-unused-vars */ - ...props + children } = this.props; - return this.props.children(this.saveProject, props); + return children( + this.saveProject, + this.updateProject, + this.createProject + ); } } ProjectSaver.propTypes = { children: PropTypes.func, - vm: PropTypes.shape({ - saveProjectSb3: PropTypes.func - }) + projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + saveProjectSb3: PropTypes.func }; const mapStateToProps = state => ({ - vm: state.scratchGui.vm + saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm), + projectId: state.scratchGui.projectId }); export default connect( diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx index 22a1aa5d1..68642c336 100644 --- a/src/lib/project-loader-hoc.jsx +++ b/src/lib/project-loader-hoc.jsx @@ -1,5 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; + +import {setProjectId} from '../reducers/project-id'; import analytics from './analytics'; import log from './log'; @@ -21,12 +24,19 @@ const ProjectLoaderHOC = function (WrappedComponent) { }; storage.setProjectHost(props.projectHost); storage.setAssetHost(props.assetHost); - - } - componentDidMount () { - if (this.props.projectId || this.props.projectId === 0) { - this.updateProject(this.props.projectId); + if (props.projectId !== props.reduxProjectId) { + props.setProjectId(props.projectId); + } + if ( + props.projectId !== '' && + props.projectId !== null && + typeof props.projectId !== 'undefined' + ) { + this.updateProject(props.projectId); + } else { + this.updateProject(props.reduxProjectId); } + } componentWillUpdate (nextProps) { if (this.props.projectHost !== nextProps.projectHost) { @@ -36,6 +46,7 @@ const ProjectLoaderHOC = function (WrappedComponent) { storage.setAssetHost(nextProps.assetHost); } if (this.props.projectId !== nextProps.projectId) { + this.props.setProjectId(nextProps.projectId); this.setState({fetchingProject: true}, () => { this.updateProject(nextProps.projectId); }); @@ -62,7 +73,13 @@ const ProjectLoaderHOC = function (WrappedComponent) { } render () { const { - projectId, // eslint-disable-line no-unused-vars + /* eslint-disable no-unused-vars */ + assetHost, + projectHost, + projectId, + reduxProjectId, + setProjectId, + /* eslint-enable no-unused-vars */ ...componentProps } = this.props; if (!this.state.projectData) return null; @@ -78,15 +95,23 @@ const ProjectLoaderHOC = function (WrappedComponent) { ProjectLoaderComponent.propTypes = { assetHost: PropTypes.string, projectHost: PropTypes.string, - projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + setProjectId: PropTypes.func }; ProjectLoaderComponent.defaultProps = { assetHost: 'https://assets.scratch.mit.edu', - projectHost: 'https://projects.scratch.mit.edu', - projectId: 0 + projectHost: 'https://projects.scratch.mit.edu' }; - return ProjectLoaderComponent; + const mapStateToProps = state => ({ + reduxProjectId: state.scratchGui.projectId + }); + + const mapDispatchToProps = dispatch => ({ + setProjectId: id => dispatch(setProjectId(id)) + }); + + return connect(mapStateToProps, mapDispatchToProps)(ProjectLoaderComponent); }; export { diff --git a/src/lib/storage.js b/src/lib/storage.js index 9c49d8170..71905ed9d 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -15,15 +15,17 @@ class Storage extends ScratchStorage { asset.data, asset.id )); - this.addWebSource( + this.addWebStore( [this.AssetType.Project], - this.getProjectURL.bind(this) + this.getProjectGetConfig.bind(this), + this.getProjectCreateConfig.bind(this), + this.getProjectUpdateConfig.bind(this) ); - this.addWebSource( + this.addWebStore( [this.AssetType.ImageVector, this.AssetType.ImageBitmap, this.AssetType.Sound], - this.getAssetURL.bind(this) + this.getAssetGetConfig.bind(this) ); - this.addWebSource( + this.addWebStore( [this.AssetType.Sound], asset => `static/extension-assets/scratch3_music/${asset.assetId}.${asset.dataFormat}` ); @@ -31,13 +33,25 @@ class Storage extends ScratchStorage { setProjectHost (projectHost) { this.projectHost = projectHost; } - getProjectURL (projectAsset) { - return `${this.projectHost}/internalapi/project/${projectAsset.assetId}/get/`; + getProjectGetConfig (projectAsset) { + return `${this.projectHost}/${projectAsset.assetId}`; + } + getProjectCreateConfig () { + return { + url: `${this.projectHost}/`, + withCredentials: true + }; + } + getProjectUpdateConfig (projectAsset) { + return { + url: `${this.projectHost}/${projectAsset.assetId}`, + withCredentials: true + }; } setAssetHost (assetHost) { this.assetHost = assetHost; } - getAssetURL (asset) { + getAssetGetConfig (asset) { return `${this.assetHost}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`; } } diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 04b264468..9340af56e 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -11,6 +11,7 @@ import modalReducer, {modalsInitialState} from './modals'; import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout'; +import projectIdReducer, {projectIdInitialState} from './project-id'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; @@ -35,6 +36,7 @@ const guiInitialState = { modals: modalsInitialState, monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, + projectId: projectIdInitialState, restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, toolbox: toolboxInitialState, @@ -77,6 +79,7 @@ const guiReducer = combineReducers({ modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer, + projectId: projectIdReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, toolbox: toolboxReducer, diff --git a/src/reducers/project-id.js b/src/reducers/project-id.js new file mode 100644 index 000000000..da98a921e --- /dev/null +++ b/src/reducers/project-id.js @@ -0,0 +1,27 @@ +const SET_PROJECT_ID = 'scratch-gui/project-id/SET_PROJECT_ID'; + +const initialState = 0; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + + switch (action.type) { + case SET_PROJECT_ID: + return action.id; + default: + return state; + } +}; + +const setProjectId = function (id) { + return { + type: SET_PROJECT_ID, + id: id + }; +}; + +export { + reducer as default, + initialState as projectIdInitialState, + setProjectId +}; -- GitLab