Skip to content

Commit af4ad81

Browse files
authored
Revert "refactor SB* file uploader into top-level HOC"
1 parent c368de8 commit af4ad81

File tree

8 files changed

+326
-389
lines changed

8 files changed

+326
-389
lines changed

src/components/gui/gui.jsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ const GUIComponent = props => {
105105
onRequestCloseTelemetryModal,
106106
onSeeCommunity,
107107
onShare,
108-
onStartSelectingFileUpload,
109108
onTelemetryModalCancel,
110109
onTelemetryModalOptIn,
111110
onTelemetryModalOptOut,
@@ -227,7 +226,6 @@ const GUIComponent = props => {
227226
onProjectTelemetryEvent={onProjectTelemetryEvent}
228227
onSeeCommunity={onSeeCommunity}
229228
onShare={onShare}
230-
onStartSelectingFileUpload={onStartSelectingFileUpload}
231229
onToggleLoginOpen={onToggleLoginOpen}
232230
/>
233231
<Box className={styles.bodyWrapper}>
@@ -401,7 +399,6 @@ GUIComponent.propTypes = {
401399
onRequestCloseTelemetryModal: PropTypes.func,
402400
onSeeCommunity: PropTypes.func,
403401
onShare: PropTypes.func,
404-
onStartSelectingFileUpload: PropTypes.func,
405402
onTabSelect: PropTypes.func,
406403
onTelemetryModalCancel: PropTypes.func,
407404
onTelemetryModalOptIn: PropTypes.func,

src/components/menu-bar/menu-bar.jsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
1717
import Divider from '../divider/divider.jsx';
1818
import LanguageSelector from '../../containers/language-selector.jsx';
1919
import SaveStatus from './save-status.jsx';
20+
import SBFileUploader from '../../containers/sb-file-uploader.jsx';
2021
import ProjectWatcher from '../../containers/project-watcher.jsx';
2122
import MenuBarMenu from './menu-bar-menu.jsx';
2223
import {MenuItem, MenuSection} from '../menu/menu.jsx';
@@ -391,11 +392,22 @@ class MenuBar extends React.Component {
391392
</MenuSection>
392393
)}
393394
<MenuSection>
394-
<MenuItem
395-
onClick={this.props.onStartSelectingFileUpload}
395+
<SBFileUploader
396+
canSave={this.props.canSave}
397+
userOwnsProject={this.props.userOwnsProject}
396398
>
397-
{this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
398-
</MenuItem>
399+
{(className, renderFileInput, handleLoadProject) => (
400+
<MenuItem
401+
className={className}
402+
onClick={handleLoadProject}
403+
>
404+
{/* eslint-disable max-len */}
405+
{this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
406+
{/* eslint-enable max-len */}
407+
{renderFileInput()}
408+
</MenuItem>
409+
)}
410+
</SBFileUploader>
399411
<SB3Downloader>{(className, downloadProjectCallback) => (
400412
<MenuItem
401413
className={className}
@@ -731,7 +743,6 @@ MenuBar.propTypes = {
731743
onRequestCloseLogin: PropTypes.func,
732744
onSeeCommunity: PropTypes.func,
733745
onShare: PropTypes.func,
734-
onStartSelectingFileUpload: PropTypes.func,
735746
onToggleLoginOpen: PropTypes.func,
736747
projectTitle: PropTypes.string,
737748
renderLogin: PropTypes.func,

src/containers/gui.jsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727

2828
import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
2929
import LocalizationHOC from '../lib/localization-hoc.jsx';
30-
import SBFileUploaderHOC from '../lib/sb-file-uploader-hoc.jsx';
3130
import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
3231
import TitledHOC from '../lib/titled-hoc.jsx';
3332
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
@@ -182,7 +181,6 @@ const WrappedGui = compose(
182181
ProjectSaverHOC,
183182
vmListenerHOC,
184183
vmManagerHOC,
185-
SBFileUploaderHOC,
186184
cloudManagerHOC
187185
)(ConnectedGUI);
188186

src/containers/sb-file-uploader.jsx

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import bindAll from 'lodash.bindall';
2+
import PropTypes from 'prop-types';
3+
import React from 'react';
4+
import {connect} from 'react-redux';
5+
import {defineMessages, injectIntl, intlShape} from 'react-intl';
6+
import {setProjectTitle} from '../reducers/project-title';
7+
8+
import log from '../lib/log';
9+
import sharedMessages from '../lib/shared-messages';
10+
11+
import {
12+
LoadingStates,
13+
getIsLoadingUpload,
14+
getIsShowingWithoutId,
15+
onLoadedProject,
16+
requestProjectUpload
17+
} from '../reducers/project-state';
18+
19+
import {
20+
openLoadingProject,
21+
closeLoadingProject
22+
} from '../reducers/modals';
23+
import {
24+
closeFileMenu
25+
} from '../reducers/menus';
26+
27+
/**
28+
* SBFileUploader component passes a file input, load handler and props to its child.
29+
* It expects this child to be a function with the signature
30+
* function (renderFileInput, handleLoadProject) {}
31+
* The component can then be used to attach project loading functionality
32+
* to any other component:
33+
*
34+
* <SBFileUploader>{(className, renderFileInput, handleLoadProject) => (
35+
* <MyCoolComponent
36+
* className={className}
37+
* onClick={handleLoadProject}
38+
* >
39+
* {renderFileInput()}
40+
* </MyCoolComponent>
41+
* )}</SBFileUploader>
42+
*/
43+
44+
const messages = defineMessages({
45+
loadError: {
46+
id: 'gui.projectLoader.loadError',
47+
defaultMessage: 'The project file that was selected failed to load.',
48+
description: 'An error that displays when a local project file fails to load.'
49+
}
50+
});
51+
52+
class SBFileUploader extends React.Component {
53+
constructor (props) {
54+
super(props);
55+
bindAll(this, [
56+
'getProjectTitleFromFilename',
57+
'renderFileInput',
58+
'setFileInput',
59+
'handleChange',
60+
'handleClick',
61+
'onload',
62+
'resetFileInput'
63+
]);
64+
}
65+
componentWillMount () {
66+
this.reader = new FileReader();
67+
this.reader.onload = this.onload;
68+
this.resetFileInput();
69+
}
70+
componentDidUpdate (prevProps) {
71+
if (this.props.isLoadingUpload && !prevProps.isLoadingUpload && this.fileToUpload && this.reader) {
72+
this.reader.readAsArrayBuffer(this.fileToUpload);
73+
}
74+
}
75+
componentWillUnmount () {
76+
this.reader = null;
77+
this.resetFileInput();
78+
}
79+
resetFileInput () {
80+
this.fileToUpload = null;
81+
if (this.fileInput) {
82+
this.fileInput.value = null;
83+
}
84+
}
85+
getProjectTitleFromFilename (fileInputFilename) {
86+
if (!fileInputFilename) return '';
87+
// only parse title with valid scratch project extensions
88+
// (.sb, .sb2, and .sb3)
89+
const matches = fileInputFilename.match(/^(.*)\.sb[23]?$/);
90+
if (!matches) return '';
91+
return matches[1].substring(0, 100); // truncate project title to max 100 chars
92+
}
93+
// called when user has finished selecting a file to upload
94+
handleChange (e) {
95+
const {
96+
intl,
97+
isShowingWithoutId,
98+
loadingState,
99+
projectChanged,
100+
userOwnsProject
101+
} = this.props;
102+
103+
const thisFileInput = e.target;
104+
if (thisFileInput.files) { // Don't attempt to load if no file was selected
105+
this.fileToUpload = thisFileInput.files[0];
106+
107+
// If user owns the project, or user has changed the project,
108+
// we must confirm with the user that they really intend to replace it.
109+
// (If they don't own the project and haven't changed it, no need to confirm.)
110+
let uploadAllowed = true;
111+
if (userOwnsProject || (projectChanged && isShowingWithoutId)) {
112+
uploadAllowed = confirm( // eslint-disable-line no-alert
113+
intl.formatMessage(sharedMessages.replaceProjectWarning)
114+
);
115+
}
116+
if (uploadAllowed) {
117+
this.props.requestProjectUpload(loadingState);
118+
} else {
119+
this.props.closeFileMenu();
120+
}
121+
}
122+
}
123+
// called when file upload raw data is available in the reader
124+
onload () {
125+
if (this.reader) {
126+
this.props.onLoadingStarted();
127+
const filename = this.fileToUpload && this.fileToUpload.name;
128+
this.props.vm.loadProject(this.reader.result)
129+
.then(() => {
130+
this.props.onLoadingFinished(this.props.loadingState, true);
131+
// Reset the file input after project is loaded
132+
// This is necessary in case the user wants to reload a project
133+
if (filename) {
134+
const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
135+
this.props.onReceivedProjectTitle(uploadedProjectTitle);
136+
}
137+
this.resetFileInput();
138+
})
139+
.catch(error => {
140+
log.warn(error);
141+
alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert
142+
this.props.onLoadingFinished(this.props.loadingState, false);
143+
// Reset the file input after project is loaded
144+
// This is necessary in case the user wants to reload a project
145+
this.resetFileInput();
146+
});
147+
}
148+
}
149+
handleClick () {
150+
// open filesystem browsing window
151+
this.fileInput.click();
152+
}
153+
setFileInput (input) {
154+
this.fileInput = input;
155+
}
156+
renderFileInput () {
157+
return (
158+
<input
159+
accept=".sb,.sb2,.sb3"
160+
ref={this.setFileInput}
161+
style={{display: 'none'}}
162+
type="file"
163+
onChange={this.handleChange}
164+
/>
165+
);
166+
}
167+
render () {
168+
return this.props.children(this.props.className, this.renderFileInput, this.handleClick);
169+
}
170+
}
171+
172+
SBFileUploader.propTypes = {
173+
canSave: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
174+
children: PropTypes.func,
175+
className: PropTypes.string,
176+
closeFileMenu: PropTypes.func,
177+
intl: intlShape.isRequired,
178+
isLoadingUpload: PropTypes.bool,
179+
isShowingWithoutId: PropTypes.bool,
180+
loadingState: PropTypes.oneOf(LoadingStates),
181+
onLoadingFinished: PropTypes.func,
182+
onLoadingStarted: PropTypes.func,
183+
projectChanged: PropTypes.bool,
184+
requestProjectUpload: PropTypes.func,
185+
onReceivedProjectTitle: PropTypes.func,
186+
userOwnsProject: PropTypes.bool,
187+
vm: PropTypes.shape({
188+
loadProject: PropTypes.func
189+
})
190+
};
191+
SBFileUploader.defaultProps = {
192+
className: ''
193+
};
194+
const mapStateToProps = state => {
195+
const loadingState = state.scratchGui.projectState.loadingState;
196+
return {
197+
isLoadingUpload: getIsLoadingUpload(loadingState),
198+
isShowingWithoutId: getIsShowingWithoutId(loadingState),
199+
loadingState: loadingState,
200+
projectChanged: state.scratchGui.projectChanged,
201+
vm: state.scratchGui.vm
202+
};
203+
};
204+
205+
const mapDispatchToProps = (dispatch, ownProps) => ({
206+
closeFileMenu: () => dispatch(closeFileMenu()),
207+
onLoadingFinished: (loadingState, success) => {
208+
dispatch(onLoadedProject(loadingState, ownProps.canSave, success));
209+
dispatch(closeLoadingProject());
210+
dispatch(closeFileMenu());
211+
},
212+
requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)),
213+
onLoadingStarted: () => dispatch(openLoadingProject()),
214+
onReceivedProjectTitle: title => dispatch(setProjectTitle(title))
215+
});
216+
217+
// Allow incoming props to override redux-provided props. Used to mock in tests.
218+
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
219+
{}, stateProps, dispatchProps, ownProps
220+
);
221+
222+
export default connect(
223+
mapStateToProps,
224+
mapDispatchToProps,
225+
mergeProps
226+
)(injectIntl(SBFileUploader));

0 commit comments

Comments
 (0)