Skip to content

Commit b3e97ad

Browse files
authored
Merge pull request #4134 from benjiwheeler/save-before-upload
When user uploads project file, project will auto-save before loading
2 parents 369cdd2 + f5e2d3d commit b3e97ad

File tree

4 files changed

+191
-77
lines changed

4 files changed

+191
-77
lines changed

src/containers/sb-file-uploader.jsx

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl';
66

77
import analytics from '../lib/analytics';
88
import log from '../lib/log';
9-
import {LoadingStates, onLoadedProject, onProjectUploadStarted} from '../reducers/project-state';
9+
import {
10+
LoadingStates,
11+
getIsLoadingUpload,
12+
onLoadedProject,
13+
requestProjectUpload
14+
} from '../reducers/project-state';
1015

1116
import {
1217
openLoadingProject,
@@ -48,9 +53,31 @@ class SBFileUploader extends React.Component {
4853
'renderFileInput',
4954
'setFileInput',
5055
'handleChange',
51-
'handleClick'
56+
'handleClick',
57+
'onload',
58+
'resetFileInput'
5259
]);
5360
}
61+
componentWillMount () {
62+
this.reader = new FileReader();
63+
this.reader.onload = this.onload;
64+
this.resetFileInput();
65+
}
66+
componentDidUpdate (prevProps) {
67+
if (this.props.isLoadingUpload && !prevProps.isLoadingUpload && this.fileToUpload && this.reader) {
68+
this.reader.readAsArrayBuffer(this.fileToUpload);
69+
}
70+
}
71+
componentWillUnmount () {
72+
this.reader = null;
73+
this.resetFileInput();
74+
}
75+
resetFileInput () {
76+
this.fileToUpload = null;
77+
if (this.fileInput) {
78+
this.fileInput.value = null;
79+
}
80+
}
5481
getProjectTitleFromFilename (fileInputFilename) {
5582
if (!fileInputFilename) return '';
5683
// only parse title from files like "filename.sb2" or "filename.sb3"
@@ -60,35 +87,43 @@ class SBFileUploader extends React.Component {
6087
}
6188
// called when user has finished selecting a file to upload
6289
handleChange (e) {
63-
// Remove the hash if any (without triggering a hash change event or a reload)
64-
history.replaceState({}, document.title, '.');
65-
const reader = new FileReader();
6690
const thisFileInput = e.target;
67-
reader.onload = () => this.props.vm.loadProject(reader.result)
68-
.then(() => {
69-
analytics.event({
70-
category: 'project',
71-
action: 'Import Project File',
72-
nonInteraction: true
73-
});
74-
this.props.onLoadingFinished(this.props.loadingState);
75-
// Reset the file input after project is loaded
76-
// This is necessary in case the user wants to reload a project
77-
thisFileInput.value = null;
78-
})
79-
.catch(error => {
80-
log.warn(error);
81-
alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert
82-
this.props.onLoadingFinished(this.props.loadingState);
83-
// Reset the file input after project is loaded
84-
// This is necessary in case the user wants to reload a project
85-
thisFileInput.value = null;
86-
});
8791
if (thisFileInput.files) { // Don't attempt to load if no file was selected
92+
this.fileToUpload = thisFileInput.files[0];
93+
this.props.requestProjectUpload(this.props.loadingState);
94+
}
95+
}
96+
// called when file upload raw data is available in the reader
97+
onload () {
98+
if (this.reader) {
8899
this.props.onLoadingStarted();
89-
reader.readAsArrayBuffer(thisFileInput.files[0]);
90-
const uploadedProjectTitle = this.getProjectTitleFromFilename(thisFileInput.files[0].name);
91-
this.props.onUpdateProjectTitle(uploadedProjectTitle);
100+
const filename = this.fileToUpload && this.fileToUpload.name;
101+
this.props.vm.loadProject(this.reader.result)
102+
.then(() => {
103+
analytics.event({
104+
category: 'project',
105+
action: 'Import Project File',
106+
nonInteraction: true
107+
});
108+
// Remove the hash if any (without triggering a hash change event or a reload)
109+
history.replaceState({}, document.title, '.');
110+
this.props.onLoadingFinished(this.props.loadingState, true);
111+
// Reset the file input after project is loaded
112+
// This is necessary in case the user wants to reload a project
113+
if (filename) {
114+
const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
115+
this.props.onUpdateProjectTitle(uploadedProjectTitle);
116+
}
117+
this.resetFileInput();
118+
})
119+
.catch(error => {
120+
log.warn(error);
121+
alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert
122+
this.props.onLoadingFinished(this.props.loadingState, false);
123+
// Reset the file input after project is loaded
124+
// This is necessary in case the user wants to reload a project
125+
this.resetFileInput();
126+
});
92127
}
93128
}
94129
handleClick () {
@@ -119,32 +154,36 @@ SBFileUploader.propTypes = {
119154
children: PropTypes.func,
120155
className: PropTypes.string,
121156
intl: intlShape.isRequired,
157+
isLoadingUpload: PropTypes.bool,
122158
loadingState: PropTypes.oneOf(LoadingStates),
123159
onLoadingFinished: PropTypes.func,
124160
onLoadingStarted: PropTypes.func,
125161
onUpdateProjectTitle: PropTypes.func,
162+
requestProjectUpload: PropTypes.func,
126163
vm: PropTypes.shape({
127164
loadProject: PropTypes.func
128165
})
129166
};
130167
SBFileUploader.defaultProps = {
131168
className: ''
132169
};
133-
const mapStateToProps = state => ({
134-
loadingState: state.scratchGui.projectState.loadingState,
135-
vm: state.scratchGui.vm
136-
});
170+
const mapStateToProps = state => {
171+
const loadingState = state.scratchGui.projectState.loadingState;
172+
return {
173+
isLoadingUpload: getIsLoadingUpload(loadingState),
174+
loadingState: loadingState,
175+
vm: state.scratchGui.vm
176+
};
177+
};
137178

138179
const mapDispatchToProps = (dispatch, ownProps) => ({
139-
onLoadingFinished: loadingState => {
140-
dispatch(onLoadedProject(loadingState, ownProps.canSave));
180+
onLoadingFinished: (loadingState, success) => {
181+
dispatch(onLoadedProject(loadingState, ownProps.canSave, success));
141182
dispatch(closeLoadingProject());
142183
dispatch(closeFileMenu());
143184
},
144-
onLoadingStarted: () => {
145-
dispatch(openLoadingProject());
146-
dispatch(onProjectUploadStarted());
147-
}
185+
requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)),
186+
onLoadingStarted: () => dispatch(openLoadingProject())
148187
});
149188

150189
// Allow incoming props to override redux-provided props. Used to mock in tests.

src/lib/vm-manager-hoc.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const vmManagerHOC = function (WrappedComponent) {
129129
const mapDispatchToProps = dispatch => ({
130130
onError: error => dispatch(projectError(error)),
131131
onLoadedProject: (loadingState, canSave) =>
132-
dispatch(onLoadedProject(loadingState, canSave)),
132+
dispatch(onLoadedProject(loadingState, canSave, true)),
133133
onSetProjectUnchanged: () => dispatch(setProjectUnchanged())
134134
});
135135

src/reducers/project-state.js

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ const DONE_LOADING_VM_WITHOUT_ID = 'scratch-gui/project-state/DONE_LOADING_VM_WI
1010
const DONE_REMIXING = 'scratch-gui/project-state/DONE_REMIXING';
1111
const DONE_UPDATING = 'scratch-gui/project-state/DONE_UPDATING';
1212
const DONE_UPDATING_BEFORE_COPY = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_COPY';
13+
const DONE_UPDATING_BEFORE_FILE_UPLOAD = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_FILE_UPLOAD';
1314
const DONE_UPDATING_BEFORE_NEW = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_NEW';
15+
const RETURN_TO_SHOWING = 'scratch-gui/project-state/RETURN_TO_SHOWING';
1416
const SET_PROJECT_ID = 'scratch-gui/project-state/SET_PROJECT_ID';
1517
const START_AUTO_UPDATING = 'scratch-gui/project-state/START_AUTO_UPDATING';
1618
const START_CREATING_NEW = 'scratch-gui/project-state/START_CREATING_NEW';
@@ -21,6 +23,7 @@ const START_MANUAL_UPDATING = 'scratch-gui/project-state/START_MANUAL_UPDATING';
2123
const START_REMIXING = 'scratch-gui/project-state/START_REMIXING';
2224
const START_UPDATING_BEFORE_CREATING_COPY = 'scratch-gui/project-state/START_UPDATING_BEFORE_CREATING_COPY';
2325
const START_UPDATING_BEFORE_CREATING_NEW = 'scratch-gui/project-state/START_UPDATING_BEFORE_CREATING_NEW';
26+
const START_UPDATING_BEFORE_FILE_UPLOAD = 'scratch-gui/project-state/START_UPDATING_BEFORE_FILE_UPLOAD';
2427

2528
const defaultProjectId = '0'; // hardcoded id of default project
2629

@@ -40,6 +43,7 @@ const LoadingState = keyMirror({
4043
SHOWING_WITH_ID: null,
4144
SHOWING_WITHOUT_ID: null,
4245
UPDATING_BEFORE_COPY: null,
46+
UPDATING_BEFORE_FILE_UPLOAD: null,
4347
UPDATING_BEFORE_NEW: null
4448
});
4549

@@ -63,6 +67,9 @@ const getIsLoading = loadingState => (
6367
loadingState === LoadingState.LOADING_VM_WITH_ID ||
6468
loadingState === LoadingState.LOADING_VM_NEW_DEFAULT
6569
);
70+
const getIsLoadingUpload = loadingState => (
71+
loadingState === LoadingState.LOADING_VM_FILE_UPLOAD
72+
);
6673
const getIsCreatingNew = loadingState => (
6774
loadingState === LoadingState.CREATING_NEW
6875
);
@@ -84,6 +91,7 @@ const getIsUpdating = loadingState => (
8491
loadingState === LoadingState.AUTO_UPDATING ||
8592
loadingState === LoadingState.MANUAL_UPDATING ||
8693
loadingState === LoadingState.UPDATING_BEFORE_COPY ||
94+
loadingState === LoadingState.UPDATING_BEFORE_FILE_UPLOAD ||
8795
loadingState === LoadingState.UPDATING_BEFORE_NEW
8896
);
8997
const getIsShowingProject = loadingState => (
@@ -141,7 +149,8 @@ const reducer = function (state, action) {
141149
if (state.loadingState === LoadingState.LOADING_VM_FILE_UPLOAD ||
142150
state.loadingState === LoadingState.LOADING_VM_NEW_DEFAULT) {
143151
return Object.assign({}, state, {
144-
loadingState: LoadingState.SHOWING_WITHOUT_ID
152+
loadingState: LoadingState.SHOWING_WITHOUT_ID,
153+
projectId: defaultProjectId
145154
});
146155
}
147156
return state;
@@ -194,6 +203,13 @@ const reducer = function (state, action) {
194203
});
195204
}
196205
return state;
206+
case DONE_UPDATING_BEFORE_FILE_UPLOAD:
207+
if (state.loadingState === LoadingState.UPDATING_BEFORE_FILE_UPLOAD) {
208+
return Object.assign({}, state, {
209+
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
210+
});
211+
}
212+
return state;
197213
case DONE_UPDATING_BEFORE_NEW:
198214
if (state.loadingState === LoadingState.UPDATING_BEFORE_NEW) {
199215
return Object.assign({}, state, {
@@ -202,6 +218,16 @@ const reducer = function (state, action) {
202218
});
203219
}
204220
return state;
221+
case RETURN_TO_SHOWING:
222+
if (state.projectId === null || state.projectId === defaultProjectId) {
223+
return Object.assign({}, state, {
224+
loadingState: LoadingState.SHOWING_WITHOUT_ID,
225+
projectId: defaultProjectId
226+
});
227+
}
228+
return Object.assign({}, state, {
229+
loadingState: LoadingState.SHOWING_WITH_ID
230+
});
205231
case SET_PROJECT_ID:
206232
// if the projectId hasn't actually changed do nothing
207233
if (state.projectId === action.projectId) {
@@ -275,8 +301,7 @@ const reducer = function (state, action) {
275301
LoadingState.SHOWING_WITHOUT_ID
276302
].includes(state.loadingState)) {
277303
return Object.assign({}, state, {
278-
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD,
279-
projectId: null // clear any current projectId
304+
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
280305
});
281306
}
282307
return state;
@@ -308,6 +333,13 @@ const reducer = function (state, action) {
308333
});
309334
}
310335
return state;
336+
case START_UPDATING_BEFORE_FILE_UPLOAD:
337+
if (state.loadingState === LoadingState.SHOWING_WITH_ID) {
338+
return Object.assign({}, state, {
339+
loadingState: LoadingState.UPDATING_BEFORE_FILE_UPLOAD
340+
});
341+
}
342+
return state;
311343
case START_ERROR:
312344
// fatal errors: there's no correct editor state for us to show
313345
if ([
@@ -328,6 +360,7 @@ const reducer = function (state, action) {
328360
LoadingState.MANUAL_UPDATING,
329361
LoadingState.REMIXING,
330362
LoadingState.UPDATING_BEFORE_COPY,
363+
LoadingState.UPDATING_BEFORE_FILE_UPLOAD,
331364
LoadingState.UPDATING_BEFORE_NEW
332365
].includes(state.loadingState)) {
333366
return Object.assign({}, state, {
@@ -398,28 +431,33 @@ const onFetchedProjectData = (projectData, loadingState) => {
398431
}
399432
};
400433

401-
const onLoadedProject = (loadingState, canSave) => {
402-
switch (loadingState) {
403-
case LoadingState.LOADING_VM_WITH_ID:
404-
return {
405-
type: DONE_LOADING_VM_WITH_ID
406-
};
407-
case LoadingState.LOADING_VM_FILE_UPLOAD:
408-
if (canSave) {
434+
const onLoadedProject = (loadingState, canSave, success) => {
435+
if (success) {
436+
switch (loadingState) {
437+
case LoadingState.LOADING_VM_WITH_ID:
438+
return {
439+
type: DONE_LOADING_VM_WITH_ID
440+
};
441+
case LoadingState.LOADING_VM_FILE_UPLOAD:
442+
if (canSave) {
443+
return {
444+
type: DONE_LOADING_VM_TO_SAVE
445+
};
446+
}
409447
return {
410-
type: DONE_LOADING_VM_TO_SAVE
448+
type: DONE_LOADING_VM_WITHOUT_ID
411449
};
450+
case LoadingState.LOADING_VM_NEW_DEFAULT:
451+
return {
452+
type: DONE_LOADING_VM_WITHOUT_ID
453+
};
454+
default:
455+
break;
412456
}
413-
return {
414-
type: DONE_LOADING_VM_WITHOUT_ID
415-
};
416-
case LoadingState.LOADING_VM_NEW_DEFAULT:
417-
return {
418-
type: DONE_LOADING_VM_WITHOUT_ID
419-
};
420-
default:
421-
break;
422457
}
458+
return {
459+
type: RETURN_TO_SHOWING
460+
};
423461
};
424462

425463
const doneUpdatingProject = loadingState => {
@@ -433,6 +471,10 @@ const doneUpdatingProject = loadingState => {
433471
return {
434472
type: DONE_UPDATING_BEFORE_COPY
435473
};
474+
case LoadingState.UPDATING_BEFORE_FILE_UPLOAD:
475+
return {
476+
type: DONE_UPDATING_BEFORE_FILE_UPLOAD
477+
};
436478
case LoadingState.UPDATING_BEFORE_NEW:
437479
return {
438480
type: DONE_UPDATING_BEFORE_NEW
@@ -457,9 +499,21 @@ const requestNewProject = needSave => {
457499
return {type: START_FETCHING_NEW};
458500
};
459501

460-
const onProjectUploadStarted = () => ({
461-
type: START_LOADING_VM_FILE_UPLOAD
462-
});
502+
const requestProjectUpload = loadingState => {
503+
switch (loadingState) {
504+
case LoadingState.SHOWING_WITH_ID:
505+
return {
506+
type: START_UPDATING_BEFORE_FILE_UPLOAD
507+
};
508+
case LoadingState.NOT_LOADED:
509+
case LoadingState.SHOWING_WITHOUT_ID:
510+
return {
511+
type: START_LOADING_VM_FILE_UPLOAD
512+
};
513+
default:
514+
break;
515+
}
516+
};
463517

464518
const autoUpdateProject = () => ({
465519
type: START_AUTO_UPDATING
@@ -495,6 +549,7 @@ export {
495549
getIsFetchingWithoutId,
496550
getIsLoading,
497551
getIsLoadingWithId,
552+
getIsLoadingUpload,
498553
getIsManualUpdating,
499554
getIsRemixing,
500555
getIsShowingProject,
@@ -504,10 +559,10 @@ export {
504559
manualUpdateProject,
505560
onFetchedProjectData,
506561
onLoadedProject,
507-
onProjectUploadStarted,
508562
projectError,
509563
remixProject,
510564
requestNewProject,
565+
requestProjectUpload,
511566
saveProjectAsCopy,
512567
setProjectId
513568
};

0 commit comments

Comments
 (0)