diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 08f698cbc0ae123848fd04e230a4a766fe2e457c..238a02ee0b8d79dd8319583e244cb9ede2e9c22e 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -81,6 +81,7 @@ const GUIComponent = props => { enableCommunity, importInfoVisible, intl, + isCreating, isPlayerOnly, isRtl, isShared, @@ -152,6 +153,9 @@ const GUIComponent = props => { {loading ? ( <Loader /> ) : null} + {isCreating ? ( + <Loader messageId="gui.loader.creating" /> + ) : null} {importInfoVisible ? ( <ImportModal /> ) : null} @@ -357,6 +361,7 @@ GUIComponent.propTypes = { enableCommunity: PropTypes.bool, importInfoVisible: PropTypes.bool, intl: intlShape.isRequired, + isCreating: PropTypes.bool, isPlayerOnly: PropTypes.bool, isRtl: PropTypes.bool, isShared: PropTypes.bool, @@ -397,7 +402,9 @@ GUIComponent.defaultProps = { canShare: false, canUseCloud: false, enableCommunity: false, + isCreating: false, isShared: false, + loading: false, onUpdateProjectTitle: () => {}, showComingSoon: false, stageSizeMode: STAGE_SIZE_MODES.large diff --git a/src/components/loader/loader.jsx b/src/components/loader/loader.jsx index dea1ff311cec08e869887991f876802c6df939a6..e06b8bb35f6b94a5778d84d84a54b65cb107db20 100644 --- a/src/components/loader/loader.jsx +++ b/src/components/loader/loader.jsx @@ -1,6 +1,7 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import styles from './loader.css'; +import PropTypes from 'prop-types'; import topBlock from './top-block.svg'; import middleBlock from './middle-block.svg'; @@ -97,6 +98,22 @@ const messages = [ weight: 1 } ]; +const mainMessages = { + 'gui.loader.headline': ( + <FormattedMessage + defaultMessage="Loading Project" + description="Main loading message" + id="gui.loader.headline" + /> + ), + 'gui.loader.creating': ( + <FormattedMessage + defaultMessage="Creating Project" + description="Main creating message" + id="gui.loader.creating" + /> + ) +}; class LoaderComponent extends React.Component { constructor (props) { @@ -148,11 +165,7 @@ class LoaderComponent extends React.Component { /> </div> <div className={styles.title}> - <FormattedMessage - defaultMessage="Loading Project" - description="Main loading message" - id="gui.loader.headline" - /> + {mainMessages[this.props.messageId]} </div> <div className={styles.messageContainerOuter}> <div @@ -175,4 +188,11 @@ class LoaderComponent extends React.Component { } } +LoaderComponent.propTypes = { + messageId: PropTypes.string +}; +LoaderComponent.defaultProps = { + messageId: 'gui.loader.headline' +}; + export default LoaderComponent; diff --git a/src/lib/alerts/index.jsx b/src/lib/alerts/index.jsx index 53de8f8489faca3a0a339179713178f0cc1a7718..e5c42cce357dac723705925df6d3d72ba3053cf0 100644 --- a/src/lib/alerts/index.jsx +++ b/src/lib/alerts/index.jsx @@ -20,10 +20,11 @@ const alerts = [ { alertId: 'createSuccess', alertType: AlertTypes.STANDARD, - clearList: ['createSuccess', 'creating', 'saveSuccess', 'saving'], + clearList: ['createSuccess', 'creating', 'createCopySuccess', 'creatingCopy', + 'createRemixSuccess', 'creatingRemix', 'saveSuccess', 'saving'], content: ( <FormattedMessage - defaultMessage="Successfully created." + defaultMessage="New project created." description="Message indicating that project was successfully created" id="gui.alerts.createsuccess" /> @@ -32,13 +33,46 @@ const alerts = [ level: AlertLevels.SUCCESS, maxDisplaySecs: 5 }, + { + alertId: 'createCopySuccess', + alertType: AlertTypes.STANDARD, + clearList: ['createSuccess', 'creating', 'createCopySuccess', 'creatingCopy', + 'createRemixSuccess', 'creatingRemix', 'saveSuccess', 'saving'], + content: ( + <FormattedMessage + defaultMessage="Project saved as a copy." + description="Message indicating that project was successfully created" + id="gui.alerts.createcopysuccess" + /> + ), + iconURL: successImage, + level: AlertLevels.SUCCESS, + maxDisplaySecs: 5 + }, + { + alertId: 'createRemixSuccess', + alertType: AlertTypes.STANDARD, + clearList: ['createSuccess', 'creating', 'createCopySuccess', 'creatingCopy', + 'createRemixSuccess', 'creatingRemix', 'saveSuccess', 'saving'], + content: ( + <FormattedMessage + defaultMessage="Project saved as a remix." + description="Message indicating that project was successfully created" + id="gui.alerts.createremixsuccess" + /> + ), + iconURL: successImage, + level: AlertLevels.SUCCESS, + maxDisplaySecs: 5 + }, { alertId: 'creating', alertType: AlertTypes.STANDARD, - clearList: ['createSuccess', 'creating', 'saveSuccess', 'saving'], + clearList: ['createSuccess', 'creating', 'createCopySuccess', 'creatingCopy', + 'createRemixSuccess', 'creatingRemix', 'saveSuccess', 'saving'], content: ( <FormattedMessage - defaultMessage="Creating..." + defaultMessage="Creating new…" description="Message indicating that project is in process of creating" id="gui.alerts.creating" /> @@ -46,9 +80,40 @@ const alerts = [ iconSpinner: true, level: AlertLevels.SUCCESS }, + { + alertId: 'creatingCopy', + alertType: AlertTypes.STANDARD, + clearList: ['createSuccess', 'creating', 'createCopySuccess', 'creatingCopy', + 'createRemixSuccess', 'creatingRemix', 'saveSuccess', 'saving'], + content: ( + <FormattedMessage + defaultMessage="Copying project…" + description="Message indicating that project is in process of copying" + id="gui.alerts.creatingCopy" + /> + ), + iconSpinner: true, + level: AlertLevels.SUCCESS + }, + { + alertId: 'creatingRemix', + alertType: AlertTypes.STANDARD, + clearList: ['createSuccess', 'creating', 'createCopySuccess', 'creatingCopy', + 'createRemixSuccess', 'creatingRemix', 'saveSuccess', 'saving'], + content: ( + <FormattedMessage + defaultMessage="Remixing project…" + description="Message indicating that project is in process of remixing" + id="gui.alerts.creatingRemix" + /> + ), + iconSpinner: true, + level: AlertLevels.SUCCESS + }, { alertId: 'creatingError', - clearList: ['creating', 'createSuccess'], + clearList: ['createSuccess', 'creating', 'createCopySuccess', 'creatingCopy', + 'createRemixSuccess', 'creatingRemix', 'saveSuccess', 'saving'], closeButton: true, content: ( <FormattedMessage @@ -61,7 +126,8 @@ const alerts = [ }, { alertId: 'savingError', - clearList: ['saving', 'saveSuccess', 'savingError'], + clearList: ['createSuccess', 'creating', 'createCopySuccess', 'creatingCopy', + 'createRemixSuccess', 'creatingRemix', 'saveSuccess', 'saving'], showDownload: true, showSaveNow: true, closeButton: false, @@ -77,10 +143,10 @@ const alerts = [ { alertId: 'saveSuccess', alertType: AlertTypes.INLINE, - clearList: ['createSuccess', 'creating', 'saveSuccess', 'saving', 'savingError'], + clearList: ['saveSuccess', 'saving', 'savingError'], content: ( <FormattedMessage - defaultMessage="Successfully saved." + defaultMessage="Project saved." description="Message indicating that project was successfully saved" id="gui.alerts.savesuccess" /> @@ -92,10 +158,10 @@ const alerts = [ { alertId: 'saving', alertType: AlertTypes.INLINE, - clearList: ['createSuccess', 'creating', 'saveSuccess', 'saving', 'savingError'], + clearList: ['saveSuccess', 'saving', 'savingError'], content: ( <FormattedMessage - defaultMessage="Saving..." + defaultMessage="Saving project…" description="Message indicating that project is in process of saving" id="gui.alerts.saving" /> diff --git a/src/lib/project-saver-hoc.jsx b/src/lib/project-saver-hoc.jsx index f3a1c2c1641071e834093dba610fdb3471ae9667..cff3db1f5d2163de5daf57f5f66bf45e17cbbccd 100644 --- a/src/lib/project-saver-hoc.jsx +++ b/src/lib/project-saver-hoc.jsx @@ -20,6 +20,7 @@ import { createProject, doneCreatingProject, doneUpdatingProject, + getIsAnyCreatingNewState, getIsCreatingCopy, getIsCreatingNew, getIsManualUpdating, @@ -128,11 +129,9 @@ const ProjectSaverHOC = function (WrappedComponent) { }); } createNewProjectToStorage () { - this.props.onShowCreatingAlert(); return this.storeProject(null) .then(response => { this.props.onCreatedProject(response.id.toString(), this.props.loadingState); - this.props.onShowCreateSuccessAlert(); }) .catch(err => { this.props.onShowAlert('creatingError'); @@ -140,7 +139,7 @@ const ProjectSaverHOC = function (WrappedComponent) { }); } createCopyToStorage () { - this.props.onShowCreatingAlert(); + this.props.onShowCreatingCopyAlert(); return this.storeProject(null, { original_id: this.props.reduxProjectId, is_copy: 1, @@ -148,7 +147,7 @@ const ProjectSaverHOC = function (WrappedComponent) { }) .then(response => { this.props.onCreatedProject(response.id.toString(), this.props.loadingState); - this.props.onShowCreateSuccessAlert(); + this.props.onShowCopySuccessAlert(); }) .catch(err => { this.props.onShowAlert('creatingError'); @@ -156,7 +155,7 @@ const ProjectSaverHOC = function (WrappedComponent) { }); } createRemixToStorage () { - this.props.onShowCreatingAlert(); + this.props.onShowCreatingRemixAlert(); return this.storeProject(null, { original_id: this.props.reduxProjectId, is_remix: 1, @@ -164,7 +163,7 @@ const ProjectSaverHOC = function (WrappedComponent) { }) .then(response => { this.props.onCreatedProject(response.id.toString(), this.props.loadingState); - this.props.onShowCreateSuccessAlert(); + this.props.onShowRemixSuccessAlert(); }) .catch(err => { this.props.onShowAlert('creatingError'); @@ -247,6 +246,7 @@ const ProjectSaverHOC = function (WrappedComponent) { isCreatingCopy, isCreatingNew, projectChanged, + isAnyCreatingNewState, isManualUpdating, isRemixing, isShowingSaveable, @@ -260,8 +260,10 @@ const ProjectSaverHOC = function (WrappedComponent) { onProjectError, onRemixing, onShowAlert, - onShowCreateSuccessAlert, - onShowCreatingAlert, + onShowCopySuccessAlert, + onShowRemixSuccessAlert, + onShowCreatingCopyAlert, + onShowCreatingRemixAlert, onShowSaveSuccessAlert, onShowSavingAlert, onUpdatedProject, @@ -274,6 +276,7 @@ const ProjectSaverHOC = function (WrappedComponent) { } = this.props; return ( <WrappedComponent + isCreating={isAnyCreatingNewState} {...componentProps} /> ); @@ -284,6 +287,7 @@ const ProjectSaverHOC = function (WrappedComponent) { autoSaveTimeoutId: PropTypes.number, canCreateNew: PropTypes.bool, canSave: PropTypes.bool, + isAnyCreatingNewState: PropTypes.bool, isCreatingCopy: PropTypes.bool, isCreatingNew: PropTypes.bool, isManualUpdating: PropTypes.bool, @@ -300,8 +304,10 @@ const ProjectSaverHOC = function (WrappedComponent) { onProjectError: PropTypes.func, onRemixing: PropTypes.func, onShowAlert: PropTypes.func, - onShowCreateSuccessAlert: PropTypes.func, - onShowCreatingAlert: PropTypes.func, + onShowCopySuccessAlert: PropTypes.func, + onShowCreatingCopyAlert: PropTypes.func, + onShowCreatingRemixAlert: PropTypes.func, + onShowRemixSuccessAlert: PropTypes.func, onShowSaveSuccessAlert: PropTypes.func, onShowSavingAlert: PropTypes.func, onUpdateProjectThumbnail: PropTypes.func, @@ -320,6 +326,7 @@ const ProjectSaverHOC = function (WrappedComponent) { const isShowingWithId = getIsShowingWithId(loadingState); return { autoSaveTimeoutId: state.scratchGui.timeout.autoSaveTimeoutId, + isAnyCreatingNewState: getIsAnyCreatingNewState(loadingState), isCreatingCopy: getIsCreatingCopy(loadingState), isCreatingNew: getIsCreatingNew(loadingState), isRemixing: getIsRemixing(loadingState), @@ -342,8 +349,10 @@ const ProjectSaverHOC = function (WrappedComponent) { onProjectError: error => dispatch(projectError(error)), onSetProjectUnchanged: () => dispatch(setProjectUnchanged()), onShowAlert: alertType => dispatch(showStandardAlert(alertType)), - onShowCreateSuccessAlert: () => showAlertWithTimeout(dispatch, 'createSuccess'), - onShowCreatingAlert: () => showAlertWithTimeout(dispatch, 'creating'), + onShowCopySuccessAlert: () => showAlertWithTimeout(dispatch, 'createCopySuccess'), + onShowRemixSuccessAlert: () => showAlertWithTimeout(dispatch, 'createRemixSuccess'), + onShowCreatingCopyAlert: () => showAlertWithTimeout(dispatch, 'creatingCopy'), + onShowCreatingRemixAlert: () => showAlertWithTimeout(dispatch, 'creatingRemix'), onShowSaveSuccessAlert: () => showAlertWithTimeout(dispatch, 'saveSuccess'), onShowSavingAlert: () => showAlertWithTimeout(dispatch, 'saving'), onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState)), diff --git a/src/reducers/project-state.js b/src/reducers/project-state.js index 0679a8c37ca09b9080066defb3fbc6de09edffaf..edec52a537c0f9fde8d9cbb07fbeffb281038227 100644 --- a/src/reducers/project-state.js +++ b/src/reducers/project-state.js @@ -61,6 +61,11 @@ const getIsLoadingWithId = loadingState => ( const getIsCreatingNew = loadingState => ( loadingState === LoadingState.CREATING_NEW ); +const getIsAnyCreatingNewState = loadingState => ( + loadingState === LoadingState.FETCHING_NEW_DEFAULT || + loadingState === LoadingState.LOADING_VM_NEW_DEFAULT || + loadingState === LoadingState.CREATING_NEW +); const getIsCreatingCopy = loadingState => ( loadingState === LoadingState.CREATING_COPY ); @@ -477,6 +482,7 @@ export { defaultProjectId, doneCreatingProject, doneUpdatingProject, + getIsAnyCreatingNewState, getIsCreatingCopy, getIsCreatingNew, getIsError, diff --git a/test/unit/reducers/alerts-reducer.test.js b/test/unit/reducers/alerts-reducer.test.js index 0ebe41875a017b7d338499e76202dae2dd86b5a6..1a9db51385ae2e03d3abfa43be973e145ee1af30 100644 --- a/test/unit/reducers/alerts-reducer.test.js +++ b/test/unit/reducers/alerts-reducer.test.js @@ -107,8 +107,9 @@ test('related alerts can clear each other', () => { }; const action = showStandardAlert('saveSuccess'); const resultState = alertsReducer(initialState, action); - expect(resultState.alertsList.length).toBe(1); - expect(resultState.alertsList[0].alertId).toBe('saveSuccess'); + expect(resultState.alertsList.length).toBe(2); + expect(resultState.alertsList[0].alertId).toBe('creating'); + expect(resultState.alertsList[1].alertId).toBe('saveSuccess'); }); test('several related alerts can be cleared at once', () => { diff --git a/test/unit/util/project-saver-hoc.test.jsx b/test/unit/util/project-saver-hoc.test.jsx index a9cecade58e5a11068a42f8fe3068dd2e8274f1f..e9d6e3b950b42faadc69afcd7ac4b23833748fb2 100644 --- a/test/unit/util/project-saver-hoc.test.jsx +++ b/test/unit/util/project-saver-hoc.test.jsx @@ -178,7 +178,7 @@ describe('projectSaverHOC', () => { }); test('if we enter remixing state, vm project should be requested, and alert should show', () => { - const mockedShowCreatingAlert = jest.fn(); + const mockedShowCreatingRemixAlert = jest.fn(); const Component = () => <div />; const WrappedComponent = projectSaverHOC(Component); const mockedStoreProject = jest.fn(() => Promise.resolve()); @@ -197,7 +197,7 @@ describe('projectSaverHOC', () => { reduxProjectId={'100'} store={store} vm={vm} - onShowCreatingAlert={mockedShowCreatingAlert} + onShowCreatingRemixAlert={mockedShowCreatingRemixAlert} /> ); mounted.setProps({ @@ -205,11 +205,11 @@ describe('projectSaverHOC', () => { loadingState: LoadingState.REMIXING }); expect(mockedStoreProject).toHaveBeenCalled(); - expect(mockedShowCreatingAlert).toHaveBeenCalled(); + expect(mockedShowCreatingRemixAlert).toHaveBeenCalled(); }); test('if we enter creating copy state, vm project should be requested, and alert should show', () => { - const mockedShowCreatingAlert = jest.fn(); + const mockedShowCreatingCopyAlert = jest.fn(); const Component = () => <div />; const WrappedComponent = projectSaverHOC(Component); const mockedStoreProject = jest.fn(() => Promise.resolve()); @@ -228,7 +228,7 @@ describe('projectSaverHOC', () => { reduxProjectId={'100'} store={store} vm={vm} - onShowCreatingAlert={mockedShowCreatingAlert} + onShowCreatingCopyAlert={mockedShowCreatingCopyAlert} /> ); mounted.setProps({ @@ -236,7 +236,7 @@ describe('projectSaverHOC', () => { loadingState: LoadingState.CREATING_COPY }); expect(mockedStoreProject).toHaveBeenCalled(); - expect(mockedShowCreatingAlert).toHaveBeenCalled(); + expect(mockedShowCreatingCopyAlert).toHaveBeenCalled(); }); test('if we enter updating/saving state, vm project should be requested', () => {