Merge pull request #5437 from LLK/revert-4809-refactor-file-uploader
Revert "refactor SB* file uploader into top-level HOC"
This commit is contained in:
8 files changed
+326
-389
No files matched your search
@@ -105,7 +105,6 @@ const GUIComponent = props => {
|
||||
onRequestCloseTelemetryModal,
|
||||
onSeeCommunity,
|
||||
onShare,
|
||||
onStartSelectingFileUpload,
|
||||
onTelemetryModalCancel,
|
||||
onTelemetryModalOptIn,
|
||||
onTelemetryModalOptOut,
|
||||
@@ -227,7 +226,6 @@ const GUIComponent = props => {
|
||||
onProjectTelemetryEvent={onProjectTelemetryEvent}
|
||||
onSeeCommunity={onSeeCommunity}
|
||||
onShare={onShare}
|
||||
onStartSelectingFileUpload={onStartSelectingFileUpload}
|
||||
onToggleLoginOpen={onToggleLoginOpen}
|
||||
/>
|
||||
<Box className={styles.bodyWrapper}>
|
||||
@@ -401,7 +399,6 @@ GUIComponent.propTypes = {
|
||||
onRequestCloseTelemetryModal: PropTypes.func,
|
||||
onSeeCommunity: PropTypes.func,
|
||||
onShare: PropTypes.func,
|
||||
onStartSelectingFileUpload: PropTypes.func,
|
||||
onTabSelect: PropTypes.func,
|
||||
onTelemetryModalCancel: PropTypes.func,
|
||||
onTelemetryModalOptIn: PropTypes.func,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
|
||||
import Divider from '../divider/divider.jsx';
|
||||
import LanguageSelector from '../../containers/language-selector.jsx';
|
||||
import SaveStatus from './save-status.jsx';
|
||||
import SBFileUploader from '../../containers/sb-file-uploader.jsx';
|
||||
import ProjectWatcher from '../../containers/project-watcher.jsx';
|
||||
import MenuBarMenu from './menu-bar-menu.jsx';
|
||||
import {MenuItem, MenuSection} from '../menu/menu.jsx';
|
||||
@@ -391,11 +392,22 @@ class MenuBar extends React.Component {
|
||||
</MenuSection>
|
||||
)}
|
||||
<MenuSection>
|
||||
<MenuItem
|
||||
onClick={this.props.onStartSelectingFileUpload}
|
||||
<SBFileUploader
|
||||
canSave={this.props.canSave}
|
||||
userOwnsProject={this.props.userOwnsProject}
|
||||
>
|
||||
{this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
|
||||
</MenuItem>
|
||||
{(className, renderFileInput, handleLoadProject) => (
|
||||
<MenuItem
|
||||
className={className}
|
||||
onClick={handleLoadProject}
|
||||
>
|
||||
{/* eslint-disable max-len */}
|
||||
{this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
|
||||
{/* eslint-enable max-len */}
|
||||
{renderFileInput()}
|
||||
</MenuItem>
|
||||
)}
|
||||
</SBFileUploader>
|
||||
<SB3Downloader>{(className, downloadProjectCallback) => (
|
||||
<MenuItem
|
||||
className={className}
|
||||
@@ -731,7 +743,6 @@ MenuBar.propTypes = {
|
||||
onRequestCloseLogin: PropTypes.func,
|
||||
onSeeCommunity: PropTypes.func,
|
||||
onShare: PropTypes.func,
|
||||
onStartSelectingFileUpload: PropTypes.func,
|
||||
onToggleLoginOpen: PropTypes.func,
|
||||
projectTitle: PropTypes.string,
|
||||
renderLogin: PropTypes.func,
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
|
||||
import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
|
||||
import LocalizationHOC from '../lib/localization-hoc.jsx';
|
||||
import SBFileUploaderHOC from '../lib/sb-file-uploader-hoc.jsx';
|
||||
import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
|
||||
import TitledHOC from '../lib/titled-hoc.jsx';
|
||||
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
|
||||
@@ -182,7 +181,6 @@ const WrappedGui = compose(
|
||||
ProjectSaverHOC,
|
||||
vmListenerHOC,
|
||||
vmManagerHOC,
|
||||
SBFileUploaderHOC,
|
||||
cloudManagerHOC
|
||||
)(ConnectedGUI);
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import bindAll from 'lodash.bindall';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import {setProjectTitle} from '../reducers/project-title';
|
||||
|
||||
import log from '../lib/log';
|
||||
import sharedMessages from '../lib/shared-messages';
|
||||
|
||||
import {
|
||||
LoadingStates,
|
||||
getIsLoadingUpload,
|
||||
getIsShowingWithoutId,
|
||||
onLoadedProject,
|
||||
requestProjectUpload
|
||||
} from '../reducers/project-state';
|
||||
|
||||
import {
|
||||
openLoadingProject,
|
||||
closeLoadingProject
|
||||
} from '../reducers/modals';
|
||||
import {
|
||||
closeFileMenu
|
||||
} from '../reducers/menus';
|
||||
|
||||
/**
|
||||
* SBFileUploader component passes a file input, load handler and props to its child.
|
||||
* It expects this child to be a function with the signature
|
||||
* function (renderFileInput, handleLoadProject) {}
|
||||
* The component can then be used to attach project loading functionality
|
||||
* to any other component:
|
||||
*
|
||||
* <SBFileUploader>{(className, renderFileInput, handleLoadProject) => (
|
||||
* <MyCoolComponent
|
||||
* className={className}
|
||||
* onClick={handleLoadProject}
|
||||
* >
|
||||
* {renderFileInput()}
|
||||
* </MyCoolComponent>
|
||||
* )}</SBFileUploader>
|
||||
*/
|
||||
|
||||
const messages = defineMessages({
|
||||
loadError: {
|
||||
id: 'gui.projectLoader.loadError',
|
||||
defaultMessage: 'The project file that was selected failed to load.',
|
||||
description: 'An error that displays when a local project file fails to load.'
|
||||
}
|
||||
});
|
||||
|
||||
class SBFileUploader extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'getProjectTitleFromFilename',
|
||||
'renderFileInput',
|
||||
'setFileInput',
|
||||
'handleChange',
|
||||
'handleClick',
|
||||
'onload',
|
||||
'resetFileInput'
|
||||
]);
|
||||
}
|
||||
componentWillMount () {
|
||||
this.reader = new FileReader();
|
||||
this.reader.onload = this.onload;
|
||||
this.resetFileInput();
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.isLoadingUpload && !prevProps.isLoadingUpload && this.fileToUpload && this.reader) {
|
||||
this.reader.readAsArrayBuffer(this.fileToUpload);
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.reader = null;
|
||||
this.resetFileInput();
|
||||
}
|
||||
resetFileInput () {
|
||||
this.fileToUpload = null;
|
||||
if (this.fileInput) {
|
||||
this.fileInput.value = null;
|
||||
}
|
||||
}
|
||||
getProjectTitleFromFilename (fileInputFilename) {
|
||||
if (!fileInputFilename) return '';
|
||||
// only parse title with valid scratch project extensions
|
||||
// (.sb, .sb2, and .sb3)
|
||||
const matches = fileInputFilename.match(/^(.*)\.sb[23]?$/);
|
||||
if (!matches) return '';
|
||||
return matches[1].substring(0, 100); // truncate project title to max 100 chars
|
||||
}
|
||||
// called when user has finished selecting a file to upload
|
||||
handleChange (e) {
|
||||
const {
|
||||
intl,
|
||||
isShowingWithoutId,
|
||||
loadingState,
|
||||
projectChanged,
|
||||
userOwnsProject
|
||||
} = this.props;
|
||||
|
||||
const thisFileInput = e.target;
|
||||
if (thisFileInput.files) { // Don't attempt to load if no file was selected
|
||||
this.fileToUpload = thisFileInput.files[0];
|
||||
|
||||
// If user owns the project, or user has changed the project,
|
||||
// we must confirm with the user that they really intend to replace it.
|
||||
// (If they don't own the project and haven't changed it, no need to confirm.)
|
||||
let uploadAllowed = true;
|
||||
if (userOwnsProject || (projectChanged && isShowingWithoutId)) {
|
||||
uploadAllowed = confirm( // eslint-disable-line no-alert
|
||||
intl.formatMessage(sharedMessages.replaceProjectWarning)
|
||||
);
|
||||
}
|
||||
if (uploadAllowed) {
|
||||
this.props.requestProjectUpload(loadingState);
|
||||
} else {
|
||||
this.props.closeFileMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
// called when file upload raw data is available in the reader
|
||||
onload () {
|
||||
if (this.reader) {
|
||||
this.props.onLoadingStarted();
|
||||
const filename = this.fileToUpload && this.fileToUpload.name;
|
||||
this.props.vm.loadProject(this.reader.result)
|
||||
.then(() => {
|
||||
this.props.onLoadingFinished(this.props.loadingState, true);
|
||||
// Reset the file input after project is loaded
|
||||
// This is necessary in case the user wants to reload a project
|
||||
if (filename) {
|
||||
const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
|
||||
this.props.onReceivedProjectTitle(uploadedProjectTitle);
|
||||
}
|
||||
this.resetFileInput();
|
||||
})
|
||||
.catch(error => {
|
||||
log.warn(error);
|
||||
alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert
|
||||
this.props.onLoadingFinished(this.props.loadingState, false);
|
||||
// Reset the file input after project is loaded
|
||||
// This is necessary in case the user wants to reload a project
|
||||
this.resetFileInput();
|
||||
});
|
||||
}
|
||||
}
|
||||
handleClick () {
|
||||
// open filesystem browsing window
|
||||
this.fileInput.click();
|
||||
}
|
||||
setFileInput (input) {
|
||||
this.fileInput = input;
|
||||
}
|
||||
renderFileInput () {
|
||||
return (
|
||||
<input
|
||||
accept=".sb,.sb2,.sb3"
|
||||
ref={this.setFileInput}
|
||||
style={{display: 'none'}}
|
||||
type="file"
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render () {
|
||||
return this.props.children(this.props.className, this.renderFileInput, this.handleClick);
|
||||
}
|
||||
}
|
||||
|
||||
SBFileUploader.propTypes = {
|
||||
canSave: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
|
||||
children: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
closeFileMenu: PropTypes.func,
|
||||
intl: intlShape.isRequired,
|
||||
isLoadingUpload: PropTypes.bool,
|
||||
isShowingWithoutId: PropTypes.bool,
|
||||
loadingState: PropTypes.oneOf(LoadingStates),
|
||||
onLoadingFinished: PropTypes.func,
|
||||
onLoadingStarted: PropTypes.func,
|
||||
projectChanged: PropTypes.bool,
|
||||
requestProjectUpload: PropTypes.func,
|
||||
onReceivedProjectTitle: PropTypes.func,
|
||||
userOwnsProject: PropTypes.bool,
|
||||
vm: PropTypes.shape({
|
||||
loadProject: PropTypes.func
|
||||
})
|
||||
};
|
||||
SBFileUploader.defaultProps = {
|
||||
className: ''
|
||||
};
|
||||
const mapStateToProps = state => {
|
||||
const loadingState = state.scratchGui.projectState.loadingState;
|
||||
return {
|
||||
isLoadingUpload: getIsLoadingUpload(loadingState),
|
||||
isShowingWithoutId: getIsShowingWithoutId(loadingState),
|
||||
loadingState: loadingState,
|
||||
projectChanged: state.scratchGui.projectChanged,
|
||||
vm: state.scratchGui.vm
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
closeFileMenu: () => dispatch(closeFileMenu()),
|
||||
onLoadingFinished: (loadingState, success) => {
|
||||
dispatch(onLoadedProject(loadingState, ownProps.canSave, success));
|
||||
dispatch(closeLoadingProject());
|
||||
dispatch(closeFileMenu());
|
||||
},
|
||||
requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)),
|
||||
onLoadingStarted: () => dispatch(openLoadingProject()),
|
||||
onReceivedProjectTitle: title => dispatch(setProjectTitle(title))
|
||||
});
|
||||
|
||||
// Allow incoming props to override redux-provided props. Used to mock in tests.
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
|
||||
{}, stateProps, dispatchProps, ownProps
|
||||
);
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(injectIntl(SBFileUploader));
|
||||
@@ -1,269 +0,0 @@
|
||||
import bindAll from 'lodash.bindall';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {defineMessages, intlShape, injectIntl} from 'react-intl';
|
||||
import {connect} from 'react-redux';
|
||||
import log from '../lib/log';
|
||||
import sharedMessages from './shared-messages';
|
||||
|
||||
import {
|
||||
LoadingStates,
|
||||
getIsLoadingUpload,
|
||||
getIsShowingWithoutId,
|
||||
onLoadedProject,
|
||||
requestProjectUpload
|
||||
} from '../reducers/project-state';
|
||||
import {
|
||||
openLoadingProject,
|
||||
closeLoadingProject
|
||||
} from '../reducers/modals';
|
||||
import {
|
||||
closeFileMenu
|
||||
} from '../reducers/menus';
|
||||
|
||||
const messages = defineMessages({
|
||||
loadError: {
|
||||
id: 'gui.projectLoader.loadError',
|
||||
defaultMessage: 'The project file that was selected failed to load.',
|
||||
description: 'An error that displays when a local project file fails to load.'
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Higher Order Component to provide behavior for loading local project files into editor.
|
||||
* @param {React.Component} WrappedComponent the component to add project file loading functionality to
|
||||
* @returns {React.Component} WrappedComponent with project file loading functionality added
|
||||
*
|
||||
* <SBFileUploaderHOC>
|
||||
* <WrappedComponent />
|
||||
* </SBFileUploaderHOC>
|
||||
*/
|
||||
const SBFileUploaderHOC = function (WrappedComponent) {
|
||||
class SBFileUploaderComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'createFileObjects',
|
||||
'getProjectTitleFromFilename',
|
||||
'handleFinishedLoadingUpload',
|
||||
'handleStartSelectingFileUpload',
|
||||
'handleChange',
|
||||
'onload',
|
||||
'removeFileObjects'
|
||||
]);
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.isLoadingUpload && !prevProps.isLoadingUpload) {
|
||||
this.handleFinishedLoadingUpload(); // cue step 5 below
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.removeFileObjects();
|
||||
}
|
||||
// step 1: this is where the upload process begins
|
||||
handleStartSelectingFileUpload () {
|
||||
this.createFileObjects(); // go to step 2
|
||||
}
|
||||
// step 2: create a FileReader and an <input> element, and issue a
|
||||
// pseudo-click to it. That will open the file chooser dialog.
|
||||
createFileObjects () {
|
||||
// redo step 7, in case it got skipped last time and its objects are
|
||||
// still in memory
|
||||
this.removeFileObjects();
|
||||
// create fileReader
|
||||
this.fileReader = new FileReader();
|
||||
this.fileReader.onload = this.onload;
|
||||
// create <input> element and add it to DOM
|
||||
this.inputElement = document.createElement('input');
|
||||
this.inputElement.accept = '.sb,.sb2,.sb3';
|
||||
this.inputElement.style = 'display: none;';
|
||||
this.inputElement.type = 'file';
|
||||
this.inputElement.onchange = this.handleChange; // connects to step 3
|
||||
document.body.appendChild(this.inputElement);
|
||||
// simulate a click to open file chooser dialog
|
||||
this.inputElement.click();
|
||||
}
|
||||
// step 3: user has picked a file using the file chooser dialog.
|
||||
// We don't actually load the file here, we only decide whether to do so.
|
||||
handleChange (e) {
|
||||
const {
|
||||
intl,
|
||||
isShowingWithoutId,
|
||||
loadingState,
|
||||
projectChanged,
|
||||
userOwnsProject
|
||||
} = this.props;
|
||||
const thisFileInput = e.target;
|
||||
if (thisFileInput.files) { // Don't attempt to load if no file was selected
|
||||
this.fileToUpload = thisFileInput.files[0];
|
||||
|
||||
// If user owns the project, or user has changed the project,
|
||||
// we must confirm with the user that they really intend to
|
||||
// replace it. (If they don't own the project and haven't
|
||||
// changed it, no need to confirm.)
|
||||
let uploadAllowed = true;
|
||||
if (userOwnsProject || (projectChanged && isShowingWithoutId)) {
|
||||
uploadAllowed = confirm( // eslint-disable-line no-alert
|
||||
intl.formatMessage(sharedMessages.replaceProjectWarning)
|
||||
);
|
||||
}
|
||||
if (uploadAllowed) {
|
||||
// cues step 4
|
||||
this.props.requestProjectUpload(loadingState);
|
||||
} else {
|
||||
// skips ahead to step 7
|
||||
this.removeFileObjects();
|
||||
}
|
||||
this.props.closeFileMenu();
|
||||
}
|
||||
}
|
||||
// step 4 is below, in mapDispatchToProps
|
||||
|
||||
// step 5: called from componentDidUpdate when project state shows
|
||||
// that project data has finished "uploading" into the browser
|
||||
handleFinishedLoadingUpload () {
|
||||
if (this.fileToUpload && this.fileReader) {
|
||||
// begin to read data from the file. When finished,
|
||||
// cues step 6 using the reader's onload callback
|
||||
this.fileReader.readAsArrayBuffer(this.fileToUpload);
|
||||
} else {
|
||||
this.props.cancelFileUpload(this.props.loadingState);
|
||||
// skip ahead to step 7
|
||||
this.removeFileObjects();
|
||||
}
|
||||
}
|
||||
// used in step 6 below
|
||||
getProjectTitleFromFilename (fileInputFilename) {
|
||||
if (!fileInputFilename) return '';
|
||||
// only parse title with valid scratch project extensions
|
||||
// (.sb, .sb2, and .sb3)
|
||||
const matches = fileInputFilename.match(/^(.*)\.sb[23]?$/);
|
||||
if (!matches) return '';
|
||||
return matches[1].substring(0, 100); // truncate project title to max 100 chars
|
||||
}
|
||||
// step 6: attached as a handler on our FileReader object; called when
|
||||
// file upload raw data is available in the reader
|
||||
onload () {
|
||||
if (this.fileReader) {
|
||||
this.props.onLoadingStarted();
|
||||
const filename = this.fileToUpload && this.fileToUpload.name;
|
||||
this.props.vm.loadProject(this.fileReader.result)
|
||||
.then(() => {
|
||||
this.props.onLoadingFinished(this.props.loadingState, true);
|
||||
if (filename) {
|
||||
const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
|
||||
this.props.onUpdateProjectTitle(uploadedProjectTitle);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
log.warn(error);
|
||||
this.props.intl.formatMessage(messages.loadError);
|
||||
this.props.onLoadingFinished(this.props.loadingState, false);
|
||||
})
|
||||
.then(() => {
|
||||
// go back to step 7: whether project loading succeeded
|
||||
// or failed, reset file objects
|
||||
this.removeFileObjects();
|
||||
});
|
||||
}
|
||||
}
|
||||
// step 7: remove the <input> element from the DOM and clear reader and
|
||||
// fileToUpload reference, so those objects can be garbage collected
|
||||
removeFileObjects () {
|
||||
if (this.inputElement) {
|
||||
this.inputElement.value = null;
|
||||
document.body.removeChild(this.inputElement);
|
||||
}
|
||||
this.inputElement = null;
|
||||
this.fileReader = null;
|
||||
this.fileToUpload = null;
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
/* eslint-disable no-unused-vars */
|
||||
cancelFileUpload,
|
||||
closeFileMenu: closeFileMenuProp,
|
||||
isLoadingUpload,
|
||||
isShowingWithoutId,
|
||||
loadingState,
|
||||
onLoadingFinished,
|
||||
onLoadingStarted,
|
||||
projectChanged,
|
||||
requestProjectUpload: requestProjectUploadProp,
|
||||
userOwnsProject,
|
||||
/* eslint-enable no-unused-vars */
|
||||
...componentProps
|
||||
} = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<WrappedComponent
|
||||
onStartSelectingFileUpload={this.handleStartSelectingFileUpload}
|
||||
{...componentProps}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SBFileUploaderComponent.propTypes = {
|
||||
canSave: PropTypes.bool,
|
||||
cancelFileUpload: PropTypes.func,
|
||||
closeFileMenu: PropTypes.func,
|
||||
intl: intlShape.isRequired,
|
||||
isLoadingUpload: PropTypes.bool,
|
||||
isShowingWithoutId: PropTypes.bool,
|
||||
loadingState: PropTypes.oneOf(LoadingStates),
|
||||
onLoadingFinished: PropTypes.func,
|
||||
onLoadingStarted: PropTypes.func,
|
||||
onUpdateProjectTitle: PropTypes.func,
|
||||
projectChanged: PropTypes.bool,
|
||||
requestProjectUpload: PropTypes.func,
|
||||
userOwnsProject: PropTypes.bool,
|
||||
vm: PropTypes.shape({
|
||||
loadProject: PropTypes.func
|
||||
})
|
||||
};
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const loadingState = state.scratchGui.projectState.loadingState;
|
||||
const user = state.session && state.session.session && state.session.session.user;
|
||||
return {
|
||||
isLoadingUpload: getIsLoadingUpload(loadingState),
|
||||
isShowingWithoutId: getIsShowingWithoutId(loadingState),
|
||||
loadingState: loadingState,
|
||||
projectChanged: state.scratchGui.projectChanged,
|
||||
userOwnsProject: ownProps.authorUsername && user &&
|
||||
(ownProps.authorUsername === user.username),
|
||||
vm: state.scratchGui.vm
|
||||
};
|
||||
};
|
||||
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
cancelFileUpload: loadingState => dispatch(onLoadedProject(loadingState, false, false)),
|
||||
closeFileMenu: () => dispatch(closeFileMenu()),
|
||||
// transition project state from loading to regular, and close
|
||||
// loading screen and file menu
|
||||
onLoadingFinished: (loadingState, success) => {
|
||||
dispatch(onLoadedProject(loadingState, ownProps.canSave, success));
|
||||
dispatch(closeLoadingProject());
|
||||
dispatch(closeFileMenu());
|
||||
},
|
||||
// show project loading screen
|
||||
onLoadingStarted: () => dispatch(openLoadingProject()),
|
||||
// step 4: transition the project state so we're ready to handle the new
|
||||
// project data. When this is done, the project state transition will be
|
||||
// noticed by componentDidUpdate()
|
||||
requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState))
|
||||
});
|
||||
// Allow incoming props to override redux-provided props. Used to mock in tests.
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
|
||||
{}, stateProps, dispatchProps, ownProps
|
||||
);
|
||||
return injectIntl(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(SBFileUploaderComponent));
|
||||
};
|
||||
|
||||
export {
|
||||
SBFileUploaderHOC as default
|
||||
};
|
||||
@@ -75,7 +75,6 @@ describe('Menu bar settings', () => {
|
||||
test('User is not warned before uploading project file over a fresh project', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
|
||||
// No replace alert since no changes were made
|
||||
@@ -90,7 +89,6 @@ describe('Menu bar settings', () => {
|
||||
await clickText('delete', scope.spriteTile);
|
||||
|
||||
await clickText('File');
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
|
||||
await driver.switchTo().alert()
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
|
||||
import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import SBFileUploader from '../../../src/containers/sb-file-uploader';
|
||||
import {LoadingState} from '../../../src/reducers/project-state';
|
||||
|
||||
jest.mock('react-ga'); // must mock this entire library, or lib/analytics causes error
|
||||
|
||||
describe('SBFileUploader Container', () => {
|
||||
const mockStore = configureStore();
|
||||
let onLoadingFinished;
|
||||
let onLoadingStarted;
|
||||
let store;
|
||||
|
||||
// Wrap this in a function so it gets test specific states and can be reused.
|
||||
const getContainer = function () {
|
||||
return (
|
||||
<SBFileUploader
|
||||
onLoadingFinished={onLoadingFinished}
|
||||
onLoadingStarted={onLoadingStarted}
|
||||
>
|
||||
{(renderFileInput, loadProject) => (
|
||||
<div
|
||||
onClick={loadProject}
|
||||
/>
|
||||
)}
|
||||
</SBFileUploader>
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({
|
||||
scratchGui: {
|
||||
projectState: {
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
},
|
||||
vm: {}
|
||||
}
|
||||
});
|
||||
onLoadingFinished = jest.fn();
|
||||
onLoadingStarted = jest.fn();
|
||||
});
|
||||
|
||||
test('correctly sets title with .sb3 filename', () => {
|
||||
const wrapper = shallowWithIntl(getContainer(), {context: {store}});
|
||||
const instance = wrapper
|
||||
.dive() // unwrap redux Connect(InjectIntl(SBFileUploader))
|
||||
.dive() // unwrap InjectIntl(SBFileUploader)
|
||||
.instance(); // SBFileUploader
|
||||
const projectName = instance.getProjectTitleFromFilename('my project is great.sb3');
|
||||
expect(projectName).toBe('my project is great');
|
||||
});
|
||||
|
||||
test('correctly sets title with .sb2 filename', () => {
|
||||
const wrapper = shallowWithIntl(getContainer(), {context: {store}});
|
||||
const instance = wrapper
|
||||
.dive() // unwrap redux Connect(InjectIntl(SBFileUploader))
|
||||
.dive() // unwrap InjectIntl(SBFileUploader)
|
||||
.instance(); // SBFileUploader
|
||||
const projectName = instance.getProjectTitleFromFilename('my project is great.sb2');
|
||||
expect(projectName).toBe('my project is great');
|
||||
});
|
||||
|
||||
test('correctly sets title with .sb filename', () => {
|
||||
const wrapper = shallowWithIntl(getContainer(), {context: {store}});
|
||||
const instance = wrapper
|
||||
.dive() // unwrap redux Connect(InjectIntl(SBFileUploader))
|
||||
.dive() // unwrap InjectIntl(SBFileUploader)
|
||||
.instance(); // SBFileUploader
|
||||
const projectName = instance.getProjectTitleFromFilename('my project is great.sb');
|
||||
expect(projectName).toBe('my project is great');
|
||||
});
|
||||
|
||||
test('sets blank title with filename with no extension', () => {
|
||||
const wrapper = shallowWithIntl(getContainer(), {context: {store}});
|
||||
const instance = wrapper
|
||||
.dive() // unwrap redux Connect(InjectIntl(SBFileUploader))
|
||||
.dive() // unwrap InjectIntl(SBFileUploader)
|
||||
.instance(); // SBFileUploader
|
||||
const projectName = instance.getProjectTitleFromFilename('my project is great');
|
||||
expect(projectName).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
import 'web-audio-test-api';
|
||||
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {mountWithIntl, shallowWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import {LoadingState} from '../../../src/reducers/project-state';
|
||||
import VM from 'scratch-vm';
|
||||
|
||||
import SBFileUploaderHOC from '../../../src/lib/sb-file-uploader-hoc.jsx';
|
||||
|
||||
describe('SBFileUploaderHOC', () => {
|
||||
const mockStore = configureStore();
|
||||
let store;
|
||||
let vm;
|
||||
|
||||
// Wrap this in a function so it gets test specific states and can be reused.
|
||||
const getContainer = function () {
|
||||
const Component = () => <div />;
|
||||
return SBFileUploaderHOC(Component);
|
||||
};
|
||||
|
||||
const shallowMountWithContext = component => (
|
||||
shallowWithIntl(component, {context: {store}})
|
||||
);
|
||||
|
||||
const unwrappedInstance = () => {
|
||||
const WrappedComponent = getContainer();
|
||||
// default starting state: looking at a project you created, not logged in
|
||||
const wrapper = shallowMountWithContext(
|
||||
<WrappedComponent
|
||||
projectChanged
|
||||
canSave={false}
|
||||
cancelFileUpload={jest.fn()}
|
||||
closeFileMenu={jest.fn()}
|
||||
requestProjectUpload={jest.fn()}
|
||||
userOwnsProject={false}
|
||||
vm={vm}
|
||||
onLoadingFinished={jest.fn()}
|
||||
onLoadingStarted={jest.fn()}
|
||||
onUpdateProjectTitle={jest.fn()}
|
||||
/>
|
||||
);
|
||||
return wrapper
|
||||
.dive() // unwrap intl
|
||||
.dive() // unwrap redux Connect(SBFileUploaderComponent)
|
||||
.instance(); // SBFileUploaderComponent
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vm = new VM();
|
||||
store = mockStore({
|
||||
scratchGui: {
|
||||
projectState: {
|
||||
loadingState: LoadingState.SHOWING_WITHOUT_ID
|
||||
},
|
||||
vm: {}
|
||||
},
|
||||
locales: {
|
||||
locale: 'en'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('correctly sets title with .sb3 filename', () => {
|
||||
const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb3');
|
||||
expect(projectName).toBe('my project is great');
|
||||
});
|
||||
|
||||
test('correctly sets title with .sb2 filename', () => {
|
||||
const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb2');
|
||||
expect(projectName).toBe('my project is great');
|
||||
});
|
||||
|
||||
test('correctly sets title with .sb filename', () => {
|
||||
const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb');
|
||||
expect(projectName).toBe('my project is great');
|
||||
});
|
||||
|
||||
test('sets blank title with filename with no extension', () => {
|
||||
const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great');
|
||||
expect(projectName).toBe('');
|
||||
});
|
||||
|
||||
test('if isLoadingUpload becomes true, without fileToUpload set, will call cancelFileUpload', () => {
|
||||
const mockedCancelFileUpload = jest.fn();
|
||||
const WrappedComponent = getContainer();
|
||||
const mounted = mountWithIntl(
|
||||
<WrappedComponent
|
||||
projectChanged
|
||||
canSave={false}
|
||||
cancelFileUpload={mockedCancelFileUpload}
|
||||
closeFileMenu={jest.fn()}
|
||||
isLoadingUpload={false}
|
||||
requestProjectUpload={jest.fn()}
|
||||
store={store}
|
||||
userOwnsProject={false}
|
||||
vm={vm}
|
||||
onLoadingFinished={jest.fn()}
|
||||
onLoadingStarted={jest.fn()}
|
||||
onUpdateProjectTitle={jest.fn()}
|
||||
/>
|
||||
);
|
||||
mounted.setProps({
|
||||
isLoadingUpload: true
|
||||
});
|
||||
expect(mockedCancelFileUpload).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user