diff --git a/README.md b/README.md index 4b5b91ea8c4..e114acb5c80 100644 --- a/README.md +++ b/README.md @@ -29,20 +29,26 @@ npm start Then go to [http://localhost:8601/](http://localhost:8601/) - the playground outputs the default GUI component ## Developing alongside other Scratch repositories - If you wish to develop scratch-gui alongside other scratch repositories that depend on it, you may wish to have the other repositories use your local scratch-gui build instead of fetching the current production version of the scratch-gui that is found by default using `npm install`. To do this: - -1. Make sure you have run `npm install` from this repository's top level +1. Make sure you have run `npm install` from this (scratch-gui) repository's top level 2. Make sure you have run `npm install` from the top level of each repository (such as scratch-www) that depends on scratch-gui -3. From this repository's top level, build the `dist` directory by running `BUILD_MODE=dist npm run build` -4. From this repository's top level, establish a link to this repository by running `npm link` +3. From this (scratch-gui) repository's top level, build the `dist` directory by running `BUILD_MODE=dist npm run build` +4. From this (scratch-gui) repository's top level, establish a link to this repository by running `npm link` 5. From the top level of each repository that depends on scratch-gui, run `npm link scratch-gui` 6. Build or run the repositories that depend on scratch-gui +Instead of `BUILD_MODE=dist npm run build` you can also use `BUILD_MODE=dist npm run watch`, however this may be unreliable. + +### Oh no! It didn't work! +* Follow the recipe above step by step and don't change the order. It is especially important to run npm first because installing after the linking will reset the linking. +* Make sure the repositories are siblings on your machine's file tree. +* If you have multiple Terminal tabs or windows open for the different Scratch repositories, make sure to use the same node version in all of them. +* In the worst case unlink the repositories with `npm unlink` and start over. + ## Testing NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64. @@ -69,7 +75,6 @@ npm run integration-test You may want to review the documentation for [Jest](https://facebook.github.io/jest/docs/en/api.html) and [Enzyme](http://airbnb.io/enzyme/docs/api/) as you write your tests. ## Publishing to GitHub Pages - You can publish the GUI to github.io so that others on the Internet can view it. [Read the wiki for a step-by-step guide.](https://github.com/LLK/scratch-gui/wiki/Publishing-to-GitHub-Pages) diff --git a/package.json b/package.json index c43fb30c032..82d9dc575e0 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "copy-webpack-plugin": "^4.5.1", "core-js": "2.5.7", "css-loader": "^1.0.0", - "enzyme": "^3.1.0", - "enzyme-adapter-react-16": "1.1.1", + "enzyme": "^3.5.0", + "enzyme-adapter-react-16": "1.3.0", "es6-object-assign": "1.1.0", "eslint": "^5.0.1", "eslint-config-scratch": "^5.0.0", @@ -96,16 +96,16 @@ "redux-throttle": "0.1.1", "rimraf": "^2.6.1", "scratch-audio": "0.1.0-prerelease.20180625202813", - "scratch-blocks": "0.1.0-prerelease.1535116879", - "scratch-l10n": "3.0.20180824134256", - "scratch-paint": "0.2.0-prerelease.20180823231354", + "scratch-blocks": "0.1.0-prerelease.1535662135", + "scratch-l10n": "3.0.20180830210150", + "scratch-paint": "0.2.0-prerelease.20180831175055", "scratch-render": "0.1.0-prerelease.20180824141819", - "scratch-storage": "1.0.0", + "scratch-storage": "1.0.2", "scratch-svg-renderer": "0.2.0-prerelease.20180817005452", - "scratch-vm": "0.2.0-prerelease.20180824135031", + "scratch-vm": "0.2.0-prerelease.20180830155110", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", - "style-loader": "^0.22.1", + "style-loader": "^0.23.0", "svg-to-image": "1.1.3", "text-encoding": "0.6.4", "to-style": "1.3.3", @@ -113,7 +113,7 @@ "wav-encoder": "1.3.0", "web-audio-test-api": "^0.5.2", "webpack": "^4.6.0", - "webpack-cli": "^2.0.15", + "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.3", "xhr": "2.5.0" }, diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 14b082cdfe7..4b05b2d2747 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -12,7 +12,10 @@ module.exports = { 'import/no-commonjs': 'error', 'import/no-amd': 'error', 'import/no-nodejs-modules': 'error', - 'react/jsx-no-literals': 'error' + 'react/jsx-no-literals': 'error', + 'no-confusing-arrow': ['error', { + 'allowParens': true + }] }, settings: { react: { diff --git a/src/components/asset-panel/icon--sound-rtl.svg b/src/components/asset-panel/icon--sound-rtl.svg new file mode 100644 index 00000000000..adf85017394 --- /dev/null +++ b/src/components/asset-panel/icon--sound-rtl.svg @@ -0,0 +1,20 @@ + + + + Artboard + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css index aec4ce630d5..16f337ef460 100644 --- a/src/components/connection-modal/connection-modal.css +++ b/src/components/connection-modal/connection-modal.css @@ -121,9 +121,16 @@ .radar-small { width: 40px; height: 40px; +} + +[dir="ltr"] .radar-small { margin-right: 0.5rem; } +[dir="rtl"] .radar-small { + margin-left: 0.5rem; +} + .radar-big { width: 120px; height: 120px; @@ -184,6 +191,7 @@ position: absolute; top: -5px; right: -15px; + left: -15px; padding: 5px 5px; background-color: $pen-primary; border-radius: 100%; @@ -232,7 +240,6 @@ flex-direction: row; justify-content: flex-start; align-items: center; - margin-left: 3rem; } [dir="ltr"] .scratch-link-help-step { diff --git a/src/components/crash-message/crash-message.jsx b/src/components/crash-message/crash-message.jsx index 8ea44473986..764c8e2167d 100644 --- a/src/components/crash-message/crash-message.jsx +++ b/src/components/crash-message/crash-message.jsx @@ -1,12 +1,7 @@ -/* eslint-disable react/jsx-no-literals */ -/* - @todo Rule is disabled because this component is rendered outside the - intl provider right now so cannot be translated. -*/ - import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; +import {FormattedMessage} from 'react-intl'; import styles from './crash-message.css'; import reloadIcon from './reload.svg'; @@ -19,19 +14,30 @@ const CrashMessage = props => ( src={reloadIcon} />

- Oops! Something went wrong. +

- We are so sorry, but it looks like Scratch has crashed. This bug has been - automatically reported to the Scratch Team. Please refresh your page to try - again. - +

diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 757653e9ac3..086011f4ba0 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -132,11 +132,15 @@ margin-left: 0.125rem; } -/* only mirror blocks tab icon */ +/* mirror blocks and sound tab icons */ [dir="rtl"] .tab:nth-of-type(1) img { transform: scaleX(-1); } +[dir="rtl"] .tab:nth-of-type(3) img { + transform: scaleX(-1); +} + .tab.is-selected img { filter: none; } diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index de2854c5006..662c0e1ae6f 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -189,3 +189,16 @@ .disabled { opacity: 0.5; } + +.save-in-progress { + animation: hue-rotate 3s linear infinite; +} + +@keyframes hue-rotate { + from { + filter: hue-rotate(); + } + to { + filter: hue-rotate(360deg); + } +} diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 5eb4ad3312d..b1ccd29412e 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -138,8 +138,10 @@ class MenuBar extends React.Component { bindAll(this, [ 'handleLanguageMouseUp', 'handleRestoreOption', + 'handleCloseFileMenuAndThen', 'restoreOptionMessage' ]); + this.state = {projectSaveInProgress: false}; } handleLanguageMouseUp (e) { if (!this.props.languageMenuOpen) { @@ -152,6 +154,24 @@ class MenuBar extends React.Component { this.props.onRequestCloseEdit(); }; } + handleUpdateProject (updateFun) { + return () => { + this.props.onRequestCloseFile(); + this.setState({projectSaveInProgress: true}, + () => { + updateFun().then(() => { + this.setState({projectSaveInProgress: false}); + }); + } + ); + }; + } + handleCloseFileMenuAndThen (fn) { + return () => { + this.props.onRequestCloseFile(); + fn(); + }; + } restoreOptionMessage (deletedItem) { switch (deletedItem) { case 'Sprite': @@ -182,17 +202,34 @@ class MenuBar extends React.Component { } } render () { + const saveNowMessage = ( + + ); return ( - +
- Scratch + + Scratch +
- - - - - + {(saveProject, updateProject) => ( + this.props.canUpdateProject ? ( + + {saveNowMessage} + + ) : ( + + {saveNowMessage} + + ) + )} )} - {(saveProject, saveProps) => ( + {saveProject => ( ({ + canUpdateProject: typeof (state.session && state.session.session && state.session.session.user) !== 'undefined', fileMenuOpen: fileMenuOpen(state), editMenuOpen: editMenuOpen(state), isRtl: state.locales.isRtl, diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css index 1bb4f196343..1ed2425ce1e 100644 --- a/src/components/modal/modal.css +++ b/src/components/modal/modal.css @@ -83,10 +83,14 @@ $sides: 20rem; user-select: none; } -.header-image { +[dir="ltr"] .header-image { margin-right: 0.5rem; } +[dir="rtl"] .header-image { + margin-left: 0.5rem; +} + .header-item-filter { display: flex; flex-basis: $sides; diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index c6e620001af..4a0e03655da 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -149,6 +149,15 @@ $border-radius: 0.25rem; margin-bottom: -0.375rem; } +/* mirror the louder/softer speaker icons when rtl */ +[dir="rtl"] .effect-button:nth-of-type(6) img { + transform: scaleX(-1); +} + +[dir="rtl"] .effect-button:nth-of-type(7) img { + transform: scaleX(-1); +} + [dir="ltr"] .button-group { margin-left: 1rem; } diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index e90fe1dff10..3f9b2dd5966 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -134,7 +134,7 @@ Backpack.propTypes = { const getTokenAndUsername = state => { // Look for the session state provided by scratch-www - if (state.session && state.session.session) { + if (state.session && state.session.session && state.session.session.user) { return { token: state.session.session.user.token, username: state.session.session.user.username diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 33d8fe58b24..6e5d96070b7 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -72,12 +72,10 @@ class GUI extends React.Component { `Failed to load project from server [id=${window.location.hash}]: ${this.state.errorMessage}`); } const { - assetHost, // eslint-disable-line no-unused-vars children, fetchingProject, loadingStateVisible, projectData, // eslint-disable-line no-unused-vars - projectHost, // eslint-disable-line no-unused-vars vm, ...componentProps } = this.props; @@ -94,7 +92,6 @@ class GUI extends React.Component { } GUI.propTypes = { - assetHost: PropTypes.string, children: PropTypes.node, fetchingProject: PropTypes.bool, importInfoVisible: PropTypes.bool, @@ -102,7 +99,6 @@ GUI.propTypes = { onSeeCommunity: PropTypes.func, previewInfoVisible: PropTypes.bool, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - projectHost: PropTypes.string, vm: PropTypes.instanceOf(VM) }; diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx index dc14578bd61..50ff56c3ed9 100644 --- a/src/containers/paint-editor-wrapper.jsx +++ b/src/containers/paint-editor-wrapper.jsx @@ -76,7 +76,8 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => { imageId: targetId && `${targetId}${costume.skinId}`, image: state.scratchGui.vm.getCostume(index), rtl: state.locales.isRtl, - vm: state.scratchGui.vm + vm: state.scratchGui.vm, + zoomLevelId: targetId }; }; diff --git a/src/containers/project-saver.jsx b/src/containers/project-saver.jsx index c594421eca3..13be33da808 100644 --- a/src/containers/project-saver.jsx +++ b/src/containers/project-saver.jsx @@ -2,6 +2,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; +import storage from '../lib/storage'; /** * Project saver component passes a saveProject function to its child. @@ -21,14 +22,17 @@ class ProjectSaver extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'saveProject' + 'createProject', + 'updateProject', + 'saveProject', + 'doStoreProject' ]); } saveProject () { const saveLink = document.createElement('a'); document.body.appendChild(saveLink); - this.props.vm.saveProjectSb3().then(content => { + this.props.saveProjectSb3().then(content => { // TODO user-friendly project name // File name: project-DATE-TIME const date = new Date(); @@ -49,27 +53,48 @@ class ProjectSaver extends React.Component { document.body.removeChild(saveLink); }); } + doStoreProject (id) { + return this.props.saveProjectSb3() + .then(content => { + const assetType = storage.AssetType.Project; + const dataFormat = storage.DataFormat.SB3; + const body = new FormData(); + body.append('sb3_file', content, 'sb3_file'); + return storage.store( + assetType, + dataFormat, + body, + id + ); + }); + } + createProject () { + return this.doStoreProject(); + } + updateProject () { + return this.doStoreProject(this.props.projectId); + } render () { const { - /* eslint-disable no-unused-vars */ - children, - vm, - /* eslint-enable no-unused-vars */ - ...props + children } = this.props; - return this.props.children(this.saveProject, props); + return children( + this.saveProject, + this.updateProject, + this.createProject + ); } } ProjectSaver.propTypes = { children: PropTypes.func, - vm: PropTypes.shape({ - saveProjectSb3: PropTypes.func - }) + projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + saveProjectSb3: PropTypes.func }; const mapStateToProps = state => ({ - vm: state.scratchGui.vm + saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm), + projectId: state.scratchGui.projectId }); export default connect( diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx index 5d7cb67f6fd..154ddaa814a 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -9,10 +9,13 @@ import analytics from '../lib/analytics'; import LibraryComponent from '../components/library/library.jsx'; import soundIcon from '../components/asset-panel/icon--sound.svg'; +import soundIconRtl from '../components/asset-panel/icon--sound-rtl.svg'; import soundLibraryContent from '../lib/libraries/sounds.json'; import soundTags from '../lib/libraries/sound-tags'; +import {connect} from 'react-redux'; + const messages = defineMessages({ libraryTitle: { defaultMessage: 'Choose a Sound', @@ -137,7 +140,7 @@ class SoundLibrary extends React.PureComponent { } = sound; return { _md5: md5, - rawURL: soundIcon, + rawURL: this.props.isRtl ? soundIconRtl : soundIcon, ...otherData }; }); @@ -159,9 +162,19 @@ class SoundLibrary extends React.PureComponent { SoundLibrary.propTypes = { intl: intlShape.isRequired, + isRtl: PropTypes.bool, onNewSound: PropTypes.func.isRequired, onRequestClose: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired }; -export default injectIntl(SoundLibrary); +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +const mapDispatchToProps = () => ({}); + +export default injectIntl(connect( + mapStateToProps, + mapDispatchToProps +)(SoundLibrary)); diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index a30b8f97e82..9631b30d358 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -6,6 +6,7 @@ import VM from 'scratch-vm'; import AssetPanel from '../components/asset-panel/asset-panel.jsx'; import soundIcon from '../components/asset-panel/icon--sound.svg'; +import soundIconRtl from '../components/asset-panel/icon--sound-rtl.svg'; import addSoundFromLibraryIcon from '../components/asset-panel/icon--add-sound-lib.svg'; import addSoundFromRecordingIcon from '../components/asset-panel/icon--add-sound-record.svg'; import fileUploadIcon from '../components/action-menu/icon--file-upload.svg'; @@ -158,6 +159,7 @@ class SoundTab extends React.Component { const { dispatchUpdateRestore, // eslint-disable-line no-unused-vars intl, + isRtl, vm, onNewSoundFromLibraryClick, onNewSoundFromRecordingClick @@ -171,7 +173,7 @@ class SoundTab extends React.Component { const sounds = sprite.sounds ? sprite.sounds.map(sound => ( { - url: soundIcon, + url: isRtl ? soundIconRtl : soundIcon, name: sound.name, details: (sound.sampleCount / sound.rate).toFixed(2), dragPayload: sound @@ -259,6 +261,7 @@ SoundTab.propTypes = { dispatchUpdateRestore: PropTypes.func, editingTarget: PropTypes.string, intl: intlShape, + isRtl: PropTypes.bool, onActivateCostumesTab: PropTypes.func.isRequired, onNewSoundFromLibraryClick: PropTypes.func.isRequired, onNewSoundFromRecordingClick: PropTypes.func.isRequired, @@ -282,6 +285,7 @@ SoundTab.propTypes = { const mapStateToProps = state => ({ editingTarget: state.scratchGui.targets.editingTarget, + isRtl: state.locales.isRtl, sprites: state.scratchGui.targets.sprites, stage: state.scratchGui.targets.stage, soundLibraryVisible: state.scratchGui.modals.soundLibrary, diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx index 22a1aa5d1ce..eafd49cb326 100644 --- a/src/lib/project-loader-hoc.jsx +++ b/src/lib/project-loader-hoc.jsx @@ -1,5 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; + +import {setProjectId} from '../reducers/project-id'; import analytics from './analytics'; import log from './log'; @@ -21,11 +24,13 @@ const ProjectLoaderHOC = function (WrappedComponent) { }; storage.setProjectHost(props.projectHost); storage.setAssetHost(props.assetHost); - - } - componentDidMount () { - if (this.props.projectId || this.props.projectId === 0) { - this.updateProject(this.props.projectId); + props.setProjectId(props.projectId); + if ( + props.projectId !== '' && + props.projectId !== null && + typeof props.projectId !== 'undefined' + ) { + this.updateProject(props.projectId); } } componentWillUpdate (nextProps) { @@ -36,6 +41,7 @@ const ProjectLoaderHOC = function (WrappedComponent) { storage.setAssetHost(nextProps.assetHost); } if (this.props.projectId !== nextProps.projectId) { + this.props.setProjectId(nextProps.projectId); this.setState({fetchingProject: true}, () => { this.updateProject(nextProps.projectId); }); @@ -62,7 +68,13 @@ const ProjectLoaderHOC = function (WrappedComponent) { } render () { const { - projectId, // eslint-disable-line no-unused-vars + /* eslint-disable no-unused-vars */ + assetHost, + projectHost, + projectId, + reduxProjectId, + setProjectId: setProjectIdProp, + /* eslint-enable no-unused-vars */ ...componentProps } = this.props; if (!this.state.projectData) return null; @@ -78,7 +90,8 @@ const ProjectLoaderHOC = function (WrappedComponent) { ProjectLoaderComponent.propTypes = { assetHost: PropTypes.string, projectHost: PropTypes.string, - projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + setProjectId: PropTypes.func }; ProjectLoaderComponent.defaultProps = { assetHost: 'https://assets.scratch.mit.edu', @@ -86,7 +99,13 @@ const ProjectLoaderHOC = function (WrappedComponent) { projectId: 0 }; - return ProjectLoaderComponent; + const mapStateToProps = () => ({}); + + const mapDispatchToProps = dispatch => ({ + setProjectId: id => dispatch(setProjectId(id)) + }); + + return connect(mapStateToProps, mapDispatchToProps)(ProjectLoaderComponent); }; export { diff --git a/src/lib/storage.js b/src/lib/storage.js index 9c49d81703f..38ee7ede405 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -15,15 +15,17 @@ class Storage extends ScratchStorage { asset.data, asset.id )); - this.addWebSource( + this.addWebStore( [this.AssetType.Project], - this.getProjectURL.bind(this) + this.getProjectGetConfig.bind(this), + this.getProjectCreateConfig.bind(this), + this.getProjectUpdateConfig.bind(this) ); - this.addWebSource( + this.addWebStore( [this.AssetType.ImageVector, this.AssetType.ImageBitmap, this.AssetType.Sound], - this.getAssetURL.bind(this) + this.getAssetGetConfig.bind(this) ); - this.addWebSource( + this.addWebStore( [this.AssetType.Sound], asset => `static/extension-assets/scratch3_music/${asset.assetId}.${asset.dataFormat}` ); @@ -31,13 +33,25 @@ class Storage extends ScratchStorage { setProjectHost (projectHost) { this.projectHost = projectHost; } - getProjectURL (projectAsset) { + getProjectGetConfig (projectAsset) { return `${this.projectHost}/internalapi/project/${projectAsset.assetId}/get/`; } + getProjectCreateConfig () { + return { + url: `${this.projectHost}/`, + withCredentials: true + }; + } + getProjectUpdateConfig (projectAsset) { + return { + url: `${this.projectHost}/${projectAsset.assetId}`, + withCredentials: true + }; + } setAssetHost (assetHost) { this.assetHost = assetHost; } - getAssetURL (asset) { + getAssetGetConfig (asset) { return `${this.assetHost}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`; } } diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 04b26446861..9340af56e11 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -11,6 +11,7 @@ import modalReducer, {modalsInitialState} from './modals'; import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout'; +import projectIdReducer, {projectIdInitialState} from './project-id'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; @@ -35,6 +36,7 @@ const guiInitialState = { modals: modalsInitialState, monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, + projectId: projectIdInitialState, restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, toolbox: toolboxInitialState, @@ -77,6 +79,7 @@ const guiReducer = combineReducers({ modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer, + projectId: projectIdReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, toolbox: toolboxReducer, diff --git a/src/reducers/project-id.js b/src/reducers/project-id.js new file mode 100644 index 00000000000..9045873c74e --- /dev/null +++ b/src/reducers/project-id.js @@ -0,0 +1,27 @@ +const SET_PROJECT_ID = 'scratch-gui/project-id/SET_PROJECT_ID'; + +const initialState = null; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + + switch (action.type) { + case SET_PROJECT_ID: + return action.id; + default: + return state; + } +}; + +const setProjectId = function (id) { + return { + type: SET_PROJECT_ID, + id: id + }; +}; + +export { + reducer as default, + initialState as projectIdInitialState, + setProjectId +}; diff --git a/test/unit/util/project-loader-hoc.test.jsx b/test/unit/util/project-loader-hoc.test.jsx index 502ac0f62d4..6c30453e8a2 100644 --- a/test/unit/util/project-loader-hoc.test.jsx +++ b/test/unit/util/project-loader-hoc.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import configureStore from 'redux-mock-store'; import ProjectLoaderHOC from '../../../src/lib/project-loader-hoc.jsx'; import storage from '../../../src/lib/storage'; import {mount} from 'enzyme'; @@ -6,18 +7,11 @@ import {mount} from 'enzyme'; jest.mock('react-ga'); describe('ProjectLoaderHOC', () => { + const mockStore = configureStore(); + let store; - test('when there is no id, it loads (default) project id 0', () => { - const Component = ({projectData}) =>
{projectData}
; - const WrappedComponent = ProjectLoaderHOC(Component); - const originalLoad = storage.load; - storage.load = jest.fn((type, id) => Promise.resolve(id)); - const mounted = mount(); - expect(mounted.props().projectId).toEqual(0); - expect(storage.load).toHaveBeenCalledWith( - storage.AssetType.Project, 0, storage.DataFormat.JSON - ); - storage.load = originalLoad; + beforeEach(() => { + store = mockStore({scratchGui: {}}); }); test('when there is an id, it tries to load that project', () => { @@ -25,7 +19,12 @@ describe('ProjectLoaderHOC', () => { const WrappedComponent = ProjectLoaderHOC(Component); const originalLoad = storage.load; storage.load = jest.fn((type, id) => Promise.resolve({data: id})); - const mounted = mount(); + const mounted = mount( + + ); expect(mounted.props().projectId).toEqual('100'); expect(storage.load).toHaveBeenLastCalledWith( storage.AssetType.Project, '100', storage.DataFormat.JSON @@ -38,7 +37,7 @@ describe('ProjectLoaderHOC', () => { const WrappedComponent = ProjectLoaderHOC(Component); const originalLoad = storage.load; storage.load = jest.fn(() => Promise.resolve(null)); - const mounted = mount(); + const mounted = mount(); storage.load = originalLoad; const mountedDiv = mounted.find('div'); expect(mountedDiv.exists()).toEqual(false);