Skip to content
Snippets Groups Projects
Commit a70b7f5c authored by Ray Schamp's avatar Ray Schamp
Browse files

Enable "Save now" menu item when there's a session

parent 21a27785
No related branches found
No related tags found
No related merge requests found
......@@ -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: {
......
......@@ -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);
}
}
......@@ -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,
......
......@@ -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
......
......@@ -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)
};
......
......@@ -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(
......
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 {
......
......@@ -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/`;
}
}
......
......@@ -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,
......
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
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment