diff --git a/.eslintignore b/.eslintignore index 941eaa3fb..322608d70 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ intl/* locales/* **/*.min.js **/node_modules/* +scratch-gui/* diff --git a/.travis.yml b/.travis.yml index 365e0ab47..6766f4c43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,11 +18,26 @@ env: - API_HOST_VAR=API_HOST_$TRAVIS_BRANCH - API_HOST=${!API_HOST_VAR} - API_HOST=${API_HOST:-$API_HOST_STAGING} + - ASSET_HOST_master=https://assets.scratch.mit.edu + - ASSET_HOST_STAGING=https://assets.scratch.ly + - ASSET_HOST_VAR=ASSET_HOST_$TRAVIS_BRANCH + - ASSET_HOST=${!ASSET_HOST_VAR} + - ASSET_HOST=${ASSET_HOST:-$ASSET_HOST_STAGING} + - BACKPACK_HOST_master=https://backpack.scratch.mit.edu + - BACKPACK_HOST_STAGING=https://backpack.scratch.ly + - BACKPACK_HOST_VAR=BACKPACK_HOST_$TRAVIS_BRANCH + - BACKPACK_HOST=${!BACKPACK_HOST_VAR} + - BACKPACK_HOST=${BACKPACK_HOST:-$BACKPACK_HOST_STAGING} - ROOT_URL_master=https://scratch.mit.edu - ROOT_URL_STAGING=https://scratch.ly - ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH - ROOT_URL=${!ROOT_URL_VAR} - ROOT_URL=${ROOT_URL:-$ROOT_URL_STAGING} + - PROJECT_HOST_master=https://projects.scratch.mit.edu + - PROJECT_HOST_STAGING=https://projects.scratch.ly + - PROJECT_HOST_VAR=PROJECT_HOST_$TRAVIS_BRANCH + - PROJECT_HOST=${!PROJECT_HOST_VAR} + - PROJECT_HOST=${PROJECT_HOST:-$PROJECT_HOST_STAGING} - PATH=$PATH:$PWD/test/integration/node_modules/chromedriver/bin - AWS_ACCESS_KEY_ID=$EB_AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY=$EB_AWS_SECRET_ACCESS_KEY @@ -69,6 +84,7 @@ addons: install: - sudo -H pip install -r requirements.txt - npm --production=false install + - npm --production=false update jobs: include: - stage: test @@ -94,8 +110,8 @@ jobs: - cd test/integration - npm install - cd - - script: npm run smoke + script: npm run smoke-sauce stages: - test - name: smoke - if: branch IN (travis) and type != pull_request + if: type != pull_request diff --git a/Makefile b/Makefile index 6fed25bde..21100b6f4 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,7 @@ integration: smoke: $(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 - + smoke-verbose: $(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 -R spec diff --git a/README.md b/README.md index 210e72876..25e52a316 100644 --- a/README.md +++ b/README.md @@ -81,14 +81,17 @@ To stop the process that is making the site available to your web browser (creat `npm start` can be configured with the following environment variables -| Variable | Default | Description | -| ------------- | ----------------------------- | ---------------------------------------------- | -| `API_HOST` | `https://api.scratch.mit.edu` | Hostname for API requests | -| `SENTRY_DSN` | `''` | DSN for Sentry | -| `FALLBACK` | `''` | Pass-through location for old site | -| `GA_TRACKER` | `''` | Where to log Google Analytics data | -| `NODE_ENV` | `null` | If not `production`, app acts like development | -| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) | +| Variable | Default | Description | +| --------------- | ---------------------------------- | ---------------------------------------------- | +| `API_HOST` | `https://api.scratch.mit.edu` | Hostname for API requests | +| `ASSETS_HOST` | `https://assets.scratch.mit.edu` | Hostname for asset requests | +| `BACKPACK_HOST` | `https://backpack.scratch.mit.edu` | Hostname for backpack requests | +| `PROJECTS_HOST` | `https://projects.scratch.mit.edu` | Hostname for project requests | +| `SENTRY_DSN` | `''` | DSN for Sentry | +| `FALLBACK` | `''` | Pass-through location for old site | +| `GA_TRACKER` | `''` | Where to log Google Analytics data | +| `NODE_ENV` | `null` | If not `production`, app acts like development | +| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) | **NOTE:** Because by default `API_HOST=https://api.scratch.mit.edu`, please be aware that, by default, you will be seeing and interacting with real data on the Scratch website. diff --git a/dev-server/index.js b/dev-server/index.js index 61874bd15..5ab70abbd 100644 --- a/dev-server/index.js +++ b/dev-server/index.js @@ -21,7 +21,18 @@ routes.forEach(route => { app.get(route.pattern, handler(route)); }); -app.use(webpackDevMiddleware(compiler)); +var middlewareOptions = {}; +if (process.env.USE_DOCKER_WATCHOPTIONS) { + middlewareOptions = { + watchOptions: { + aggregateTimeout: 500, + poll: 2500, + ignored: ['node_modules', 'build'] + } + }; +} + +app.use(webpackDevMiddleware(compiler, middlewareOptions)); var proxyHost = process.env.FALLBACK || ''; if (proxyHost !== '') { diff --git a/docker-compose.yml b/docker-compose.yml index 8915d2b21..e7919d96f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,6 @@ version: '3.4' volumes: npm_data: runtime_data: - intl_data: networks: scratch-api_scratch_network: @@ -15,7 +14,8 @@ services: environment: - API_HOST=http://localhost:8491 - FALLBACK=http://localhost:8080 - build: + - USE_DOCKER_WATCHOPTIONS=true + build: context: ./ dockerfile: Dockerfile image: scratch-www:latest @@ -33,7 +33,6 @@ services: nocopy: true - npm_data:/var/app/current/node_modules - runtime_data:/runtime - - intl_data:/var/app/current/intl ports: - "8333:8333" networks: diff --git a/package.json b/package.json index 6b1315452..e8bbee511 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "lodash.merge": "3.3.2", "lodash.omit": "3.1.0", "lodash.range": "3.0.1", + "lodash.truncate": "4.4.2", "minilog": "2.0.8", "node-dir": "0.1.16", "node-sass": "4.6.1", diff --git a/src/_colors.scss b/src/_colors.scss index 10f4a49ef..5ef54a7ce 100644 --- a/src/_colors.scss +++ b/src/_colors.scss @@ -5,6 +5,7 @@ $ui-blue-10percent: hsla(215, 100, 65, .1); $ui-blue-25percent: hsla(215, 100, 65, .25); $ui-orange: hsla(38, 100, 55, 1); // #FFAB19 Control Primary +$ui-orange-10percent: hsla(35, 90, 55, .1); $ui-orange-25percent: hsla(35, 90, 55, .25); $ui-light-gray: hsla(0, 0, 98, 1); //#FAFAFA @@ -13,7 +14,6 @@ $ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3 $background-color: hsla(0, 0, 99, 1); //#FDFDFD - /* UI Secondary Colors */ /* 3.0 colors */ /* Using www naming convention for now, should be consistent with gui */ @@ -27,20 +27,25 @@ $ui-coral-dark: hsla(350, 100, 60, 1); // #FF3355 More Blocks tertiary $ui-white: hsla(0, 100%, 100%, 1); //#FFF $ui-white-15percent: hsla(0, 100%, 100%, .15); //#FFF +$ui-light-primary: hsl(215, 100, 95); $ui-border: hsla(0, 0, 85, 1); //#D9D9D9 +/* modals */ $ui-mint-green: hsl(163, 69, 44); +$ui-light-mint: hsl(163, 53, 67); /* Overlay UI Gray Colors */ $active-gray: hsla(0, 0, 0, .1); $active-dark-gray: hsla(0, 0, 0, .2); $box-shadow-gray: hsla(0, 0, 0, .25); $overlay-gray: hsla(0, 0, 0, .75); +$transparent-light-blue: rgba(229, 240, 254, 0); /* Typography Colors */ $header-gray: hsla(225, 15, 40, 1); //#575E75 $type-gray: hsla(225, 15, 40, 1); //#575E75 +$type-gray-75percent: hsla(225, 15, 40, .75); $type-white: hsla(0, 100, 100, 1); //#FFF $link-blue: $ui-blue; diff --git a/src/components/footer/www/footer.jsx b/src/components/footer/www/footer.jsx index 523389516..74bd1add8 100644 --- a/src/components/footer/www/footer.jsx +++ b/src/components/footer/www/footer.jsx @@ -108,7 +108,7 @@ const Footer = props => (
- +
diff --git a/src/components/forms/charcount.scss b/src/components/forms/charcount.scss index ff5d85e94..05d1a4bee 100644 --- a/src/components/forms/charcount.scss +++ b/src/components/forms/charcount.scss @@ -2,7 +2,7 @@ .char-count { letter-spacing: 1px; - color: lighten($type-gray, 30%); + color: $type-gray-75percent; font-weight: 500; &.overmax { diff --git a/src/components/forms/input.scss b/src/components/forms/input.scss index 214365f28..3fd202c3e 100644 --- a/src/components/forms/input.scss +++ b/src/components/forms/input.scss @@ -2,7 +2,6 @@ @import "../../frameless"; $base-bg: $ui-light-gray; -$pass-bg: lighten($ui-aqua, 35%); .row { label { @@ -32,8 +31,7 @@ $pass-bg: lighten($ui-aqua, 35%); } &.pass { - border: 1px solid $active-dark-gray; - background-color: $pass-bg; + border: 1px solid $ui-aqua; } /* IE10/11-specific style resets */ diff --git a/src/components/forms/validations.jsx b/src/components/forms/validations.jsx index c83ce63cb..c882aa40d 100644 --- a/src/components/forms/validations.jsx +++ b/src/components/forms/validations.jsx @@ -28,8 +28,8 @@ module.exports.validationHOCFactory = defaultValidationErrors => (Component => { diff --git a/src/components/modal/addtostudio/container.jsx b/src/components/modal/addtostudio/container.jsx new file mode 100644 index 000000000..582f194eb --- /dev/null +++ b/src/components/modal/addtostudio/container.jsx @@ -0,0 +1,72 @@ +const bindAll = require('lodash.bindall'); +const PropTypes = require('prop-types'); +const React = require('react'); +const AddToStudioModalPresentation = require('./presentation.jsx'); + +class AddToStudioModal extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleRequestClose', + 'handleSubmit' + ]); + + this.state = { + waitingToClose: false + }; + } + + componentWillUpdate () { + this.closeIfFinishedUpdating(); + } + + hasOutstandingUpdates () { + return (this.props.studios.some(studio => (studio.hasRequestOutstanding === true))); + } + + closeIfFinishedUpdating () { + if (this.state.waitingToClose === true && this.hasOutstandingUpdates() === false) { + this.closeAndStopWaiting(); + } + } + + // before closing, set waitingToClose to false. That way, if user reopens + // modal, it won't unexpectedly close. + closeAndStopWaiting () { + this.setState({waitingToClose: false}, () => { + this.props.onRequestClose(); + }); + } + + handleRequestClose () { + this.closeAndStopWaiting(); + } + + handleSubmit () { + this.setState({waitingToClose: true}, () => { + this.closeIfFinishedUpdating(); + }); + } + + render () { + return ( + + ); + } +} + +AddToStudioModal.propTypes = { + isOpen: PropTypes.bool, + onRequestClose: PropTypes.func, + onToggleStudio: PropTypes.func, + studios: PropTypes.arrayOf(PropTypes.object) +}; + +module.exports = AddToStudioModal; diff --git a/src/components/modal/addtostudio/modal.scss b/src/components/modal/addtostudio/modal.scss new file mode 100644 index 000000000..08b4c1abc --- /dev/null +++ b/src/components/modal/addtostudio/modal.scss @@ -0,0 +1,189 @@ +@import "../../../colors"; +@import "../../../frameless"; + +.mod-addToStudio * { + box-sizing: border-box; +} + +.mod-addToStudio { + margin: 100px auto; + outline: none; + padding: 0; + width: 36.25rem; /* 580px; */ + height: 388px; /* 24.25rem; */ + overflow: hidden; + user-select: none; +} + +.addToStudio-modal-header { + box-shadow: inset 0 -1px 0 0 $ui-blue-dark; + background-color: $ui-blue; + padding-top: .75rem; + width: 100%; + height: 3rem; + box-sizing: border-box; +} + +.addToStudio-content-label { + text-align: center; + color: $type-white; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 1rem; + font-weight: bold; +} + +.addToStudio-modal-content { + margin: 0 auto; + width: 100%; + font-size: .875rem; +} + +.studio-list-outer-scrollbox { + position: relative; + background-color: $ui-blue-10percent; +} + +.studio-list-inner-scrollbox { + margin-right: .5rem; + padding-right: .5rem; + height: 16.9375rem; + overflow: scroll; + overflow-x: hidden; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: $active-dark-gray; + height: 92px; + } + + &::-webkit-scrollbar-track { + margin-top: 8px; + margin-bottom: 10px; + } +} + +.studio-list-container { + display: flex; + padding: .40625rem 0 0 1.46875rem; + justify-content: flex-start; + flex-flow: row wrap; +} +/* NOTE: force scrolling: add to above: + min-height: 30rem; +*/ + +.studio-list-bottom-gradient { + position: absolute; + right: 1rem; + bottom: 0; + left: 0; + background: linear-gradient( + $transparent-light-blue, + $ui-light-primary + ); + height: 32px; + pointer-events: none; /* pass clicks through to buttons underneath */ +} + + +.studio-selector-button { + display: flex; + position: relative; + margin: .21875rem .21875rem; + border-radius: .5rem; + background-color: $ui-white; + padding: 0; + width: 16.1875rem; /* 259px */ + height: 2.5rem; + box-sizing: border-box; + justify-content: space-between; +} + +.studio-selector-button-text { + position: absolute; + /* per spec, should be: + margin: .375rem 2.18375rem .375rem .6875rem + but in practice, our css seems to vertically align text to top, where + invision spec aligned to middle. + */ + margin: .575rem 2.18375rem .175rem .6875rem; + width: 13.3125rem; + height: 1rem; /* diff from spec, in case we ever do valign to middle */ + line-height: 1.25rem; + font-family: "Helvetica Neue"; + font-size: .875rem; + font-weight: regular; +} + +.studio-selector-button-selected { + background-color: $ui-mint-green; + color: $ui-white; +} + +.studio-selector-button-waiting { + background-color: $ui-light-mint; + color: $ui-white; +} + +.studio-selector-button-text-selected { + color: $ui-white; +} + +.studio-selector-button-text-unselected { + color: $type-gray; +} + +.studio-status-icon { + position: absolute; + margin: .5rem .625rem .5rem 14.0625rem; + border-radius: .75rem; + padding: .0625rem .075rem; + width: 1.5rem; + height: 1.5rem; + color: $ui-white; + box-sizing: border-box; +} + +.studio-status-icon-unselected { + background-color: $ui-blue; +} + +.submit-button { + background-color: $ui-blue; +} + +.submit-button-waiting { + background-color: $ui-blue; +} + +.studio-status-icon-plus-img { + width: 1.4rem; + height: 1.4rem; +} + +.studio-status-icon--img { + width: 1.4rem; + height: 1.4rem; +} + +.action-button-text .spinner-smooth { + margin: .2125rem auto; + width: 1.875rem; + height: 1rem; +} + +.studio-status-icon .spinner-smooth { + position: unset; /* don't understand why neither relative nor absolute work */ +} + +.studio-status-icon .spinner-smooth .circle { + /* overlay spinner on circle */ + position: absolute; + margin: .1875rem; /* stay within boundaries of circle */ + width: 75%; /* stay within boundaries of circle */ + height: 75%; /* stay within boundaries of circle */ +} diff --git a/src/components/modal/addtostudio/presentation.jsx b/src/components/modal/addtostudio/presentation.jsx new file mode 100644 index 000000000..29e8eedcc --- /dev/null +++ b/src/components/modal/addtostudio/presentation.jsx @@ -0,0 +1,119 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +const FormattedMessage = require('react-intl').FormattedMessage; +const injectIntl = require('react-intl').injectIntl; +const intlShape = require('react-intl').intlShape; +const Modal = require('../base/modal.jsx'); + +const Form = require('../../forms/form.jsx'); +const Button = require('../../forms/button.jsx'); +const Spinner = require('../../spinner/spinner.jsx'); +const FlexRow = require('../../flex-row/flex-row.jsx'); +const StudioButton = require('./studio-button.jsx'); + +require('../../forms/button.scss'); +require('./modal.scss'); + +const AddToStudioModalPresentation = ({ + intl, + isOpen, + studios, + waitingToClose, + onToggleStudio, + onRequestClose, + onSubmit +}) => { + const contentLabel = intl.formatMessage({id: 'addToStudio.title'}); + const studioButtons = studios.map(studio => ( + + )); + + return ( + +
+
+
+ {contentLabel} +
+
+
+
+
+
+ {studioButtons} +
+
+
+
+ + +
+ + + {waitingToClose ? [ + + ] : [ + + ]} + +
+
+
+ + ); +}; + +AddToStudioModalPresentation.propTypes = { + intl: intlShape, + isOpen: PropTypes.bool, + onRequestClose: PropTypes.func, + onSubmit: PropTypes.func, + onToggleStudio: PropTypes.func, + studios: PropTypes.arrayOf(PropTypes.object), + waitingToClose: PropTypes.bool +}; + +module.exports = injectIntl(AddToStudioModalPresentation); diff --git a/src/components/modal/addtostudio/studio-button.jsx b/src/components/modal/addtostudio/studio-button.jsx new file mode 100644 index 000000000..137c4cc8a --- /dev/null +++ b/src/components/modal/addtostudio/studio-button.jsx @@ -0,0 +1,72 @@ +const truncateAtWordBoundary = require('../../../lib/truncate').truncateAtWordBoundary; +const PropTypes = require('prop-types'); +const React = require('react'); +const classNames = require('classnames'); +const Spinner = require('../../spinner/spinner.jsx'); + +require('./modal.scss'); + +const StudioButton = ({ + hasRequestOutstanding, + id, + includesProject, + title, + onToggleStudio +}) => { + const checkmark = ( + checkmark-icon + ); + const plus = ( + plus-icon + ); + return ( +
+
+ {truncateAtWordBoundary(title, 25)} +
+
+ {(hasRequestOutstanding ? + () : + (includesProject ? checkmark : plus))} +
+
+ ); +}; + +StudioButton.propTypes = { + hasRequestOutstanding: PropTypes.bool, + id: PropTypes.number, + includesProject: PropTypes.bool, + onToggleStudio: PropTypes.func, + title: PropTypes.string +}; + +module.exports = StudioButton; diff --git a/src/components/modal/base/modal.scss b/src/components/modal/base/modal.scss index 9fec2deef..cb878ae12 100644 --- a/src/components/modal/base/modal.scss +++ b/src/components/modal/base/modal.scss @@ -5,10 +5,14 @@ position: relative; margin: 3.75rem auto; border-radius: 1rem; - box-shadow: 0 0 0 1px $active-gray; + box-shadow: 0 0 0 4px $ui-white-15percent; background-color: $ui-white; padding: 0; width: 48.75rem; + + &:focus { + outline: none; + } } .modal-overlay { @@ -21,10 +25,6 @@ background-color: transparentize($ui-blue, .3); } -.modal-content:focus { - outline: none; -} - $modal-close-size: 1rem; .modal-content-close { position: absolute; @@ -59,3 +59,52 @@ $modal-close-size: 1rem; position: fixed; } } + +/* Close button, Submit button, etc. */ +.action-buttons { + display: flex; + margin: 1.125rem .8275rem .9375rem .8275rem; + line-height: 1.5rem; + justify-content: flex-end !important; + align-items: flex-start; + flex-wrap: nowrap; +} + +/* setting overall modal to contain overflow looks good, but isn't +compatible with elements (like validation popups) that need to bleed +past modal boundary. This class can be used to force modal button +row to appear to contain overflow. */ +.action-buttons-overflow-fix { + margin-bottom: .9375rem; +} + +.action-button { + margin: 0 0 0 .54625rem; + border-radius: .25rem; + padding: 6px 1.25rem 14px 1.25rem; + height: 36px; +} + +.action-button.close-button { + border: 1px solid $active-gray; +} + +.action-button-text { + display: flex; +} + +.action-button.disabled { + background-color: $active-dark-gray; +} + +.error-text +{ + display: block; + border: 1px solid $active-gray; + border-radius: 5px; + background-color: $ui-orange; + padding: 1rem; + min-height: 1rem; + overflow: visible; + color: $type-white; +} diff --git a/src/components/modal/report/modal.jsx b/src/components/modal/report/modal.jsx index 14be94544..7271458fd 100644 --- a/src/components/modal/report/modal.jsx +++ b/src/components/modal/report/modal.jsx @@ -1,16 +1,20 @@ const bindAll = require('lodash.bindall'); const PropTypes = require('prop-types'); const React = require('react'); +const connect = require('react-redux').connect; const FormattedMessage = require('react-intl').FormattedMessage; const injectIntl = require('react-intl').injectIntl; const intlShape = require('react-intl').intlShape; const Modal = require('../base/modal.jsx'); +const classNames = require('classnames'); const Form = require('../../forms/form.jsx'); const Button = require('../../forms/button.jsx'); const Select = require('../../forms/select.jsx'); const Spinner = require('../../spinner/spinner.jsx'); const TextArea = require('../../forms/textarea.jsx'); +const FlexRow = require('../../flex-row/flex-row.jsx'); +const previewActions = require('../../../redux/preview.js'); require('../../forms/button.scss'); require('./modal.scss'); @@ -67,12 +71,24 @@ class ReportModal extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleReportCategorySelect' + 'handleCategorySelect', + 'handleValid', + 'handleInvalid' ]); - this.state = {reportCategory: this.props.report.category}; + this.state = { + category: '', + notes: '', + valid: false + }; } - handleReportCategorySelect (name, value) { - this.setState({reportCategory: value}); + handleCategorySelect (name, value) { + this.setState({category: value}); + } + handleValid () { + this.setState({valid: true}); + } + handleInvalid () { + this.setState({valid: false}); } lookupPrompt (value) { const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt; @@ -81,17 +97,24 @@ class ReportModal extends React.Component { render () { const { intl, + isConfirmed, + isError, + isOpen, + isWaiting, onReport, // eslint-disable-line no-unused-vars - report, + onRequestClose, type, ...modalProps } = this.props; + const submitEnabled = this.state.valid && !isWaiting; + const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'}; const contentLabel = intl.formatMessage({id: `report.${type}`}); return (
@@ -101,72 +124,120 @@ class ReportModal extends React.Component {
-
- - - - ) - }} - /> -
-