diff --git a/README.md b/README.md index e114acb5c80cb62555d3371c56b59562ab1231ee..91613cdcdc36a486a997cb8bf97889457f64d73e 100644 --- a/README.md +++ b/README.md @@ -47,32 +47,69 @@ Instead of `BUILD_MODE=dist npm run build` you can also use `BUILD_MODE=dist npm * 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. +* In the worst case unlink the repositories by running `npm unlink` in both, and start over. ## Testing -NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64. -Run linter, unit tests, build, and integration tests. +### Documentation + +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. + +See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. + +### Running tests + +*NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64.* + +Before running any test, make sure you have run `npm install` from this (scratch-gui) repository's top level. + +#### Main testing command + +To run linter, unit tests, build, and integration tests, all at once: ```bash npm test ``` -Run unit tests in isolation. +#### Running unit tests + +To run unit tests in isolation: +```bash +npm run test:unit +``` + +To run unit tests in watch mode (watches for code changes and continuously runs tests): ```bash -npm run unit-test +npm run test:unit -- --watch ``` -Run unit tests in watch mode (watches for code changes and continuously runs tests). See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. +#### Running integration tests + +Integration tests use a headless browser to manipulate the actual html and javascript that the repo +produces. You will not see this activity (though you can hear it when sounds are played!). + +Note that integration tests require you to first create a build that can be loaded in a browser: + ```bash -npm run unit-test -- --watch +npm run build ``` -Run integration tests in isolation. +Then, you can run all integration tests: + ```bash -npm run integration-test +npm run test:integration ``` -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. +Or, you can run a single file of integration tests (in this example, the `backpack` tests): + +```bash +$(npm bin)/jest --runInBand test/integration/backpack.test.js +``` + +If you want to watch the browser as it runs the test, rather than running headless, use: + +```bash +USE_HEADLESS=no $(npm bin)/jest --runInBand test/integration/backpack.test.js +``` ## Publishing to GitHub Pages You can publish the GUI to github.io so that others on the Internet can view it. diff --git a/package.json b/package.json index 52a455237e3cfebc1b5fcc02e55da8af29f5b401..7764fd51becddbe2098d8fc53adb2fbbef90a222 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "mkdirp": "^0.5.1", "postcss-import": "^12.0.0", "postcss-loader": "^3.0.0", - "postcss-simple-vars": "^4.0.0", + "postcss-simple-vars": "^5.0.1", "prop-types": "^15.5.10", "raf": "^3.4.0", "raw-loader": "^0.5.1", @@ -86,7 +86,7 @@ "react-popover": "0.5.7", "react-redux": "5.0.7", "react-responsive": "5.0.0", - "react-style-proptype": "3.2.1", + "react-style-proptype": "3.2.2", "react-tabs": "2.3.0", "react-test-renderer": "16.2.0", "react-tooltip": "3.8.0", @@ -98,8 +98,8 @@ "scratch-audio": "0.1.0-prerelease.20180625202813", "scratch-blocks": "0.1.0-prerelease.1535662135", "scratch-l10n": "3.0.20180830210150", - "scratch-paint": "0.2.0-prerelease.20180907144642", - "scratch-render": "0.1.0-prerelease.20180824141819", + "scratch-paint": "0.2.0-prerelease.20180912222409", + "scratch-render": "0.1.0-prerelease.20180907144714", "scratch-storage": "1.0.2", "scratch-svg-renderer": "0.2.0-prerelease.20180907141232", "scratch-vm": "0.2.0-prerelease.20180912222010", diff --git a/src/components/connection-modal/auto-scanning-step.jsx b/src/components/connection-modal/auto-scanning-step.jsx index 623bbdd02aa904ee4d0e60606a80ccd0aa1d2225..a73c0b58db974d065bc2d3372cee0c43595ad8ec 100644 --- a/src/components/connection-modal/auto-scanning-step.jsx +++ b/src/components/connection-modal/auto-scanning-step.jsx @@ -45,8 +45,8 @@ const AutoScanningStep = props => ( src={radarIcon} /> <img - className={styles.deviceButtonImage} - src={props.deviceButtonImage} + className={styles.peripheralButtonImage} + src={props.peripheralButtonImage} /> </React.Fragment> )} @@ -55,7 +55,7 @@ const AutoScanningStep = props => ( <FormattedMessage defaultMessage="No devices found" description="Text shown when no devices could be found" - id="gui.connection.auto-scanning.noDevicesFound" + id="gui.connection.auto-scanning.noPeripheralsFound" /> </Box> )} @@ -139,9 +139,9 @@ const AutoScanningStep = props => ( ); AutoScanningStep.propTypes = { - deviceButtonImage: PropTypes.string, onRefresh: PropTypes.func, onStartScan: PropTypes.func, + peripheralButtonImage: PropTypes.string, phase: PropTypes.oneOf(Object.keys(PHASES)) }; diff --git a/src/components/connection-modal/connected-step.jsx b/src/components/connection-modal/connected-step.jsx index c02f644bc4e55d807124bd8cc41004a98cdca24b..aa71c63c8a11730b8a30379de6112a2be98150d4 100644 --- a/src/components/connection-modal/connected-step.jsx +++ b/src/components/connection-modal/connected-step.jsx @@ -12,10 +12,10 @@ const ConnectedStep = props => ( <Box className={styles.body}> <Box className={styles.activityArea}> <Box className={styles.centeredRow}> - <div className={styles.deviceActivity}> + <div className={styles.peripheralActivity}> <img - className={styles.deviceActivityIcon} - src={props.deviceImage} + className={styles.peripheralActivityIcon} + src={props.peripheralImage} /> <img className={styles.bluetoothConnectedIcon} @@ -63,9 +63,9 @@ const ConnectedStep = props => ( ); ConnectedStep.propTypes = { - deviceImage: PropTypes.string.isRequired, onCancel: PropTypes.func, - onDisconnect: PropTypes.func + onDisconnect: PropTypes.func, + peripheralImage: PropTypes.string.isRequired }; export default ConnectedStep; diff --git a/src/components/connection-modal/connecting-step.jsx b/src/components/connection-modal/connecting-step.jsx index be378b3fdd54773f4a1bf9989364a930f9faf433..e4753628677d3f7532b72f9c498823ba38215ce2 100644 --- a/src/components/connection-modal/connecting-step.jsx +++ b/src/components/connection-modal/connecting-step.jsx @@ -14,10 +14,10 @@ const ConnectingStep = props => ( <Box className={styles.body}> <Box className={styles.activityArea}> <Box className={styles.centeredRow}> - <div className={styles.deviceActivity}> + <div className={styles.peripheralActivity}> <img - className={styles.deviceActivityIcon} - src={props.deviceImage} + className={styles.peripheralActivityIcon} + src={props.peripheralImage} /> <img className={styles.bluetoothConnectingIcon} @@ -61,8 +61,8 @@ const ConnectingStep = props => ( ConnectingStep.propTypes = { connectingMessage: PropTypes.node.isRequired, - deviceImage: PropTypes.string.isRequired, - onDisconnect: PropTypes.func + onDisconnect: PropTypes.func, + peripheralImage: PropTypes.string.isRequired }; export default ConnectingStep; diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css index 16f337ef460e7a88120f9b5fe49958bdec07ba91..f024489c591456d75b65e6170f1ace7c71748351 100644 --- a/src/components/connection-modal/connection-modal.css +++ b/src/components/connection-modal/connection-modal.css @@ -25,14 +25,14 @@ align-items: center; } -.device-tile-pane { +.peripheral-tile-pane { overflow-y: auto; width: 100%; height: 100%; padding: 0.5rem; } -.device-tile { +.peripheral-tile { display: flex; flex-direction: row; justify-content: space-between; @@ -46,36 +46,36 @@ margin-bottom: 0.5rem; } -.device-tile-name { +.peripheral-tile-name { display: flex; align-items: center; } -[dir="ltr"] .device-tile-image { +[dir="ltr"] .peripheral-tile-image { margin-right: 0.5rem; } -[dir="rtl"] .device-tile-image { +[dir="rtl"] .peripheral-tile-image { margin-left: 0.5rem; } -.device-tile-name-wrapper { +.peripheral-tile-name-wrapper { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; } -.device-tile-name-label { +.peripheral-tile-name-label { font-weight: bold; font-size: 0.625rem; } -.device-tile-name-text { +.peripheral-tile-name-text { font-size: 0.875rem; } -.device-tile button { +.peripheral-tile button { padding: 0.6rem 0.75rem; border: none; border-radius: 0.25rem; @@ -154,16 +154,16 @@ } } -.device-activity { +.peripheral-activity { position: relative; } -.device-activity-icon { +.peripheral-activity-icon { /* width: 80px; height: 80px; */ } -.device-button-image { +.peripheral-button-image { position: absolute; } @@ -214,7 +214,7 @@ box-shadow: 0px 0px 0px 2px $motion-transparent; } -.device-tile-widgets { +.peripheral-tile-widgets { display: flex; align-items: center; } diff --git a/src/components/connection-modal/connection-modal.jsx b/src/components/connection-modal/connection-modal.jsx index 6fb9cd4ff98c924e05d0ea5f35ebbf3aa7f1f348..99c9a6a531f9f73344cf5395c49d4f5bb7edaf35 100644 --- a/src/components/connection-modal/connection-modal.jsx +++ b/src/components/connection-modal/connection-modal.jsx @@ -27,7 +27,7 @@ const ConnectionModalComponent = props => ( className={styles.modalContent} contentLabel={props.name} headerClassName={styles.header} - headerImage={props.smallDeviceImage} + headerImage={props.smallPeripheralImage} onHelp={props.onHelp} onRequestClose={props.onCancel} > @@ -44,12 +44,12 @@ const ConnectionModalComponent = props => ( ConnectionModalComponent.propTypes = { connectingMessage: PropTypes.node, - deviceButtonImage: PropTypes.string, name: PropTypes.node, onCancel: PropTypes.func.isRequired, onHelp: PropTypes.func.isRequired, + peripheralButtonImage: PropTypes.string, phase: PropTypes.oneOf(Object.keys(PHASES)).isRequired, - smallDeviceImage: PropTypes.string, + smallPeripheralImage: PropTypes.string, title: PropTypes.string.isRequired, useAutoScan: PropTypes.bool.isRequired }; diff --git a/src/components/connection-modal/error-step.jsx b/src/components/connection-modal/error-step.jsx index a5a5ce1b70085d66bfd351599673da58da171821..21dcd8d54c92c4138862a053fe19e849835e744c 100644 --- a/src/components/connection-modal/error-step.jsx +++ b/src/components/connection-modal/error-step.jsx @@ -14,10 +14,10 @@ const ErrorStep = props => ( <Box className={styles.body}> <Box className={styles.activityArea}> <Box className={styles.centeredRow}> - <div className={styles.deviceActivity}> + <div className={styles.peripheralActivity}> <img - className={styles.deviceActivityIcon} - src={props.deviceImage} + className={styles.peripheralActivityIcon} + src={props.peripheralImage} /> </div> </Box> @@ -69,9 +69,9 @@ const ErrorStep = props => ( ); ErrorStep.propTypes = { - deviceImage: PropTypes.string.isRequired, onHelp: PropTypes.func, - onScanning: PropTypes.func + onScanning: PropTypes.func, + peripheralImage: PropTypes.string.isRequired }; export default ErrorStep; diff --git a/src/components/connection-modal/device-tile.jsx b/src/components/connection-modal/peripheral-tile.jsx similarity index 77% rename from src/components/connection-modal/device-tile.jsx rename to src/components/connection-modal/peripheral-tile.jsx index c806453aa0ff60828462376bf9c01a3a38ae550f..6b7e0388d6216eb4dab3094bef76c6ab0b15e613 100644 --- a/src/components/connection-modal/device-tile.jsx +++ b/src/components/connection-modal/peripheral-tile.jsx @@ -7,7 +7,7 @@ import Box from '../box/box.jsx'; import styles from './connection-modal.css'; -class DeviceTile extends React.Component { +class PeripheralTile extends React.Component { constructor (props) { super(props); bindAll(this, [ @@ -19,26 +19,26 @@ class DeviceTile extends React.Component { } render () { return ( - <Box className={styles.deviceTile}> - <Box className={styles.deviceTileName}> + <Box className={styles.peripheralTile}> + <Box className={styles.peripheralTileName}> <img - className={styles.deviceTileImage} - src={this.props.smallDeviceImage} + className={styles.peripheralTileImage} + src={this.props.smallPeripheralImage} /> - <Box className={styles.deviceTileNameWrapper}> - <Box className={styles.deviceTileNameLabel}> + <Box className={styles.peripheralTileNameWrapper}> + <Box className={styles.peripheralTileNameLabel}> <FormattedMessage defaultMessage="Device name" description="Label for field showing the device name" - id="gui.connection.device-name-label" + id="gui.connection.peripheral-name-label" /> </Box> - <Box className={styles.deviceTileNameText}> + <Box className={styles.peripheralTileNameText}> {this.props.name} </Box> </Box> </Box> - <Box className={styles.deviceTileWidgets}> + <Box className={styles.peripheralTileWidgets}> <Box className={styles.signalStrengthMeter}> <div className={classNames(styles.signalBar, { @@ -76,12 +76,12 @@ class DeviceTile extends React.Component { } } -DeviceTile.propTypes = { +PeripheralTile.propTypes = { name: PropTypes.string, onConnecting: PropTypes.func, peripheralId: PropTypes.string, rssi: PropTypes.number, - smallDeviceImage: PropTypes.string + smallPeripheralImage: PropTypes.string }; -export default DeviceTile; +export default PeripheralTile; diff --git a/src/components/connection-modal/scanning-step.jsx b/src/components/connection-modal/scanning-step.jsx index cadee33c3e1a6a5669c7a3232f7f3e11ef7e04b5..bb6276ac09bf575012e3e90039b852d5f6a76a97 100644 --- a/src/components/connection-modal/scanning-step.jsx +++ b/src/components/connection-modal/scanning-step.jsx @@ -4,7 +4,7 @@ import React from 'react'; import classNames from 'classnames'; import Box from '../box/box.jsx'; -import DeviceTile from './device-tile.jsx'; +import PeripheralTile from './peripheral-tile.jsx'; import Dots from './dots.jsx'; import radarIcon from './icons/searching.png'; @@ -16,7 +16,7 @@ const ScanningStep = props => ( <Box className={styles.body}> <Box className={styles.activityArea}> {props.scanning ? ( - props.deviceList.length === 0 ? ( + props.peripheralList.length === 0 ? ( <div className={styles.activityAreaInfo}> <div className={styles.centeredRow}> <img @@ -26,19 +26,19 @@ const ScanningStep = props => ( <FormattedMessage defaultMessage="Looking for devices" description="Text shown while scanning for devices" - id="gui.connection.scanning.lookingfordevices" + id="gui.connection.scanning.lookingforperipherals" /> </div> </div> ) : ( - <div className={styles.deviceTilePane}> - {props.deviceList.map(device => - (<DeviceTile - key={device.peripheralId} - name={device.name} - peripheralId={device.peripheralId} - rssi={device.rssi} - smallDeviceImage={props.smallDeviceImage} + <div className={styles.peripheralTilePane}> + {props.peripheralList.map(peripheral => + (<PeripheralTile + key={peripheral.peripheralId} + name={peripheral.name} + peripheralId={peripheral.peripheralId} + rssi={peripheral.rssi} + smallPeripheralImage={props.smallPeripheralImage} onConnecting={props.onConnecting} />) )} @@ -49,7 +49,7 @@ const ScanningStep = props => ( <FormattedMessage defaultMessage="No devices found" description="Text shown when no devices could be found" - id="gui.connection.scanning.noDevicesFound" + id="gui.connection.scanning.noPeripheralsFound" /> </Box> )} @@ -85,19 +85,19 @@ const ScanningStep = props => ( ); ScanningStep.propTypes = { - deviceList: PropTypes.arrayOf(PropTypes.shape({ + onConnecting: PropTypes.func, + onRefresh: PropTypes.func, + peripheralList: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string, rssi: PropTypes.number, peripheralId: PropTypes.string })), - onConnecting: PropTypes.func, - onRefresh: PropTypes.func, scanning: PropTypes.bool.isRequired, - smallDeviceImage: PropTypes.string + smallPeripheralImage: PropTypes.string }; ScanningStep.defaultProps = { - deviceList: [], + peripheralList: [], scanning: true }; diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 71c623135093df1f503ff64c93a57183eb33e761..aa1dfc0393388111d80c4179e21b7a0d62fa95f1 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -74,6 +74,7 @@ const GUIComponent = props => { onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, onSeeCommunity, + onUpdateProjectTitle, previewInfoVisible, targetIsStage, soundsTabVisible, @@ -147,6 +148,7 @@ const GUIComponent = props => { <MenuBar enableCommunity={enableCommunity} onSeeCommunity={onSeeCommunity} + onUpdateProjectTitle={onUpdateProjectTitle} /> <Box className={styles.bodyWrapper}> <Box className={styles.flexWrapper}> @@ -294,6 +296,7 @@ GUIComponent.propTypes = { onRequestCloseCostumeLibrary: PropTypes.func, onSeeCommunity: PropTypes.func, onTabSelect: PropTypes.func, + onUpdateProjectTitle: PropTypes.func, previewInfoVisible: PropTypes.bool, soundsTabVisible: PropTypes.bool, stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)), diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index 662c0e1ae6fe2e403367d2d68194b81b83a0f409..46fd7313c16c6a12c55799979d43a876f6e33eff 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -37,6 +37,7 @@ justify-content: flex-start; flex-wrap: nowrap; align-items: center; + flex-grow: 1; } .scratch-logo { @@ -84,6 +85,16 @@ background-color: $ui-black-transparent; } +.menu-bar-item.growable { + max-width: 12rem; + flex: 1; +} + +.title-field-growable { + flex-grow: 1; + width: 2rem; +} + .file-group { display: flex; flex-direction: row; @@ -109,22 +120,6 @@ height: 34px; } -.title-field { - border: 1px dashed $ui-black-transparent; - border-radius: .25rem; - width: 12rem; - height: 34px; - background-color: transparent; - padding: .5rem; -} - -.title-field, -.title-field::placeholder { - color: $ui-white; - font-weight: bold; - font-size: .8rem; -} - .share-button { background: $data-primary; height: 32px; diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index b1ccd29412e736cd5c39b6297e17ddaf3e190668..897fedc3f3166da91678c56be805566d0200b35d 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -13,6 +13,7 @@ import LanguageSelector from '../../containers/language-selector.jsx'; import ProjectLoader from '../../containers/project-loader.jsx'; import Menu from '../../containers/menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; +import ProjectTitleInput from './project-title-input.jsx'; import ProjectSaver from '../../containers/project-saver.jsx'; import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; @@ -392,12 +393,14 @@ class MenuBar extends React.Component { <FormattedMessage {...ariaMessages.tutorials} /> </div> <Divider className={classNames(styles.divider)} /> - <div className={classNames(styles.menuBarItem)}> - <MenuBarItemTooltip id="title-field"> - <input - disabled - className={classNames(styles.titleField)} - placeholder="Untitled-1" + <div className={classNames(styles.menuBarItem, styles.growable)}> + <MenuBarItemTooltip + enable + id="title-field" + > + <ProjectTitleInput + className={classNames(styles.titleFieldGrowable)} + onUpdateProjectTitle={this.props.onUpdateProjectTitle} /> </MenuBarItemTooltip> </div> @@ -521,7 +524,8 @@ MenuBar.propTypes = { onRequestCloseEdit: PropTypes.func, onRequestCloseFile: PropTypes.func, onRequestCloseLanguage: PropTypes.func, - onSeeCommunity: PropTypes.func + onSeeCommunity: PropTypes.func, + onUpdateProjectTitle: PropTypes.func }; const mapStateToProps = state => ({ diff --git a/src/components/menu-bar/project-title-input.css b/src/components/menu-bar/project-title-input.css new file mode 100644 index 0000000000000000000000000000000000000000..0567ca76614924b6a09d57b1b0a2169ceac8d655 --- /dev/null +++ b/src/components/menu-bar/project-title-input.css @@ -0,0 +1,46 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/z-index.css"; + +/* +If project-title-input.jsx is part of a menu bar say menu-bar.jsx, it can have additional css classes that +can set a width for example or what it should do in a flex box (eg. grow). +*/ + +.title-field { + border: 1px dashed $ui-black-transparent; + border-radius: $form-radius; + -webkit-border-radius: $form-radius; + -moz-border-radius: $form-radius; + background-color: $ui-white-transparent; + background-clip: padding-box; + -webkit-background-clip: padding-box; + height: auto; + padding: .5rem; +} + +.title-field { + color: $ui-white; + font-weight: bold; + font-size: .8rem; +} + +.title-field::placeholder { + color: $ui-white; + font-weight: normal; + font-size: .8rem; + font-style: italic; +} + +.title-field:hover { + background-color: hsla(0, 100%, 100%, 0.5); +} + +.title-field:focus { + outline:none; + border: 1px solid $ui-transparent; + -webkit-box-shadow: 0 0 0 calc($space * .5) $ui-white-transparent; + box-shadow: 0 0 0 calc($space * .5) $ui-white-transparent; + background-color: $ui-white; + color: $text-primary; +} diff --git a/src/components/menu-bar/project-title-input.jsx b/src/components/menu-bar/project-title-input.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b9b5aa6d8382e55c8483a4dde3290ae1ed6ad44a --- /dev/null +++ b/src/components/menu-bar/project-title-input.jsx @@ -0,0 +1,67 @@ +import classNames from 'classnames'; +import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; +import React from 'react'; +import {defineMessages, intlShape, injectIntl} from 'react-intl'; + +import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; +import Input from '../forms/input.jsx'; +const BufferedInput = BufferedInputHOC(Input); + +import styles from './project-title-input.css'; + +const messages = defineMessages({ + projectTitlePlaceholder: { + id: 'gui.gui.projectTitlePlaceholder', + description: 'Placeholder for project title when blank', + defaultMessage: 'Project title here' + } +}); + +class ProjectTitleInput extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleUpdateProjectTitle' + ]); + } + // call onUpdateProjectTitle if it is defined (only defined when gui + // is used within scratch-www) + handleUpdateProjectTitle (newTitle) { + if (this.props.onUpdateProjectTitle) { + this.props.onUpdateProjectTitle(newTitle); + } + } + render () { + return ( + <BufferedInput + className={classNames(styles.titleField, this.props.className)} + maxLength="100" + placeholder={this.props.intl.formatMessage(messages.projectTitlePlaceholder)} + tabIndex="0" + type="text" + value={this.props.projectTitle} + onSubmit={this.handleUpdateProjectTitle} + /> + ); + } +} + +ProjectTitleInput.propTypes = { + className: PropTypes.string, + intl: intlShape.isRequired, + onUpdateProjectTitle: PropTypes.func, + projectTitle: PropTypes.string +}; + +const mapStateToProps = state => ({ + projectTitle: state.scratchGui.projectTitle +}); + +const mapDispatchToProps = () => ({}); + +export default injectIntl(connect( + mapStateToProps, + mapDispatchToProps +)(ProjectTitleInput)); diff --git a/src/components/prompt/prompt.jsx b/src/components/prompt/prompt.jsx index 42de1599939801b8896374f032525be7c77982a9..78192f3adaac829916243929db569003bab283c0 100644 --- a/src/components/prompt/prompt.jsx +++ b/src/components/prompt/prompt.jsx @@ -48,6 +48,7 @@ const PromptComponent = props => ( <input autoFocus className={styles.variableNameTextInput} + name={props.label} placeholder={props.placeholder} onChange={props.onChange} onKeyPress={props.onKeyPress} diff --git a/src/components/question/icon--enter.svg b/src/components/question/icon--enter.svg new file mode 100644 index 0000000000000000000000000000000000000000..03b7c08627d980f0e45ad989d39eae3e602f9e19 Binary files /dev/null and b/src/components/question/icon--enter.svg differ diff --git a/src/components/question/question.css b/src/components/question/question.css index ad5e748e67f711e63a36e038ccd882ff61bf80e1..fb8d7869bfbbfe90a7a021e93c01bf1fd0a40d39 100644 --- a/src/components/question/question.css +++ b/src/components/question/question.css @@ -55,3 +55,10 @@ .question-input > input:focus { box-shadow: 0px 0px 0px 3px $motion-transparent; } + +.question-submit-button-icon { + width: calc(2rem - $space); + height: calc(2rem - $space); + position: relative; + left: -7px; +} diff --git a/src/components/question/question.jsx b/src/components/question/question.jsx index 2e8b9c9a53159006eb6e92548b60fbb4910c760f..c57fb085d37de1f42a75f3a709c05d0f313f76ac 100644 --- a/src/components/question/question.jsx +++ b/src/components/question/question.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import styles from './question.css'; import Input from '../forms/input.jsx'; +import enterIcon from './icon--enter.svg'; const QuestionComponent = props => { const { @@ -28,7 +29,11 @@ const QuestionComponent = props => { className={styles.questionSubmitButton} onClick={onClick} > - {'✔︎' /* @todo should this be an image? */} + <img + className={styles.questionSubmitButtonIcon} + draggable={false} + src={enterIcon} + /> </button> </div> </div> diff --git a/src/containers/auto-scanning-step.jsx b/src/containers/auto-scanning-step.jsx index f8c0360a331dd2e141bef116dc3376c67406796c..f674738419d52289403add07bb1b1c52a21a4900 100644 --- a/src/containers/auto-scanning-step.jsx +++ b/src/containers/auto-scanning-step.jsx @@ -18,7 +18,7 @@ class AutoScanningStep extends React.Component { }; } componentWillUnmount () { - // @todo: stop the device scan here + // @todo: stop the peripheral scan here this.unbindPeripheralUpdates(); } handlePeripheralScanTimeout () { @@ -49,7 +49,7 @@ class AutoScanningStep extends React.Component { 'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout); } handleRefresh () { - // @todo: stop the device scan here, it is more important for auto scan + // @todo: stop the peripheral scan here, it is more important for auto scan // due to timeout and cancellation this.setState({ phase: PHASES.prescan @@ -58,7 +58,7 @@ class AutoScanningStep extends React.Component { } handleStartScan () { this.bindPeripheralUpdates(); - this.props.vm.startDeviceScan(this.props.extensionId); + this.props.vm.scanForPeripheral(this.props.extensionId); this.setState({ phase: PHASES.pressbutton }); @@ -67,7 +67,7 @@ class AutoScanningStep extends React.Component { render () { return ( <ScanningStepComponent - deviceButtonImage={this.props.deviceButtonImage} + peripheralButtonImage={this.props.peripheralButtonImage} phase={this.state.phase} title={this.props.extensionId} onRefresh={this.handleRefresh} @@ -78,9 +78,9 @@ class AutoScanningStep extends React.Component { } AutoScanningStep.propTypes = { - deviceButtonImage: PropTypes.string, extensionId: PropTypes.string.isRequired, onConnecting: PropTypes.func.isRequired, + peripheralButtonImage: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 8a6bec5fd7578beff79912dea9b6dd5cc52595ed..499d77636116b287d0acb3de60c939a0d31f0bd3 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -336,7 +336,7 @@ class Blocks extends React.Component { } handleCategorySelected (categoryId) { const extension = extensionData.find(ext => ext.extensionId === categoryId); - if (extension && extension.launchDeviceConnectionFlow) { + if (extension && extension.launchPeripheralConnectionFlow) { this.handleConnectionModalStart(categoryId); } @@ -365,9 +365,9 @@ class Blocks extends React.Component { this.setState({connectionModal: { extensionId: extensionId, useAutoScan: extension.useAutoScan, - deviceImage: extension.deviceImage, - smallDeviceImage: extension.smallDeviceImage, - deviceButtonImage: extension.deviceButtonImage, + peripheralImage: extension.peripheralImage, + smallPeripheralImage: extension.smallPeripheralImage, + peripheralButtonImage: extension.peripheralButtonImage, name: extension.name, connectingMessage: extension.connectingMessage, helpLink: extension.helpLink diff --git a/src/containers/connection-modal.jsx b/src/containers/connection-modal.jsx index 276d4f72a886e6df3b5f150d1cdd443e5eee9956..164f2455bb6a22a065ce5e5ba3946170271b43e0 100644 --- a/src/containers/connection-modal.jsx +++ b/src/containers/connection-modal.jsx @@ -35,7 +35,7 @@ class ConnectionModal extends React.Component { }); } handleConnecting (peripheralId) { - this.props.vm.connectToPeripheral(this.props.extensionId, peripheralId); + this.props.vm.connectPeripheral(this.props.extensionId, peripheralId); this.setState({ phase: PHASES.connecting }); @@ -47,13 +47,13 @@ class ConnectionModal extends React.Component { } handleDisconnect () { this.props.onStatusButtonUpdate(this.props.extensionId, 'not ready'); - this.props.vm.disconnectExtensionSession(this.props.extensionId); + this.props.vm.disconnectPeripheral(this.props.extensionId); this.props.onCancel(); } handleCancel () { - // If we're not connected to a device, close the websocket so we stop scanning. + // If we're not connected to a peripheral, close the websocket so we stop scanning. if (!this.props.vm.getPeripheralIsConnected(this.props.extensionId)) { - this.props.vm.disconnectExtensionSession(this.props.extensionId); + this.props.vm.disconnectPeripheral(this.props.extensionId); } this.props.onCancel(); } @@ -99,12 +99,12 @@ class ConnectionModal extends React.Component { return ( <ConnectionModalComponent connectingMessage={this.props.connectingMessage} - deviceButtonImage={this.props.deviceButtonImage} - deviceImage={this.props.deviceImage} extensionId={this.props.extensionId} name={this.props.name} + peripheralButtonImage={this.props.peripheralButtonImage} + peripheralImage={this.props.peripheralImage} phase={this.state.phase} - smallDeviceImage={this.props.smallDeviceImage} + smallPeripheralImage={this.props.smallPeripheralImage} title={this.props.extensionId} useAutoScan={this.props.useAutoScan} vm={this.props.vm} @@ -121,14 +121,14 @@ class ConnectionModal extends React.Component { ConnectionModal.propTypes = { connectingMessage: PropTypes.node.isRequired, - deviceButtonImage: PropTypes.string, - deviceImage: PropTypes.string.isRequired, extensionId: PropTypes.string.isRequired, helpLink: PropTypes.string.isRequired, name: PropTypes.node.isRequired, onCancel: PropTypes.func.isRequired, onStatusButtonUpdate: PropTypes.func.isRequired, - smallDeviceImage: PropTypes.string.isRequired, + peripheralButtonImage: PropTypes.string, + peripheralImage: PropTypes.string.isRequired, + smallPeripheralImage: PropTypes.string.isRequired, useAutoScan: PropTypes.bool.isRequired, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index f08e6bab746dd171f040e48e916663bbf438538b..04c6142dce25e073c4f0a0676cd2ab3e5c37590d 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -7,6 +7,7 @@ import ReactModal from 'react-modal'; import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; import {openExtensionLibrary} from '../reducers/modals'; +import {setProjectTitle} from '../reducers/project-title'; import { activateTab, BLOCKS_TAB_INDEX, @@ -34,6 +35,10 @@ class GUI extends React.Component { }; } componentDidMount () { + if (this.props.projectTitle) { + this.props.onUpdateReduxProjectTitle(this.props.projectTitle); + } + if (this.props.vm.initialized) return; this.audioEngine = new AudioEngine(); this.props.vm.attachAudioEngine(this.audioEngine); @@ -65,6 +70,9 @@ class GUI extends React.Component { }); }); } + if (this.props.projectTitle !== nextProps.projectTitle) { + this.props.onUpdateReduxProjectTitle(nextProps.projectTitle); + } } render () { if (this.state.loadingError) { @@ -75,8 +83,10 @@ class GUI extends React.Component { /* eslint-disable no-unused-vars */ assetHost, hideIntro, + onUpdateReduxProjectTitle, projectData, projectHost, + projectTitle, /* eslint-enable no-unused-vars */ children, fetchingProject, @@ -97,14 +107,19 @@ class GUI extends React.Component { } GUI.propTypes = { + assetHost: PropTypes.string, children: PropTypes.node, fetchingProject: PropTypes.bool, hideIntro: PropTypes.bool, importInfoVisible: PropTypes.bool, loadingStateVisible: PropTypes.bool, onSeeCommunity: PropTypes.func, + onUpdateProjectTitle: PropTypes.func, + onUpdateReduxProjectTitle: PropTypes.func, previewInfoVisible: PropTypes.bool, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), + projectHost: PropTypes.string, + projectTitle: PropTypes.string, vm: PropTypes.instanceOf(VM) }; @@ -134,7 +149,8 @@ const mapDispatchToProps = dispatch => ({ onActivateCostumesTab: () => dispatch(activateTab(COSTUMES_TAB_INDEX)), onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)), onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()), - onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()) + onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()), + onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title)) }); const ConnectedGUI = connect( diff --git a/src/containers/project-loader.jsx b/src/containers/project-loader.jsx index 507b716523796e14fb9b73fbb93cca44df2785f8..66081b7a906871cef4a5230d9264cc434c2d3dcd 100644 --- a/src/containers/project-loader.jsx +++ b/src/containers/project-loader.jsx @@ -6,6 +6,7 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl'; import analytics from '../lib/analytics'; import log from '../lib/log'; +import {setProjectTitle} from '../reducers/project-title'; import { openLoadingProject, @@ -75,6 +76,12 @@ class ProjectLoader extends React.Component { if (thisFileInput.files) { // Don't attempt to load if no file was selected this.props.openLoadingState(); reader.readAsArrayBuffer(thisFileInput.files[0]); + if (thisFileInput.files[0].name) { + const matches = thisFileInput.files[0].name.match(/^(.*)\.sb3$/); + if (matches) { + this.props.onSetProjectTitle(matches[1].substring(0, 100)); + } + } } } handleClick () { @@ -112,6 +119,7 @@ ProjectLoader.propTypes = { children: PropTypes.func, closeLoadingState: PropTypes.func, intl: intlShape.isRequired, + onSetProjectTitle: PropTypes.func, openLoadingState: PropTypes.func, vm: PropTypes.shape({ loadProject: PropTypes.func @@ -124,6 +132,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ closeLoadingState: () => dispatch(closeLoadingProject()), + onSetProjectTitle: title => dispatch(setProjectTitle(title)), openLoadingState: () => dispatch(openLoadingProject()) }); diff --git a/src/containers/project-saver.jsx b/src/containers/project-saver.jsx index 13be33da808b1fa2cf9e10c6d2db2644112ef2e0..c32d4b1bf0a045b6c20f9d9593cf5548aebf922b 100644 --- a/src/containers/project-saver.jsx +++ b/src/containers/project-saver.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import storage from '../lib/storage'; +import {projectTitleInitialState} from '../reducers/project-title'; + /** * Project saver component passes a saveProject function to its child. @@ -33,21 +35,15 @@ class ProjectSaver extends React.Component { document.body.appendChild(saveLink); this.props.saveProjectSb3().then(content => { - // TODO user-friendly project name - // File name: project-DATE-TIME - const date = new Date(); - const timestamp = `${date.toLocaleDateString()}-${date.toLocaleTimeString()}`; - const filename = `untitled-project-${timestamp}.sb3`; - // Use special ms version if available to get it working on Edge. if (navigator.msSaveOrOpenBlob) { - navigator.msSaveOrOpenBlob(content, filename); + navigator.msSaveOrOpenBlob(content, this.props.projectFilename); return; } const url = window.URL.createObjectURL(content); saveLink.href = url; - saveLink.download = filename; + saveLink.download = this.props.projectFilename; saveLink.click(); window.URL.revokeObjectURL(url); document.body.removeChild(saveLink); @@ -86,14 +82,24 @@ class ProjectSaver extends React.Component { } } +const getProjectFilename = (curTitle, defaultTitle) => { + let filenameTitle = curTitle; + if (!filenameTitle || filenameTitle.length === 0) { + filenameTitle = defaultTitle; + } + return `${filenameTitle.substring(0, 100)}.sb3`; +}; + ProjectSaver.propTypes = { children: PropTypes.func, + projectFilename: PropTypes.string, projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), saveProjectSb3: PropTypes.func }; const mapStateToProps = state => ({ saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm), + projectFilename: getProjectFilename(state.scratchGui.projectTitle, projectTitleInitialState), projectId: state.scratchGui.projectId }); diff --git a/src/containers/scanning-step.jsx b/src/containers/scanning-step.jsx index 07595d5266d937c577f2e44634995b7849e3e5fb..6e1daa8e6eb32ebdfbef49db1d4ca4b8b87ed03a 100644 --- a/src/containers/scanning-step.jsx +++ b/src/containers/scanning-step.jsx @@ -14,18 +14,18 @@ class ScanningStep extends React.Component { ]); this.state = { scanning: true, - deviceList: [] + peripheralList: [] }; } componentDidMount () { - this.props.vm.startDeviceScan(this.props.extensionId); + this.props.vm.scanForPeripheral(this.props.extensionId); this.props.vm.on( 'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate); this.props.vm.on( 'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout); } componentWillUnmount () { - // @todo: stop the device scan here + // @todo: stop the peripheral scan here this.props.vm.removeListener( 'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate); this.props.vm.removeListener( @@ -34,7 +34,7 @@ class ScanningStep extends React.Component { handlePeripheralScanTimeout () { this.setState({ scanning: false, - deviceList: [] + peripheralList: [] }); } handlePeripheralListUpdate (newList) { @@ -42,22 +42,22 @@ class ScanningStep extends React.Component { const peripheralArray = Object.keys(newList).map(id => newList[id] ); - this.setState({deviceList: peripheralArray}); + this.setState({peripheralList: peripheralArray}); } handleRefresh () { - this.props.vm.startDeviceScan(this.props.extensionId); + this.props.vm.scanForPeripheral(this.props.extensionId); this.setState({ scanning: true, - deviceList: [] + peripheralList: [] }); } render () { return ( <ScanningStepComponent - deviceList={this.state.deviceList} + peripheralList={this.state.peripheralList} phase={this.state.phase} scanning={this.state.scanning} - smallDeviceImage={this.props.smallDeviceImage} + smallPeripheralImage={this.props.smallPeripheralImage} title={this.props.extensionId} onConnected={this.props.onConnected} onConnecting={this.props.onConnecting} @@ -71,7 +71,7 @@ ScanningStep.propTypes = { extensionId: PropTypes.string.isRequired, onConnected: PropTypes.func.isRequired, onConnecting: PropTypes.func.isRequired, - smallDeviceImage: PropTypes.string, + smallPeripheralImage: PropTypes.string, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/css/colors.css b/src/css/colors.css index 37ffa62227a6f6ccb42a6170d4652db037f4f769..2222608922a9df36770fb8330fb663b2ea1e43c7 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -6,6 +6,7 @@ $ui-modal-overlay: hsla(215, 100%, 65%, 0.9); /* 90% transparent version of moti $ui-white: hsla(0, 100%, 100%, 1); /* #FFFFFF */ $ui-white-transparent: hsla(0, 100%, 100%, 0.25); /* 25% transparent version of ui-white */ +$ui-transparent: hsla(0, 100%, 100%, 0); /* 25% transparent version of ui-white */ $ui-black-transparent: hsla(0, 0%, 0%, 0.15); /* 15% transparent version of black */ diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx index edd4fa512b6e2e414762aa6fccb9fdab5cf7dfdf..4b5a4db07c6c2779693bdd71549ed8cd60db3583 100644 --- a/src/lib/libraries/extensions/index.jsx +++ b/src/lib/libraries/extensions/index.jsx @@ -9,13 +9,13 @@ import microbitImage from './microbit.png'; import ev3Image from './ev3.png'; import wedoImage from './wedo.png'; -import microbitDeviceImage from './device-connection/microbit/microbit-illustration.svg'; -import microbitMenuImage from './device-connection/microbit/microbit-small.svg'; -import ev3DeviceImage from './device-connection/ev3/ev3-hub-illustration.svg'; -import ev3MenuImage from './device-connection/ev3/ev3-small.svg'; -import wedoDeviceImage from './device-connection/wedo/wedo-illustration.svg'; -import wedoMenuImage from './device-connection/wedo/wedo-small.svg'; -import wedoButtonImage from './device-connection/wedo/wedo-button-illustration.svg'; +import microbitPeripheralImage from './peripheral-connection/microbit/microbit-illustration.svg'; +import microbitMenuImage from './peripheral-connection/microbit/microbit-small.svg'; +import ev3PeripheralImage from './peripheral-connection/ev3/ev3-hub-illustration.svg'; +import ev3MenuImage from './peripheral-connection/ev3/ev3-small.svg'; +import wedoPeripheralImage from './peripheral-connection/wedo/wedo-illustration.svg'; +import wedoMenuImage from './peripheral-connection/wedo/wedo-small.svg'; +import wedoButtonImage from './peripheral-connection/wedo/wedo-button-illustration.svg'; export default [ { @@ -78,9 +78,9 @@ export default [ { name: ( <FormattedMessage - defaultMessage="Google Translate" - description="Name for the 'Google Translate' extension. Do not translate 'Google'." - id="gui.extension.googletranslate.name" + defaultMessage="Translate" + description="Name for the Translate extension" + id="gui.extension.translate.name" /> ), extensionId: 'translate', @@ -88,8 +88,8 @@ export default [ description: ( <FormattedMessage defaultMessage="Translate text into many languages." - description="Description for the 'Google Translate' extension" - id="gui.extension.googletranslate.description" + description="Description for the Translate extension" + id="gui.extension.translate.description" /> ), featured: true @@ -107,10 +107,10 @@ export default [ ), featured: true, disabled: false, - launchDeviceConnectionFlow: true, + launchPeripheralConnectionFlow: true, useAutoScan: false, - deviceImage: microbitDeviceImage, - smallDeviceImage: microbitMenuImage, + peripheralImage: microbitPeripheralImage, + smallPeripheralImage: microbitMenuImage, connectingMessage: ( <FormattedMessage defaultMessage="Connecting" @@ -133,10 +133,10 @@ export default [ ), featured: true, disabled: false, - launchDeviceConnectionFlow: true, + launchPeripheralConnectionFlow: true, useAutoScan: false, - deviceImage: ev3DeviceImage, - smallDeviceImage: ev3MenuImage, + peripheralImage: ev3PeripheralImage, + smallPeripheralImage: ev3MenuImage, connectingMessage: ( <FormattedMessage defaultMessage="Connecting. Make sure the pin on your EV3 is set to 1234." @@ -159,11 +159,11 @@ export default [ ), featured: true, disabled: false, - launchDeviceConnectionFlow: true, + launchPeripheralConnectionFlow: true, useAutoScan: true, - deviceImage: wedoDeviceImage, - smallDeviceImage: wedoMenuImage, - deviceButtonImage: wedoButtonImage, + peripheralImage: wedoPeripheralImage, + smallPeripheralImage: wedoMenuImage, + peripheralButtonImage: wedoButtonImage, connectingMessage: ( <FormattedMessage defaultMessage="Connecting" diff --git a/src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg b/src/lib/libraries/extensions/peripheral-connection/ev3/ev3-hub-illustration.svg similarity index 100% rename from src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg rename to src/lib/libraries/extensions/peripheral-connection/ev3/ev3-hub-illustration.svg diff --git a/src/lib/libraries/extensions/device-connection/ev3/ev3-small.svg b/src/lib/libraries/extensions/peripheral-connection/ev3/ev3-small.svg similarity index 100% rename from src/lib/libraries/extensions/device-connection/ev3/ev3-small.svg rename to src/lib/libraries/extensions/peripheral-connection/ev3/ev3-small.svg diff --git a/src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg b/src/lib/libraries/extensions/peripheral-connection/microbit/microbit-illustration.svg similarity index 100% rename from src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg rename to src/lib/libraries/extensions/peripheral-connection/microbit/microbit-illustration.svg diff --git a/src/lib/libraries/extensions/device-connection/microbit/microbit-small.svg b/src/lib/libraries/extensions/peripheral-connection/microbit/microbit-small.svg similarity index 100% rename from src/lib/libraries/extensions/device-connection/microbit/microbit-small.svg rename to src/lib/libraries/extensions/peripheral-connection/microbit/microbit-small.svg diff --git a/src/lib/libraries/extensions/device-connection/wedo/wedo-button-illustration.svg b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-button-illustration.svg similarity index 100% rename from src/lib/libraries/extensions/device-connection/wedo/wedo-button-illustration.svg rename to src/lib/libraries/extensions/peripheral-connection/wedo/wedo-button-illustration.svg diff --git a/src/lib/libraries/extensions/device-connection/wedo/wedo-illustration.svg b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-illustration.svg similarity index 100% rename from src/lib/libraries/extensions/device-connection/wedo/wedo-illustration.svg rename to src/lib/libraries/extensions/peripheral-connection/wedo/wedo-illustration.svg diff --git a/src/lib/libraries/extensions/device-connection/wedo/wedo-small.svg b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-small.svg similarity index 100% rename from src/lib/libraries/extensions/device-connection/wedo/wedo-small.svg rename to src/lib/libraries/extensions/peripheral-connection/wedo/wedo-small.svg diff --git a/src/lib/libraries/extensions/translate.png b/src/lib/libraries/extensions/translate.png index 95961ae52fee6e27c5f7946c41a7e4fd1d22fe7a..8f63ade37587318aa72e0ccf8eb822ee273dd130 100644 Binary files a/src/lib/libraries/extensions/translate.png and b/src/lib/libraries/extensions/translate.png differ diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx index eafd49cb32606437837a1e3837ae1812d9507f51..571f220fdea8786a24f8d557ae6c72fde38b37de 100644 --- a/src/lib/project-loader-hoc.jsx +++ b/src/lib/project-loader-hoc.jsx @@ -72,7 +72,6 @@ const ProjectLoaderHOC = function (WrappedComponent) { assetHost, projectHost, projectId, - reduxProjectId, setProjectId: setProjectIdProp, /* eslint-enable no-unused-vars */ ...componentProps diff --git a/src/lib/titled-hoc.jsx b/src/lib/titled-hoc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2534d96bd61ba9f4cb4db89a51e33c031f1be6c4 --- /dev/null +++ b/src/lib/titled-hoc.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import bindAll from 'lodash.bindall'; +import {defineMessages, intlShape, injectIntl} from 'react-intl'; + +const messages = defineMessages({ + defaultProjectTitle: { + id: 'gui.gui.defaultProjectTitle', + description: 'Default title for project', + defaultMessage: 'Scratch Project' + } +}); + +/* Higher Order Component to get and set the project title + * @param {React.Component} WrappedComponent component to receive project title related props + * @returns {React.Component} component with project loading behavior + */ +const TitledHOC = function (WrappedComponent) { + class TitledComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleUpdateProjectTitle' + ]); + this.state = { + projectTitle: this.props.intl.formatMessage(messages.defaultProjectTitle) + }; + } + handleUpdateProjectTitle (newTitle) { + this.setState({projectTitle: newTitle}); + } + render () { + return ( + <WrappedComponent + projectTitle={this.state.projectTitle} + onUpdateProjectTitle={this.handleUpdateProjectTitle} + {...this.props} + /> + ); + } + } + + TitledComponent.propTypes = { + intl: intlShape.isRequired + }; + + // return TitledComponent; + const IntlTitledComponent = injectIntl(TitledComponent); + return IntlTitledComponent; + +}; + +export { + TitledHOC as default +}; diff --git a/src/playground/player.jsx b/src/playground/player.jsx index 6c2833f1b8108d08022e9d6c85bac9357f1f497f..e236de667eb4b12919548908daad377dfa827fe9 100644 --- a/src/playground/player.jsx +++ b/src/playground/player.jsx @@ -8,6 +8,7 @@ import Box from '../components/box/box.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; import AppStateHOC from '../lib/app-state-hoc.jsx'; +import TitledHOC from '../lib/titled-hoc.jsx'; import {setPlayer} from '../reducers/mode'; @@ -48,7 +49,7 @@ const mapDispatchToProps = dispatch => ({ }); const ConnectedPlayer = connect(mapStateToProps, mapDispatchToProps)(Player); -const WrappedPlayer = HashParserHOC(AppStateHOC(ConnectedPlayer)); +const WrappedPlayer = HashParserHOC(AppStateHOC(TitledHOC(ConnectedPlayer))); const appTarget = document.createElement('div'); document.body.appendChild(appTarget); diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx index adec922b8c206c9211888da34a8c426362b9a10b..562e57eac9c4c60ad4aea322adeec8b907067761 100644 --- a/src/playground/render-gui.jsx +++ b/src/playground/render-gui.jsx @@ -4,6 +4,7 @@ import ReactDOM from 'react-dom'; import AppStateHOC from '../lib/app-state-hoc.jsx'; import GUI from '../containers/gui.jsx'; import HashParserHOC from '../lib/hash-parser-hoc.jsx'; +import TitledHOC from '../lib/titled-hoc.jsx'; /* * Render the GUI playground. This is a separate function because importing anything @@ -12,7 +13,7 @@ import HashParserHOC from '../lib/hash-parser-hoc.jsx'; */ export default appTarget => { GUI.setAppElement(appTarget); - const WrappedGui = HashParserHOC(AppStateHOC(GUI)); + const WrappedGui = HashParserHOC(AppStateHOC(TitledHOC(GUI))); // TODO a hack for testing the backpack, allow backpack host to be set by url param const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/); diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 9340af56e1177485aa02c361c404070b6b4c6bf3..cfad8f6825ddad3a07a56b4c62757ffbc3a97909 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -12,6 +12,7 @@ import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout'; import projectIdReducer, {projectIdInitialState} from './project-id'; +import projectTitleReducer, {projectTitleInitialState} from './project-title'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; @@ -37,6 +38,7 @@ const guiInitialState = { monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, projectId: projectIdInitialState, + projectTitle: projectTitleInitialState, restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, toolbox: toolboxInitialState, @@ -80,6 +82,7 @@ const guiReducer = combineReducers({ monitors: monitorReducer, monitorLayout: monitorLayoutReducer, projectId: projectIdReducer, + projectTitle: projectTitleReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, toolbox: toolboxReducer, diff --git a/src/reducers/project-title.js b/src/reducers/project-title.js new file mode 100644 index 0000000000000000000000000000000000000000..09bf6c4eab417e626b8a719326ac685c25d46cdd --- /dev/null +++ b/src/reducers/project-title.js @@ -0,0 +1,25 @@ +const SET_PROJECT_TITLE = 'projectTitle/SET_PROJECT_TITLE'; + +// we are initializing to a blank string instead of an actual title, +// because it would be hard to localize here +const initialState = ''; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_PROJECT_TITLE: + return action.title; + default: + return state; + } +}; +const setProjectTitle = title => ({ + type: SET_PROJECT_TITLE, + title: title +}); + +export { + reducer as default, + initialState as projectTitleInitialState, + setProjectTitle +}; diff --git a/test/integration/blocks.test.js b/test/integration/blocks.test.js index 83b72620bcc04dad457f157071a54016f74381aa..b3ec62e2f6042c69ccb5f7ea985d8267e73da93e 100644 --- a/test/integration/blocks.test.js +++ b/test/integration/blocks.test.js @@ -68,11 +68,11 @@ describe('Working with the blocks', () => { await findByText('0', scope.reportedValue); await clickText('Make a Variable'); - let el = await findByXpath("//input[@placeholder='']"); + let el = await findByXpath("//input[@name='New variable name:']"); await el.sendKeys('score'); await clickButton('OK'); await clickText('Make a Variable'); - el = await findByXpath("//input[@placeholder='']"); + el = await findByXpath("//input[@name='New variable name:']"); await el.sendKeys('second variable'); await clickButton('OK'); @@ -98,7 +98,7 @@ describe('Working with the blocks', () => { await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await clickText('Make a List'); - let el = await findByXpath("//input[@placeholder='']"); + let el = await findByXpath("//input[@name='New list name:']"); await el.sendKeys('list1'); await clickButton('OK'); diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js index ecaeb56f2fa36f9bd83095563395f72a7759426f..8408c88f39bc949ef360095c1997252acf35428f 100644 --- a/test/integration/examples.test.js +++ b/test/integration/examples.test.js @@ -87,11 +87,11 @@ describe('blocks example', () => { await clickText('Variables'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await clickText('Make a Variable'); - let el = await findByXpath("//input[@placeholder='']"); + let el = await findByXpath("//input[@name='New variable name:']"); await el.sendKeys('score'); await clickButton('OK'); await clickText('Make a Variable'); - el = await findByXpath("//input[@placeholder='']"); + el = await findByXpath("//input[@name='New variable name:']"); await el.sendKeys('second variable'); await clickButton('OK'); const logs = await getLogs();