Merge pull request #3150 from LLK/release/2019-07-18

[Master] Release 2019-07-18
This commit is contained in:
picklesrus 2019-07-18 15:47:25 -04:00 committed by GitHub
commit 23fe3677ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 16048 additions and 309 deletions

View file

@ -98,17 +98,23 @@ addons:
- ubuntu-toolchain-r-test - ubuntu-toolchain-r-test
packages: packages:
- g++-4.8 - g++-4.8
before_install:
# package-lock.json was introduced in npm@5
- '[[ $(node -v) =~ ^v9.*$ ]] || npm install -g npm@latest' # skipped when using node 9
- npm install -g greenkeeper-lockfile
install: install:
- sudo -H pip install -r requirements.txt - sudo -H pip install -r requirements.txt
- npm --production=false install - npm --production=false install
- npm --production=false update - npm --production=false update
before_script: greenkeeper-lockfile-update
after_script: greenkeeper-lockfile-upload
jobs: jobs:
include: include:
- stage: test - stage: test
deploy: deploy:
- provider: script - provider: script
skip_cleanup: $SKIP_CLEANUP skip_cleanup: $SKIP_CLEANUP
script: env make sync script: npm run deploy
on: on:
repo: LLK/scratch-www repo: LLK/scratch-www
branch: branch:
@ -117,7 +123,7 @@ jobs:
- release/* - release/*
- provider: script - provider: script
skip_cleanup: $SKIP_CLEANUP skip_cleanup: $SKIP_CLEANUP
script: env make sync script: npm run deploy
on: on:
repo: LLK/scratch-www repo: LLK/scratch-www
branch: branch:
@ -127,7 +133,7 @@ jobs:
- cd test/integration - cd test/integration
- npm install - npm install
- cd - - cd -
script: npm run smoke-sauce script: npm run test:smoke:sauce
stages: stages:
- test - test
- name: smoke - name: smoke

View file

@ -1,10 +0,0 @@
FROM node:8
RUN mkdir -p /var/app/current
WORKDIR /var/app/current
COPY . ./
RUN rm -rf ./node_modules
RUN npm install
EXPOSE 8333

View file

@ -1,96 +0,0 @@
ESLINT=./node_modules/.bin/eslint
NODE= NODE_OPTIONS=--max_old_space_size=8000 node
SCRATCH_DOCKER_CONFIG=./node_modules/.bin/docker_config.sh
S3CMD=s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600
TAP=./node_modules/.bin/tap
WATCH= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/watch
WEBPACK= NODE_OPTIONS=--max_old_space_size=8000 ./node_modules/.bin/webpack
# ------------------------------------
$(SCRATCH_DOCKER_CONFIG):
npm install scratch-docker
docker-up: $(SCRATCH_DOCKER_CONFIG)
$(SCRATCH_DOCKER_CONFIG) network create
docker-compose up
docker-down:
docker-compose down
# ------------------------------------
build:
@make clean
@make translations
@make webpack
clean:
rm -rf ./build
rm -rf ./intl
mkdir -p build
mkdir -p intl
deploy:
@make build
@make sync
translations:
./bin/get-localized-urls localized-urls.json
./bin/build-locales node_modules/scratch-l10n/www intl
webpack:
$(WEBPACK) --bail
sync-s3:
$(S3CMD) --exclude '.DS_Store' --exclude '*.svg' --exclude '*.js' ./build/ s3://$(S3_BUCKET_NAME)/
$(S3CMD) --exclude '*' --include '*.svg' --mime-type 'image/svg+xml' ./build/ s3://$(S3_BUCKET_NAME)/
$(S3CMD) --exclude '*' --include '*.js' --mime-type 'application/javascript' ./build/ s3://$(S3_BUCKET_NAME)/
sync-fastly:
$(NODE) ./bin/configure-fastly.js
sync:
@make sync-s3
@make sync-fastly
# ------------------------------------
start:
$(NODE) ./dev-server/index.js
# ------------------------------------
test:
@make lint
@make build
@make tap
lint:
$(ESLINT) . --ext .js,.jsx,.json
unit:
$(TAP) ./test/unit/*.js --no-coverage -R classic
integration:
$(TAP) ./test/integration/*.js --no-coverage -R classic
smoke:
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 --no-coverage -R classic
smoke-verbose:
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 -R spec --no-coverage -R classic
localization:
$(TAP) ./test/localization/*.js --no-coverage -R classic
tap:
$(TAP) ./test/{unit,localization}/*.js --no-coverage -R classic
coverage:
$(TAP) ./test/{unit,localization}/*.js --coverage --coverage-report=lcov
# ------------------------------------
.PHONY: build clean deploy translations webpack start test lint unit functional integration localization tap coverage

View file

@ -109,7 +109,7 @@ npm install
virtualenv ENV virtualenv ENV
. ENV/bin/activate . ENV/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
make deploy npm run build && npm run deploy
``` ```
| Variable | Default | Description | | Variable | Default | Description |
@ -138,35 +138,3 @@ Additionally, if you set `FALLBACK=https://scratch.mit.edu`, be aware that click
#### Windows #### Windows
Some users have experienced difficulties when trying to get our web client to work on Windows. One solution could be to use [Cygwin](https://www.cygwin.com/). If that doesn't work, you might want to use [Wubi](https://wiki.ubuntu.com/WubiGuide) (Windows XP, Vista, 7) or [Wubiuefi](https://github.com/hakuna-m/wubiuefi) (Windows 8 or higher). Wubi(uefi) is a Windows Installer for Ubuntu that allows you to have Ubuntu and Windows on one disk, without the need of an extra partition. Some users have experienced difficulties when trying to get our web client to work on Windows. One solution could be to use [Cygwin](https://www.cygwin.com/). If that doesn't work, you might want to use [Wubi](https://wiki.ubuntu.com/WubiGuide) (Windows XP, Vista, 7) or [Wubiuefi](https://github.com/hakuna-m/wubiuefi) (Windows 8 or higher). Wubi(uefi) is a Windows Installer for Ubuntu that allows you to have Ubuntu and Windows on one disk, without the need of an extra partition.
#### Docker
_This section is only relevant to the Scratch Team since it requires access to private repositories, so is not usable by 3rd party contributors._
A set of [Docker](https://www.docker.com/what-docker) related files are provided to create isolated [container](https://www.docker.com/what-container) environments suitable for end-to-end local development:
* Dockerfile
* docker-compose.yml
* docker_entrypoint.sh
##### Docker Quick Start (CLI)
Make sure you already have the Scratch REST API running locally in its docker environment.
```
$ docker-compose up
```
After this has launched you will be able to access a running copy of `scratch-www` on port 8333 via `http://localhost:8333`
##### Docker Configuration
`Dockerfile` defines how a `scratch-www` docker image is created.
`docker-compose.yml` takes care of launching `scratch-www` into a development environment that is composed of other components, such as the Scratch REST API server and the legacy Scratch code. If you have not already setup the Scratch REST API in your local environment, you will need to modify `docker-compose.yml` by removing `external: true` from:
```yaml
networks:
scratchapi_scratch_network:
external: true
```

View file

@ -37,11 +37,11 @@ async.auto({
} }
}); });
}, },
recvCustomVCL: ['version', function (cb, results) { recvCustomVCL: ['version', function (results, cb) {
// For all the routes in routes.json, construct a varnish-style regex that matches // For all the routes in routes.json, construct a varnish-style regex that matches
// on any of those route conditions. // on any of those route conditions.
var notPassStatement = fastlyConfig.getAppRouteCondition('../build/*', routes, extraAppRoutes, __dirname); var notPassStatement = fastlyConfig.getAppRouteCondition('../build/*', routes, extraAppRoutes, __dirname);
// For a non-pass condition, point backend at s3 // For a non-pass condition, point backend at s3
var recvCondition = '' + var recvCondition = '' +
'if (' + notPassStatement + ') {\n' + 'if (' + notPassStatement + ') {\n' +
@ -74,7 +74,7 @@ async.auto({
' return(pass);\n' + ' return(pass);\n' +
' }\n' + ' }\n' +
'}\n'; '}\n';
fastly.setCustomVCL( fastly.setCustomVCL(
results.version, results.version,
@ -83,14 +83,14 @@ async.auto({
cb cb
); );
}], }],
fetchCustomVCL: ['version', function (cb, results) { fetchCustomVCL: ['version', function (results, cb) {
var passStatement = fastlyConfig.negateConditionStatement( var passStatement = fastlyConfig.negateConditionStatement(
fastlyConfig.getAppRouteCondition('../build/*', routes, extraAppRoutes, __dirname) fastlyConfig.getAppRouteCondition('../build/*', routes, extraAppRoutes, __dirname)
); );
var ttlCondition = fastlyConfig.setResponseTTL(passStatement); var ttlCondition = fastlyConfig.setResponseTTL(passStatement);
fastly.setCustomVCL(results.version, 'fetch-condition', ttlCondition, cb); fastly.setCustomVCL(results.version, 'fetch-condition', ttlCondition, cb);
}], }],
appRouteRequestConditions: ['version', function (cb, results) { appRouteRequestConditions: ['version', function (results, cb) {
var conditions = {}; var conditions = {};
async.forEachOf(routes, function (route, id, cb2) { async.forEachOf(routes, function (route, id, cb2) {
var condition = { var condition = {
@ -110,7 +110,7 @@ async.auto({
cb(null, conditions); cb(null, conditions);
}); });
}], }],
appRouteHeaders: ['version', 'appRouteRequestConditions', function (cb, results) { appRouteHeaders: ['version', 'appRouteRequestConditions', function (results, cb) {
var headers = {}; var headers = {};
async.forEachOf(routes, function (route, id, cb2) { async.forEachOf(routes, function (route, id, cb2) {
if (route.redirect) { if (route.redirect) {
@ -133,7 +133,7 @@ async.auto({
}; };
fastly.setResponseObject(results.version, responseObject, cb3); fastly.setResponseObject(results.version, responseObject, cb3);
}, },
redirectHeader: ['responseCondition', function (cb3, redirectResults) { redirectHeader: ['responseCondition', function (redirectResults, cb3) {
var header = { var header = {
name: fastlyConfig.getHeaderNameForRoute(route), name: fastlyConfig.getHeaderNameForRoute(route),
action: 'set', action: 'set',
@ -172,7 +172,7 @@ async.auto({
cb(null, headers); cb(null, headers);
}); });
}], }],
tipbarRedirectHeaders: ['version', function (cb, results) { tipbarRedirectHeaders: ['version', function (results, cb) {
async.auto({ async.auto({
requestCondition: function (cb2) { requestCondition: function (cb2) {
var condition = { var condition = {
@ -192,7 +192,7 @@ async.auto({
}; };
fastly.setCondition(results.version, condition, cb2); fastly.setCondition(results.version, condition, cb2);
}, },
responseObject: ['requestCondition', function (cb2, redirectResults) { responseObject: ['requestCondition', function (redirectResults, cb2) {
var responseObject = { var responseObject = {
name: 'redirects/?tip_bar=', name: 'redirects/?tip_bar=',
status: 301, status: 301,
@ -201,7 +201,7 @@ async.auto({
}; };
fastly.setResponseObject(results.version, responseObject, cb2); fastly.setResponseObject(results.version, responseObject, cb2);
}], }],
redirectHeader: ['responseCondition', function (cb2, redirectResults) { redirectHeader: ['responseCondition', function (redirectResults, cb2) {
var header = { var header = {
name: 'redirects/?tip_bar=', name: 'redirects/?tip_bar=',
action: 'set', action: 'set',
@ -218,7 +218,7 @@ async.auto({
cb(null, redirectResults); cb(null, redirectResults);
}); });
}], }],
embedRedirectHeaders: ['version', function (cb, results) { embedRedirectHeaders: ['version', function (results, cb) {
async.auto({ async.auto({
requestCondition: function (cb2) { requestCondition: function (cb2) {
var condition = { var condition = {
@ -238,7 +238,7 @@ async.auto({
}; };
fastly.setCondition(results.version, condition, cb2); fastly.setCondition(results.version, condition, cb2);
}, },
responseObject: ['requestCondition', function (cb2, redirectResults) { responseObject: ['requestCondition', function (redirectResults, cb2) {
var responseObject = { var responseObject = {
name: 'redirects/projects/embed', name: 'redirects/projects/embed',
status: 301, status: 301,
@ -247,7 +247,7 @@ async.auto({
}; };
fastly.setResponseObject(results.version, responseObject, cb2); fastly.setResponseObject(results.version, responseObject, cb2);
}], }],
redirectHeader: ['responseCondition', function (cb2, redirectResults) { redirectHeader: ['responseCondition', function (redirectResults, cb2) {
var header = { var header = {
name: 'redirects/projects/embed', name: 'redirects/projects/embed',
action: 'set', action: 'set',

View file

@ -22,15 +22,6 @@ routes.forEach(route => {
}); });
var middlewareOptions = {}; var middlewareOptions = {};
if (process.env.USE_DOCKER_WATCHOPTIONS) {
middlewareOptions = {
watchOptions: {
aggregateTimeout: 500,
poll: 2500,
ignored: ['node_modules', 'build']
}
};
}
app.use(webpackDevMiddleware(compiler, middlewareOptions)); app.use(webpackDevMiddleware(compiler, middlewareOptions));

View file

@ -1,38 +0,0 @@
version: '3.4'
volumes:
npm_data:
runtime_data:
networks:
default:
external:
name: scratchapi_scratch_network
services:
app:
container_name: scratch-www-app
hostname: scratch-www-app
environment:
- API_HOST=http://localhost:8491
- FALLBACK=http://scratchr2-app:8080
- USE_DOCKER_WATCHOPTIONS=true
build:
context: ./
dockerfile: Dockerfile
image: scratch-www:latest
command: ./docker_entrypoint.sh npm start
volumes:
- type: bind
source: ./
target: /var/app/current
volume:
nocopy: true
- type: bind
source: ../scratch-gui
target: /var/app/current/scratch-gui
volume:
nocopy: true
- npm_data:/var/app/current/node_modules
- runtime_data:/runtime
ports:
- "8333:8333"

View file

@ -1,11 +0,0 @@
#!/usr/bin/env bash
echo "App Entrypoint"
if [ ! -f /runtime/.translations ]; then
echo "Generating intl/translations"
make translations
touch /runtime/.translations
fi
exec "$@"

15727
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,15 +3,23 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Standalone WWW client for Scratch", "description": "Standalone WWW client for Scratch",
"scripts": { "scripts": {
"start": "make start", "start": "node ./dev-server/index.js",
"stop": "make stop", "test": "npm run test:lint && npm run build && npm run test:tap",
"test": "make test", "test:lint": "eslint . --ext .js,.jsx,.json",
"smoke": "tap ./test/integration/smoke-testing/*.js --timeout=3600 --no-coverage -R classic", "test:smoke": "tap ./test/integration/smoke-testing/*.js --timeout=3600 --no-coverage -R classic",
"smoke-verbose": "tap ./test/integration/smoke-testing/*.js --timeout=3600 --no-coverage -R spec", "test:smoke:verbose": "tap ./test/integration/smoke-testing/*.js --timeout=3600 --no-coverage -R spec",
"smoke-sauce": "SMOKE_REMOTE=true tap ./test/integration/smoke-testing/*.js --timeout=60000 --no-coverage -R classic", "test:smoke:sauce": "SMOKE_REMOTE=true tap ./test/integration/smoke-testing/*.js --timeout=60000 --no-coverage -R classic",
"watch": "make watch", "test:tap": "tap ./test/{unit,localization}/*.js --no-coverage -R classic",
"build": "make build", "test:coverage": "tap ./test/{unit,localization}/*.js --coverage --coverage-report=lcov",
"dev": "make watch && make start &", "build": "npm run clean && npm run translate && webpack --bail",
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
"deploy": "npm run deploy:s3 && npm run deploy:fastly",
"deploy:fastly": "node ./bin/configure-fastly.js",
"deploy:s3": "npm run deploy:s3:all && npm run deploy:s3:svg && npm run deploy:s3:js",
"deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600",
"deploy:s3:all": "npm run deploy:s3cmd -- --exclude '.DS_Store' --exclude '*.svg' --exclude '*.js' ./build/ s3://$S3_BUCKET_NAME/",
"deploy:s3:svg": "npm run deploy:s3cmd -- --exclude '*' --include '*.svg' --mime-type 'image/svg+xml' ./build/ s3://$S3_BUCKET_NAME/",
"deploy:s3:js": "npm run deploy:s3cmd -- --exclude '*' --include '*.js' --mime-type 'application/javascript' ./build/ s3://$S3_BUCKET_NAME/",
"translate:urls": "node ./bin/get-localized-urls localized-urls.json", "translate:urls": "node ./bin/get-localized-urls localized-urls.json",
"translate:files": "node ./bin/build-locales node_modules/scratch-l10n/www intl", "translate:files": "node ./bin/build-locales node_modules/scratch-l10n/www intl",
"translate": "npm run translate:urls && npm run translate:files" "translate": "npm run translate:urls && npm run translate:files"
@ -36,13 +44,12 @@
"lodash.defaults": "4.0.1", "lodash.defaults": "4.0.1",
"newrelic": "1.25.4", "newrelic": "1.25.4",
"react-helmet": "5.2.0", "react-helmet": "5.2.0",
"scratch-docker": "^1.0.2",
"scratch-parser": "^5.0.0", "scratch-parser": "^5.0.0",
"scratch-storage": "^0.5.1" "scratch-storage": "^0.5.1"
}, },
"devDependencies": { "devDependencies": {
"ajv": "6.4.0", "ajv": "6.4.0",
"async": "1.5.2", "async": "3.1.0",
"autoprefixer": "6.3.6", "autoprefixer": "6.3.6",
"babel-cli": "6.26.0", "babel-cli": "6.26.0",
"babel-core": "6.23.1", "babel-core": "6.23.1",
@ -65,13 +72,13 @@
"eslint-plugin-react": "7.4.0", "eslint-plugin-react": "7.4.0",
"exenv": "1.2.0", "exenv": "1.2.0",
"fastly": "1.2.1", "fastly": "1.2.1",
"file-loader": "0.8.4", "file-loader": "4.0.0",
"formik": "1.5.4", "formik": "1.5.4",
"formsy-react": "1.1.4", "formsy-react": "1.1.4",
"formsy-react-components": "1.0.0", "formsy-react-components": "1.0.0",
"git-bundle-sha": "0.0.2", "git-bundle-sha": "0.0.2",
"glob": "5.0.15", "glob": "5.0.15",
"google-libphonenumber": "1.0.21", "google-libphonenumber": "3.2.3",
"html-webpack-plugin": "2.22.0", "html-webpack-plugin": "2.22.0",
"iso-3166-2": "0.4.0", "iso-3166-2": "0.4.0",
"json-loader": "0.5.2", "json-loader": "0.5.2",
@ -84,7 +91,7 @@
"lodash.merge": "3.3.2", "lodash.merge": "3.3.2",
"lodash.mergewith": "4.6.1", "lodash.mergewith": "4.6.1",
"lodash.omit": "3.1.0", "lodash.omit": "3.1.0",
"lodash.range": "3.0.1", "lodash.range": "3.2.0",
"lodash.uniqby": "4.7.0", "lodash.uniqby": "4.7.0",
"minilog": "2.0.8", "minilog": "2.0.8",
"node-dir": "0.1.16", "node-dir": "0.1.16",
@ -96,7 +103,7 @@
"react": "16.2.0", "react": "16.2.0",
"react-dom": "16.2.0", "react-dom": "16.2.0",
"react-intl": "2.8.0", "react-intl": "2.8.0",
"react-modal": "3.4.1", "react-modal": "3.8.2",
"react-onclickoutside": "6.7.1", "react-onclickoutside": "6.7.1",
"react-redux": "5.0.7", "react-redux": "5.0.7",
"react-responsive": "3.0.0", "react-responsive": "3.0.0",
@ -106,13 +113,13 @@
"redux": "3.5.2", "redux": "3.5.2",
"redux-thunk": "2.0.1", "redux-thunk": "2.0.1",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20190711011201", "scratch-gui": "0.1.0-prerelease.20190718181300",
"scratch-l10n": "latest", "scratch-l10n": "latest",
"slick-carousel": "1.6.0", "slick-carousel": "1.6.0",
"source-map-support": "0.3.2", "source-map-support": "0.3.2",
"style-loader": "0.12.3", "style-loader": "0.12.3",
"tap": "14.2.0", "tap": "14.2.0",
"url-loader": "0.5.6", "url-loader": "2.0.1",
"watch": "0.16.0", "watch": "0.16.0",
"webpack": "2.7.0", "webpack": "2.7.0",
"webpack-dev-middleware": "2.0.4", "webpack-dev-middleware": "2.0.4",

View file

@ -0,0 +1,62 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
import {Field} from 'formik';
const ValidationMessage = require('../forms/validation-message.jsx');
require('../forms/select.scss');
require('../forms/row.scss');
const FormikSelect = ({
className,
error,
options,
validationClassName,
...props
}) => {
const optionsList = options.map((item, index) => (
<option
key={index}
value={item.value}
>
{item.label}
</option>
));
return (
<div className="col-sm-9 row">
<Field
className={classNames(
'select',
className
)}
component="select"
{...props}
>
{optionsList}
</Field>
{error && (
<ValidationMessage
className={validationClassName}
message={error}
/>
)}
</div>
);
};
FormikSelect.propTypes = {
className: PropTypes.string,
error: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
disabled: PropTypes.bool,
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
})).isRequired,
validationClassName: PropTypes.string,
// selected value
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
};
module.exports = FormikSelect;

View file

@ -2,6 +2,10 @@ const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const NextStepButton = require('./next-step-button.jsx'); const NextStepButton = require('./next-step-button.jsx');
const ModalTitle = require('../modal/base/modal-title.jsx');
const ModalInnerContent = require('../modal/base/modal-inner-content.jsx');
require('./join-flow-step.scss');
const JoinFlowStep = ({ const JoinFlowStep = ({
children, children,
@ -12,19 +16,20 @@ const JoinFlowStep = ({
}) => ( }) => (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div> <div>
{title && ( <ModalInnerContent className="join-flow-inner-content">
<h2> {title && (
{title} <ModalTitle
</h2> className="join-flow-title"
)} title={title}
{description && ( />
<p> )}
<span> {description && (
<div className="join-flow-description">
{description} {description}
</span> </div>
</p> )}
)} {children}
{children} </ModalInnerContent>
</div> </div>
<NextStepButton waiting={waiting} /> <NextStepButton waiting={waiting} />
</form> </form>

View file

@ -0,0 +1,23 @@
@import "../../colors";
@import "../../frameless";
.join-flow-title {
color: $type-gray;
font-size: 1.875rem;
}
.join-flow-description {
font-size: .875rem;
font-weight: bold;
margin-bottom: 1.875rem;
text-align: center;
}
.join-flow-inner-content {
box-shadow: none;
width: calc(100% - 5.875rem);
/* must use padding for top, rather than margin, because margins will collapse */
margin: 0 auto;
padding: 2.3125rem 0 2.5rem;
font-size: .875rem;
}

View file

@ -1,5 +1,6 @@
/* eslint-disable react/no-multi-comp */ /* eslint-disable react/no-multi-comp */
const bindAll = require('lodash.bindall'); const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
import {Formik} from 'formik'; import {Formik} from 'formik';
@ -114,57 +115,70 @@ class UsernameStep extends React.Component {
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div> <div>
<div> <div className="join-flow-input-title">
<b> {this.props.intl.formatMessage({id: 'registration.createUsername'})}
{this.props.intl.formatMessage({id: 'registration.createUsername'})}
</b>
</div> </div>
<FormikInput <FormikInput
className={errors.username ? 'fail' : ''} className={classNames(
'join-flow-input',
{fail: errors.username}
)}
error={errors.username} error={errors.username}
id="username" id="username"
name="username" name="username"
validate={this.validateUsernameIfPresent} validate={this.validateUsernameIfPresent}
onBlur={() => validateField('username')} // eslint-disable-line react/jsx-no-bind onBlur={() => validateField('username')} // eslint-disable-line react/jsx-no-bind
/> />
<b> <div className="join-flow-password-section">
{this.props.intl.formatMessage({id: 'general.password'})} <div className="join-flow-input-title">
</b> {this.props.intl.formatMessage({id: 'general.password'})}
<div </div>
onClick={this.handleChangeShowPassword} <FormikInput
> className={classNames(
{/* TODO: should localize 'Hide password' if we use that */} 'join-flow-input',
{this.state.showPassword ? 'Hide password' : ( {fail: errors.password}
this.props.intl.formatMessage({id: 'registration.showPassword'}) )}
)} error={errors.password}
id="password"
name="password"
type={this.state.showPassword ? 'text' : 'password'}
validate={this.validatePasswordIfPresent}
validationClassName="validation-full-width-input"
/* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('password')}
/* eslint-enable react/jsx-no-bind */
/>
<FormikInput
className={classNames(
'join-flow-input',
{fail: errors.passwordConfirm}
)}
error={errors.passwordConfirm}
id="passwordConfirm"
name="passwordConfirm"
type={this.state.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={() =>
this.validatePasswordConfirmIfPresent(values.password,
values.passwordConfirm)
}
validationClassName="validation-full-width-input"
onBlur={() =>
validateField('passwordConfirm')
}
/* eslint-enable react/jsx-no-bind */
/>
<div className="join-flow-input-title">
<div
onClick={this.handleChangeShowPassword}
>
{/* TODO: should localize 'Hide password' if we use that */}
{this.state.showPassword ? 'Hide password' : (
this.props.intl.formatMessage({id: 'registration.showPassword'})
)}
</div>
</div>
</div> </div>
<FormikInput
className={errors.password ? 'fail' : ''}
error={errors.password}
id="password"
name="password"
type={this.state.showPassword ? 'text' : 'password'}
validate={this.validatePasswordIfPresent}
onBlur={() => validateField('password')} // eslint-disable-line react/jsx-no-bind
/>
<b>
{this.props.intl.formatMessage({id: 'general.error'})}
</b>
<FormikInput
className={errors.passwordConfirm ? 'fail' : ''}
error={errors.passwordConfirm}
id="passwordConfirm"
name="passwordConfirm"
type={this.state.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={() =>
this.validatePasswordConfirmIfPresent(values.password, values.passwordConfirm)
}
onBlur={() =>
validateField('passwordConfirm')
}
/* eslint-enable react/jsx-no-bind */
/>
</div> </div>
</JoinFlowStep> </JoinFlowStep>
); );

View file

@ -0,0 +1,27 @@
@import "../../colors";
@import "../../frameless";
.join-flow-input {
width: 100%;
height: 2.75rem;
border-radius: .5rem;
background-color: $ui-white;
margin-bottom: .5rem;
&:focus {
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
}
}
.join-flow-input-title {
font-weight: bold;
margin-bottom: .5rem;
}
.validation-full-width-input {
transform: translate(21.5625rem, 0);
}
.join-flow-password-section {
margin-top: 1.125rem;
}

View file

@ -6,6 +6,7 @@ const React = require('react');
const injectIntl = require('../../lib/intl.jsx').injectIntl; const injectIntl = require('../../lib/intl.jsx').injectIntl;
const intlShape = require('../../lib/intl.jsx').intlShape; const intlShape = require('../../lib/intl.jsx').intlShape;
const Progression = require('../progression/progression.jsx');
const JoinFlowSteps = require('./join-flow-steps.jsx'); const JoinFlowSteps = require('./join-flow-steps.jsx');
/* /*
@ -17,6 +18,11 @@ class JoinFlow extends React.Component {
bindAll(this, [ bindAll(this, [
'handleAdvanceStep' 'handleAdvanceStep'
]); ]);
this.state = {
formData: {},
registrationError: null,
step: 0
};
} }
handleAdvanceStep (formData) { handleAdvanceStep (formData) {
formData = formData || {}; formData = formData || {};
@ -28,7 +34,11 @@ class JoinFlow extends React.Component {
render () { render () {
return ( return (
<React.Fragment> <React.Fragment>
<JoinFlowSteps.UsernameStep /> <Progression step={this.state.step}>
<JoinFlowSteps.UsernameStep
onNextStep={this.handleAdvanceStep}
/>
</Progression>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -5,19 +5,23 @@ const injectIntl = require('react-intl').injectIntl;
const intl = require('../../lib/intl.jsx'); const intl = require('../../lib/intl.jsx');
const Spinner = require('../../components/spinner/spinner.jsx'); const Spinner = require('../../components/spinner/spinner.jsx');
const ModalTitle = require('../modal/base/modal-title.jsx');
require('./next-step-button.scss');
const NextStepButton = props => ( const NextStepButton = props => (
<button <button
className="modal-flush-bottom-button"
disabled={props.waiting} disabled={props.waiting}
type="submit" type="submit"
{...omit(props, ['text', 'waiting'])} {...omit(props, ['intl', 'text', 'waiting'])}
> >
{props.waiting ? {props.waiting ?
<Spinner /> : <Spinner /> : (
(props.text ? <ModalTitle
props.text : ( className="next-step-title-large"
<intl.FormattedMessage id="registration.nextStep" /> title={props.text ? props.text : props.intl.formatMessage({id: 'registration.nextStep'})}
) />
) )
} }
</button> </button>

View file

@ -0,0 +1,15 @@
@import "../../colors";
@import "../../frameless";
.modal-flush-bottom-button {
margin: 0;
width: 100%;
border-bottom-left-radius: 1rem;
border-bottom-right-radius: 1rem;
height: 5.1875rem;
background-color: $ui-orange;
}
.next-step-title-large {
font-size: 1.25rem;
}

View file

@ -11,6 +11,7 @@ const Spinner = require('../../spinner/spinner.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx'); const FlexRow = require('../../flex-row/flex-row.jsx');
const StudioButton = require('./studio-button.jsx'); const StudioButton = require('./studio-button.jsx');
const ModalTitle = require('../base/modal-title.jsx'); const ModalTitle = require('../base/modal-title.jsx');
const ModalInnerContent = require('../base/modal-inner-content.jsx');
require('../../forms/button.scss'); require('../../forms/button.scss');
require('./modal.scss'); require('./modal.scss');
@ -49,7 +50,7 @@ const AddToStudioModalPresentation = ({
<div className="addToStudio-modal-header modal-header"> <div className="addToStudio-modal-header modal-header">
<ModalTitle title={contentLabel} /> <ModalTitle title={contentLabel} />
</div> </div>
<div className="addToStudio-modal-content modal-content"> <ModalInnerContent className="addToStudio-modal-content">
<div className="studio-list-outer-scrollbox"> <div className="studio-list-outer-scrollbox">
<div className="studio-list-inner-scrollbox"> <div className="studio-list-inner-scrollbox">
<div className="studio-list-container"> <div className="studio-list-container">
@ -101,7 +102,7 @@ const AddToStudioModalPresentation = ({
]} ]}
</FlexRow> </FlexRow>
</Form> </Form>
</div> </ModalInnerContent>
</Modal> </Modal>
); );
}; };

View file

@ -0,0 +1,26 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
require('./modal-inner-content.scss');
const ModalInnerContent = ({
children,
className
}) => (
<div
className={classNames(
'modal-inner-content',
className
)}
>
{children}
</div>
);
ModalInnerContent.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};
module.exports = ModalInnerContent;

View file

@ -0,0 +1,9 @@
@import "../../../colors";
@import "../../../frameless";
.modal-inner-content {
box-sizing: border-box;
display: flex;
border-radius: 0;
flex-direction: column;
}

View file

@ -16,12 +16,6 @@
padding: 0; padding: 0;
width: 48.75rem; width: 48.75rem;
.modal-content { /* content inside of content */
display: flex;
border-radius: 0;
flex-direction: column;
}
&:focus { &:focus {
outline: none; outline: none;
} }

View file

@ -2,4 +2,5 @@
@import "../../../frameless"; @import "../../../frameless";
.mod-join { .mod-join {
width: 27.4375rem;
} }

View file

@ -9,6 +9,7 @@ const Modal = require('../base/modal.jsx');
const classNames = require('classnames'); const classNames = require('classnames');
const ModalTitle = require('../base/modal-title.jsx'); const ModalTitle = require('../base/modal-title.jsx');
const ModalInnerContent = require('../base/modal-inner-content.jsx');
const Form = require('../../forms/form.jsx'); const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx'); const Button = require('../../forms/button.jsx');
const Select = require('../../forms/select.jsx'); const Select = require('../../forms/select.jsx');
@ -130,7 +131,7 @@ class ReportModal extends React.Component {
onValid={this.handleValid} onValid={this.handleValid}
onValidSubmit={onReport} onValidSubmit={onReport}
> >
<div className="report-modal-content modal-content"> <ModalInnerContent className="report-modal-content">
{isConfirmed ? ( {isConfirmed ? (
<div className="received"> <div className="received">
<div className="received-header"> <div className="received-header">
@ -198,7 +199,7 @@ class ReportModal extends React.Component {
<FormattedMessage id="report.error" /> <FormattedMessage id="report.error" />
</div> </div>
)} )}
</div> </ModalInnerContent>
<FlexRow className="action-buttons"> <FlexRow className="action-buttons">
<div className="action-buttons-overflow-fix"> <div className="action-buttons-overflow-fix">
{isConfirmed ? ( {isConfirmed ? (

View file

@ -6,6 +6,8 @@ const classNames = require('classnames');
const Modal = require('../base/modal.jsx'); const Modal = require('../base/modal.jsx');
const ModalTitle = require('../base/modal-title.jsx'); const ModalTitle = require('../base/modal-title.jsx');
const ModalInnerContent = require('../base/modal-inner-content.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx'); const FlexRow = require('../../flex-row/flex-row.jsx');
require('../../forms/button.scss'); require('../../forms/button.scss');
@ -37,7 +39,7 @@ const SocialModalPresentation = ({
<div className="social-modal-header modal-header"> <div className="social-modal-header modal-header">
<ModalTitle title={intl.formatMessage({id: 'general.copyLink'})} /> <ModalTitle title={intl.formatMessage({id: 'general.copyLink'})} />
</div> </div>
<div className="modal-content social-modal-content"> <ModalInnerContent className="social-modal-content">
{/* top row: link */} {/* top row: link */}
<div className="link-section"> <div className="link-section">
@ -111,7 +113,7 @@ const SocialModalPresentation = ({
</FlexRow> </FlexRow>
</div> </div>
</div> </ModalInnerContent>
</Modal> </Modal>
); );
}; };

View file

@ -1,6 +1,6 @@
const defaults = require('lodash.defaults'); const defaults = require('lodash.defaults');
const keyMirror = require('keymirror'); const keyMirror = require('keymirror');
const async = require('async'); const eachLimit = require('async/eachLimit');
const mergeWith = require('lodash.mergewith'); const mergeWith = require('lodash.mergewith');
const uniqBy = require('lodash.uniqby'); const uniqBy = require('lodash.uniqby');
@ -524,7 +524,7 @@ module.exports.getCommentById = (projectId, commentId, ownerUsername, isAdmin, t
module.exports.getReplies = (projectId, commentIds, offset, ownerUsername, isAdmin, token) => (dispatch => { module.exports.getReplies = (projectId, commentIds, offset, ownerUsername, isAdmin, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHING)); dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHING));
const fetchedReplies = {}; const fetchedReplies = {};
async.eachLimit(commentIds, 10, (parentId, callback) => { eachLimit(commentIds, 10, (parentId, callback) => {
api({ api({
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${parentId}/replies`, uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${parentId}/replies`,
authentication: token ? token : null, authentication: token ? token : null,

View file

@ -12,10 +12,10 @@
* Tests can be run using Saucelabs, an online service that can test browser/os combinations remotely. Currently all tests are written for use for chrome on mac. * Tests can be run using Saucelabs, an online service that can test browser/os combinations remotely. Currently all tests are written for use for chrome on mac.
## Using tap ## Using tap
* Run all tests in the smoke-testing directory from the command-line: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu npm run smoke` * Run all tests in the smoke-testing directory from the command-line: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu npm run test:smoke`
* To run a single file from the command-line: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu node_modules/.bin/tap ./test/integration/smoke-testing/filename.js --timeout=3600` * To run a single file from the command-line: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu node_modules/.bin/tap ./test/integration/smoke-testing/filename.js --timeout=3600`
* The timeout var is for the length of the entire tap test-suite; if you are getting a timeout error, you may need to adjust this value (some of the Selenium tests take a while to run) * The timeout var is for the length of the entire tap test-suite; if you are getting a timeout error, you may need to adjust this value (some of the Selenium tests take a while to run)
* To run tests using saucelabs run this command `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password SAUCE_USERNAME=saucelabsUsername SAUCE_ACCESS_KEY=saucelabsAccessKey ROOT_URL=https://scratch.mit.edu npm run smoke-sauce` * To run tests using saucelabs run this command `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password SAUCE_USERNAME=saucelabsUsername SAUCE_ACCESS_KEY=saucelabsAccessKey ROOT_URL=https://scratch.mit.edu npm run test:smoke:sauce`
### Configuration ### Configuration
@ -25,7 +25,7 @@
| `ROOT_URL` | `scratch.ly` | Location you want to run the tests against | | `ROOT_URL` | `scratch.ly` | Location you want to run the tests against |
| `SMOKE_USERNAME` | `None` | Username for Scratch user you're signing in with to test | | `SMOKE_USERNAME` | `None` | Username for Scratch user you're signing in with to test |
| `SMOKE_PASSWORD` | `None` | Password for Scratch user you're signing in with to test | | `SMOKE_PASSWORD` | `None` | Password for Scratch user you're signing in with to test |
| `SMOKE_REMOTE` | `false` | Tests with Sauce Labs or not. True if running smoke-sauce | | `SMOKE_REMOTE` | `false` | Tests with Sauce Labs or not. True if running test:smoke:sauce |
| `SMOKE_HEADLESS` | `false` | Run browser in headless mode. Flaky at the moment | | `SMOKE_HEADLESS` | `false` | Run browser in headless mode. Flaky at the moment |
| `SAUCE_USERNAME` | `None` | Username for your Sauce Labs account | | `SAUCE_USERNAME` | `None` | Username for your Sauce Labs account |
| `SAUCE_ACCESS_KEY` | `None` | Access Key for Sauce Labs found under User Settings | | `SAUCE_ACCESS_KEY` | `None` | Access Key for Sauce Labs found under User Settings |

View file

@ -81,7 +81,8 @@ module.exports = {
include: [ include: [
path.resolve(__dirname, 'src'), path.resolve(__dirname, 'src'),
/node_modules[\\/]scratch-[^\\/]+[\\/]src/, /node_modules[\\/]scratch-[^\\/]+[\\/]src/,
/node_modules[\\/]pify/ /node_modules[\\/]pify/,
/node_modules[\\/]async/
] ]
}, },
{ {