diff --git a/src/containers/sb-file-uploader.jsx b/src/containers/sb-file-uploader.jsx index 6bd0bfae490145fa999f9ab99df5ad840edbf7ab..68bb5ae907777a2340314f896c4882c06d327f0e 100644 --- a/src/containers/sb-file-uploader.jsx +++ b/src/containers/sb-file-uploader.jsx @@ -6,7 +6,12 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl'; import analytics from '../lib/analytics'; import log from '../lib/log'; -import {LoadingStates, onLoadedProject, onProjectUploadStarted} from '../reducers/project-state'; +import { + LoadingStates, + getIsLoadingUpload, + onLoadedProject, + requestProjectUpload +} from '../reducers/project-state'; import { openLoadingProject, @@ -48,9 +53,31 @@ class SBFileUploader extends React.Component { 'renderFileInput', 'setFileInput', 'handleChange', - 'handleClick' + '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 from files like "filename.sb2" or "filename.sb3" @@ -60,35 +87,43 @@ class SBFileUploader extends React.Component { } // called when user has finished selecting a file to upload handleChange (e) { - // Remove the hash if any (without triggering a hash change event or a reload) - history.replaceState({}, document.title, '.'); - const reader = new FileReader(); const thisFileInput = e.target; - reader.onload = () => this.props.vm.loadProject(reader.result) - .then(() => { - analytics.event({ - category: 'project', - action: 'Import Project File', - nonInteraction: true - }); - this.props.onLoadingFinished(this.props.loadingState); - // Reset the file input after project is loaded - // This is necessary in case the user wants to reload a project - thisFileInput.value = null; - }) - .catch(error => { - log.warn(error); - alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert - this.props.onLoadingFinished(this.props.loadingState); - // Reset the file input after project is loaded - // This is necessary in case the user wants to reload a project - thisFileInput.value = null; - }); if (thisFileInput.files) { // Don't attempt to load if no file was selected + this.fileToUpload = thisFileInput.files[0]; + this.props.requestProjectUpload(this.props.loadingState); + } + } + // called when file upload raw data is available in the reader + onload () { + if (this.reader) { this.props.onLoadingStarted(); - reader.readAsArrayBuffer(thisFileInput.files[0]); - const uploadedProjectTitle = this.getProjectTitleFromFilename(thisFileInput.files[0].name); - this.props.onUpdateProjectTitle(uploadedProjectTitle); + const filename = this.fileToUpload && this.fileToUpload.name; + this.props.vm.loadProject(this.reader.result) + .then(() => { + analytics.event({ + category: 'project', + action: 'Import Project File', + nonInteraction: true + }); + // Remove the hash if any (without triggering a hash change event or a reload) + history.replaceState({}, document.title, '.'); + 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.onUpdateProjectTitle(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 () { @@ -119,10 +154,12 @@ SBFileUploader.propTypes = { children: PropTypes.func, className: PropTypes.string, intl: intlShape.isRequired, + isLoadingUpload: PropTypes.bool, loadingState: PropTypes.oneOf(LoadingStates), onLoadingFinished: PropTypes.func, onLoadingStarted: PropTypes.func, onUpdateProjectTitle: PropTypes.func, + requestProjectUpload: PropTypes.func, vm: PropTypes.shape({ loadProject: PropTypes.func }) @@ -130,21 +167,23 @@ SBFileUploader.propTypes = { SBFileUploader.defaultProps = { className: '' }; -const mapStateToProps = state => ({ - loadingState: state.scratchGui.projectState.loadingState, - vm: state.scratchGui.vm -}); +const mapStateToProps = state => { + const loadingState = state.scratchGui.projectState.loadingState; + return { + isLoadingUpload: getIsLoadingUpload(loadingState), + loadingState: loadingState, + vm: state.scratchGui.vm + }; +}; const mapDispatchToProps = (dispatch, ownProps) => ({ - onLoadingFinished: loadingState => { - dispatch(onLoadedProject(loadingState, ownProps.canSave)); + onLoadingFinished: (loadingState, success) => { + dispatch(onLoadedProject(loadingState, ownProps.canSave, success)); dispatch(closeLoadingProject()); dispatch(closeFileMenu()); }, - onLoadingStarted: () => { - dispatch(openLoadingProject()); - dispatch(onProjectUploadStarted()); - } + requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)), + onLoadingStarted: () => dispatch(openLoadingProject()) }); // Allow incoming props to override redux-provided props. Used to mock in tests. diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx index fb71d6c0885f23e668b21615abc164f8de0fec70..f344907807e1def1370f78d813d62d84a262ab3e 100644 --- a/src/lib/vm-manager-hoc.jsx +++ b/src/lib/vm-manager-hoc.jsx @@ -129,7 +129,7 @@ const vmManagerHOC = function (WrappedComponent) { const mapDispatchToProps = dispatch => ({ onError: error => dispatch(projectError(error)), onLoadedProject: (loadingState, canSave) => - dispatch(onLoadedProject(loadingState, canSave)), + dispatch(onLoadedProject(loadingState, canSave, true)), onSetProjectUnchanged: () => dispatch(setProjectUnchanged()) }); diff --git a/src/reducers/project-state.js b/src/reducers/project-state.js index a587844243a80dbac98e152b18989c614c6056c8..84c956b72cb39b7556ae6899f128575e67895f8e 100644 --- a/src/reducers/project-state.js +++ b/src/reducers/project-state.js @@ -10,7 +10,9 @@ const DONE_LOADING_VM_WITHOUT_ID = 'scratch-gui/project-state/DONE_LOADING_VM_WI const DONE_REMIXING = 'scratch-gui/project-state/DONE_REMIXING'; const DONE_UPDATING = 'scratch-gui/project-state/DONE_UPDATING'; const DONE_UPDATING_BEFORE_COPY = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_COPY'; +const DONE_UPDATING_BEFORE_FILE_UPLOAD = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_FILE_UPLOAD'; const DONE_UPDATING_BEFORE_NEW = 'scratch-gui/project-state/DONE_UPDATING_BEFORE_NEW'; +const RETURN_TO_SHOWING = 'scratch-gui/project-state/RETURN_TO_SHOWING'; const SET_PROJECT_ID = 'scratch-gui/project-state/SET_PROJECT_ID'; const START_AUTO_UPDATING = 'scratch-gui/project-state/START_AUTO_UPDATING'; 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'; const START_REMIXING = 'scratch-gui/project-state/START_REMIXING'; const START_UPDATING_BEFORE_CREATING_COPY = 'scratch-gui/project-state/START_UPDATING_BEFORE_CREATING_COPY'; const START_UPDATING_BEFORE_CREATING_NEW = 'scratch-gui/project-state/START_UPDATING_BEFORE_CREATING_NEW'; +const START_UPDATING_BEFORE_FILE_UPLOAD = 'scratch-gui/project-state/START_UPDATING_BEFORE_FILE_UPLOAD'; const defaultProjectId = '0'; // hardcoded id of default project @@ -40,6 +43,7 @@ const LoadingState = keyMirror({ SHOWING_WITH_ID: null, SHOWING_WITHOUT_ID: null, UPDATING_BEFORE_COPY: null, + UPDATING_BEFORE_FILE_UPLOAD: null, UPDATING_BEFORE_NEW: null }); @@ -63,6 +67,9 @@ const getIsLoading = loadingState => ( loadingState === LoadingState.LOADING_VM_WITH_ID || loadingState === LoadingState.LOADING_VM_NEW_DEFAULT ); +const getIsLoadingUpload = loadingState => ( + loadingState === LoadingState.LOADING_VM_FILE_UPLOAD +); const getIsCreatingNew = loadingState => ( loadingState === LoadingState.CREATING_NEW ); @@ -84,6 +91,7 @@ const getIsUpdating = loadingState => ( loadingState === LoadingState.AUTO_UPDATING || loadingState === LoadingState.MANUAL_UPDATING || loadingState === LoadingState.UPDATING_BEFORE_COPY || + loadingState === LoadingState.UPDATING_BEFORE_FILE_UPLOAD || loadingState === LoadingState.UPDATING_BEFORE_NEW ); const getIsShowingProject = loadingState => ( @@ -141,7 +149,8 @@ const reducer = function (state, action) { if (state.loadingState === LoadingState.LOADING_VM_FILE_UPLOAD || state.loadingState === LoadingState.LOADING_VM_NEW_DEFAULT) { return Object.assign({}, state, { - loadingState: LoadingState.SHOWING_WITHOUT_ID + loadingState: LoadingState.SHOWING_WITHOUT_ID, + projectId: defaultProjectId }); } return state; @@ -194,6 +203,13 @@ const reducer = function (state, action) { }); } return state; + case DONE_UPDATING_BEFORE_FILE_UPLOAD: + if (state.loadingState === LoadingState.UPDATING_BEFORE_FILE_UPLOAD) { + return Object.assign({}, state, { + loadingState: LoadingState.LOADING_VM_FILE_UPLOAD + }); + } + return state; case DONE_UPDATING_BEFORE_NEW: if (state.loadingState === LoadingState.UPDATING_BEFORE_NEW) { return Object.assign({}, state, { @@ -202,6 +218,16 @@ const reducer = function (state, action) { }); } return state; + case RETURN_TO_SHOWING: + if (state.projectId === null || state.projectId === defaultProjectId) { + return Object.assign({}, state, { + loadingState: LoadingState.SHOWING_WITHOUT_ID, + projectId: defaultProjectId + }); + } + return Object.assign({}, state, { + loadingState: LoadingState.SHOWING_WITH_ID + }); case SET_PROJECT_ID: // if the projectId hasn't actually changed do nothing if (state.projectId === action.projectId) { @@ -275,8 +301,7 @@ const reducer = function (state, action) { LoadingState.SHOWING_WITHOUT_ID ].includes(state.loadingState)) { return Object.assign({}, state, { - loadingState: LoadingState.LOADING_VM_FILE_UPLOAD, - projectId: null // clear any current projectId + loadingState: LoadingState.LOADING_VM_FILE_UPLOAD }); } return state; @@ -308,6 +333,13 @@ const reducer = function (state, action) { }); } return state; + case START_UPDATING_BEFORE_FILE_UPLOAD: + if (state.loadingState === LoadingState.SHOWING_WITH_ID) { + return Object.assign({}, state, { + loadingState: LoadingState.UPDATING_BEFORE_FILE_UPLOAD + }); + } + return state; case START_ERROR: // fatal errors: there's no correct editor state for us to show if ([ @@ -328,6 +360,7 @@ const reducer = function (state, action) { LoadingState.MANUAL_UPDATING, LoadingState.REMIXING, LoadingState.UPDATING_BEFORE_COPY, + LoadingState.UPDATING_BEFORE_FILE_UPLOAD, LoadingState.UPDATING_BEFORE_NEW ].includes(state.loadingState)) { return Object.assign({}, state, { @@ -398,28 +431,33 @@ const onFetchedProjectData = (projectData, loadingState) => { } }; -const onLoadedProject = (loadingState, canSave) => { - switch (loadingState) { - case LoadingState.LOADING_VM_WITH_ID: - return { - type: DONE_LOADING_VM_WITH_ID - }; - case LoadingState.LOADING_VM_FILE_UPLOAD: - if (canSave) { +const onLoadedProject = (loadingState, canSave, success) => { + if (success) { + switch (loadingState) { + case LoadingState.LOADING_VM_WITH_ID: + return { + type: DONE_LOADING_VM_WITH_ID + }; + case LoadingState.LOADING_VM_FILE_UPLOAD: + if (canSave) { + return { + type: DONE_LOADING_VM_TO_SAVE + }; + } return { - type: DONE_LOADING_VM_TO_SAVE + type: DONE_LOADING_VM_WITHOUT_ID }; + case LoadingState.LOADING_VM_NEW_DEFAULT: + return { + type: DONE_LOADING_VM_WITHOUT_ID + }; + default: + break; } - return { - type: DONE_LOADING_VM_WITHOUT_ID - }; - case LoadingState.LOADING_VM_NEW_DEFAULT: - return { - type: DONE_LOADING_VM_WITHOUT_ID - }; - default: - break; } + return { + type: RETURN_TO_SHOWING + }; }; const doneUpdatingProject = loadingState => { @@ -433,6 +471,10 @@ const doneUpdatingProject = loadingState => { return { type: DONE_UPDATING_BEFORE_COPY }; + case LoadingState.UPDATING_BEFORE_FILE_UPLOAD: + return { + type: DONE_UPDATING_BEFORE_FILE_UPLOAD + }; case LoadingState.UPDATING_BEFORE_NEW: return { type: DONE_UPDATING_BEFORE_NEW @@ -457,9 +499,21 @@ const requestNewProject = needSave => { return {type: START_FETCHING_NEW}; }; -const onProjectUploadStarted = () => ({ - type: START_LOADING_VM_FILE_UPLOAD -}); +const requestProjectUpload = loadingState => { + switch (loadingState) { + case LoadingState.SHOWING_WITH_ID: + return { + type: START_UPDATING_BEFORE_FILE_UPLOAD + }; + case LoadingState.NOT_LOADED: + case LoadingState.SHOWING_WITHOUT_ID: + return { + type: START_LOADING_VM_FILE_UPLOAD + }; + default: + break; + } +}; const autoUpdateProject = () => ({ type: START_AUTO_UPDATING @@ -495,6 +549,7 @@ export { getIsFetchingWithoutId, getIsLoading, getIsLoadingWithId, + getIsLoadingUpload, getIsManualUpdating, getIsRemixing, getIsShowingProject, @@ -504,10 +559,10 @@ export { manualUpdateProject, onFetchedProjectData, onLoadedProject, - onProjectUploadStarted, projectError, remixProject, requestNewProject, + requestProjectUpload, saveProjectAsCopy, setProjectId }; diff --git a/test/unit/reducers/project-state-reducer.test.js b/test/unit/reducers/project-state-reducer.test.js index 657bd39a4813d7e69901bf2100eb34f22a97c01b..412287d441d315e6f8ab75acc97cd2c0147785ab 100644 --- a/test/unit/reducers/project-state-reducer.test.js +++ b/test/unit/reducers/project-state-reducer.test.js @@ -8,10 +8,10 @@ import { manualUpdateProject, onFetchedProjectData, onLoadedProject, - onProjectUploadStarted, projectError, remixProject, requestNewProject, + requestProjectUpload, saveProjectAsCopy, setProjectId } from '../../../src/reducers/project-state'; @@ -96,7 +96,7 @@ test('onLoadedProject upload, with canSave false, shows without id', () => { const initialState = { loadingState: LoadingState.LOADING_VM_FILE_UPLOAD }; - const action = onLoadedProject(initialState.loadingState, false); + const action = onLoadedProject(initialState.loadingState, false, true); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); }); @@ -105,7 +105,7 @@ test('onLoadedProject upload, with canSave true, prepares to save', () => { const initialState = { loadingState: LoadingState.LOADING_VM_FILE_UPLOAD }; - const action = onLoadedProject(initialState.loadingState, true); + const action = onLoadedProject(initialState.loadingState, true, true); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING); }); @@ -114,7 +114,7 @@ test('onLoadedProject with id shows with id', () => { const initialState = { loadingState: LoadingState.LOADING_VM_WITH_ID }; - const action = onLoadedProject(initialState.loadingState); + const action = onLoadedProject(initialState.loadingState, true, true); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); }); @@ -123,7 +123,7 @@ test('onLoadedProject new shows without id', () => { const initialState = { loadingState: LoadingState.LOADING_VM_NEW_DEFAULT }; - const action = onLoadedProject(initialState.loadingState); + const action = onLoadedProject(initialState.loadingState, false, true); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); }); @@ -132,11 +132,31 @@ test('onLoadedProject new, to save shows without id', () => { const initialState = { loadingState: LoadingState.LOADING_VM_NEW_DEFAULT }; - const action = onLoadedProject(initialState.loadingState); + const action = onLoadedProject(initialState.loadingState, true, true); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); }); +test('onLoadedProject with success false, no project id, shows without id', () => { + const initialState = { + loadingState: LoadingState.LOADING_VM_WITH_ID, + projectId: null + }; + const action = onLoadedProject(initialState.loadingState, false, false); + const resultState = projectStateReducer(initialState, action); + expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); +}); + +test('onLoadedProject with success false, valid project id, shows with id', () => { + const initialState = { + loadingState: LoadingState.LOADING_VM_WITH_ID, + projectId: '12345' + }; + const action = onLoadedProject(initialState.loadingState, false, false); + const resultState = projectStateReducer(initialState, action); + expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); +}); + test('doneUpdatingProject with id shows with id', () => { const initialState = { loadingState: LoadingState.MANUAL_UPDATING @@ -215,29 +235,29 @@ test('requestNewProject, when can create new, should save and prepare to fetch d expect(resultState.loadingState).toBe(LoadingState.UPDATING_BEFORE_NEW); }); -test('onProjectUploadStarted when project not loaded should load', () => { +test('requestProjectUpload when project not loaded should load', () => { const initialState = { loadingState: LoadingState.NOT_LOADED }; - const action = onProjectUploadStarted(); + const action = requestProjectUpload(initialState.loadingState); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD); }); -test('onProjectUploadStarted when showing project with id should load', () => { +test('requestProjectUpload when showing project with id should load', () => { const initialState = { loadingState: LoadingState.SHOWING_WITH_ID }; - const action = onProjectUploadStarted(); + const action = requestProjectUpload(initialState.loadingState); const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD); + expect(resultState.loadingState).toBe(LoadingState.UPDATING_BEFORE_FILE_UPLOAD); }); -test('onProjectUploadStarted when showing project without id should load', () => { +test('requestProjectUpload when showing project without id should load', () => { const initialState = { loadingState: LoadingState.SHOWING_WITHOUT_ID }; - const action = onProjectUploadStarted(); + const action = requestProjectUpload(initialState.loadingState); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD); });