Merge pull request #5262 from LLK/release/2021-04-15

[Master] Release 2021-04-15
This commit is contained in:
picklesrus 2021-04-20 10:58:33 -04:00 committed by GitHub
commit 2367d8e143
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1506 additions and 809 deletions

View file

@ -81,7 +81,7 @@ aliases:
- run:
name: "setup python"
command: |
curl https://bootstrap.pypa.io/3.5/get-pip.py -o get-pip.py
curl https://bootstrap.pypa.io/pip/3.5/get-pip.py -o get-pip.py
python3 get-pip.py pip==21.0.1
pip install s3cmd==2.1.0
- run:
@ -134,7 +134,7 @@ jobs:
# <<: *integration_tap
workflows:
build-staging-production: # build-test-deploy
build-test-deploy:
jobs:
- build-staging:
context:
@ -154,42 +154,39 @@ workflows:
branches:
only:
- master
# - deploy-staging:
# context:
# - scratch-www-all
# - scratch-www-staging
# requires:
# - build-staging
# filters:
# branches:
# only:
# - develop
# - /^hotfix\/.*/
# - /^release\/.*/
# - circleCI-configure-tests
# - integration-staging-jest:
# context:
# - scratch-www-all
# - scratch-www-staging
# requires:
# - deploy-staging
# filters:
# branches:
# only:
# - develop
# - /^hotfix\/.*/
# - /^release\/.*/
# - circleCI-configure-tests
# - integration-staging-tap:
# context:
# - scratch-www-all
# - scratch-www-staging
# requires:
# - deploy-staging
# filters:
# branches:
# only:
# - develop
# - /^hotfix\/.*/
# - /^release\/.*/
# - circleCI-configure-tests
- deploy-staging:
context:
- scratch-www-all
- scratch-www-staging
requires:
- build-staging
filters:
branches:
only:
- develop
- /^hotfix\/.*/
- /^release\/.*/
- integration-staging-jest:
context:
- scratch-www-all
- scratch-www-staging
requires:
- deploy-staging
filters:
branches:
only:
- develop
- /^hotfix\/.*/
- /^release\/.*/
- integration-staging-tap:
context:
- scratch-www-all
- scratch-www-staging
requires:
- deploy-staging
filters:
branches:
only:
- develop
- /^hotfix\/.*/
- /^release\/.*/

View file

@ -114,15 +114,6 @@ jobs:
include:
- stage: test
deploy:
- provider: script
skip_cleanup: $SKIP_CLEANUP
script: npm run deploy
on:
repo: LLK/scratch-www
branch:
- develop
- hotfix/*
- release/*
- provider: script
skip_cleanup: $SKIP_CLEANUP
script: npm run deploy
@ -138,6 +129,6 @@ stages:
- name: test
if: type != cron
- name: smoke
if: type NOT IN (cron, pull_request) AND (branch =~ /^(develop|master|release\/|hotfix\/)/)
if: type NOT IN (cron, pull_request) AND (branch =~ /^(master)/)
- name: update translations
if: branch == develop AND type == cron

421
package-lock.json generated
View file

@ -16,9 +16,9 @@
}
},
"@babel/cli": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.13.10.tgz",
"integrity": "sha512-lYSBC7B4B9hJ7sv0Ojx1BrGhuzCoOIYfLjd+Xpd4rOzdS+a47yi8voV8vFkfjlZR1N5qZO7ixOCbobUdT304PQ==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.13.14.tgz",
"integrity": "sha512-zmEFV8WBRsW+mPQumO1/4b34QNALBVReaiHJOkxhUsdo/AvYM62c+SKSuLi2aZ42t3ocK6OI0uwUXRvrIbREZw==",
"dev": true,
"requires": {
"@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents",
@ -34,9 +34,9 @@
},
"dependencies": {
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"optional": true,
"requires": {
@ -226,31 +226,30 @@
}
},
"@babel/compat-data": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.12.tgz",
"integrity": "sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.15.tgz",
"integrity": "sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==",
"dev": true
},
"@babel/core": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.10.tgz",
"integrity": "sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.15.tgz",
"integrity": "sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.13.9",
"@babel/helper-compilation-targets": "^7.13.10",
"@babel/helper-module-transforms": "^7.13.0",
"@babel/helper-compilation-targets": "^7.13.13",
"@babel/helper-module-transforms": "^7.13.14",
"@babel/helpers": "^7.13.10",
"@babel/parser": "^7.13.10",
"@babel/parser": "^7.13.15",
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.13.0",
"@babel/types": "^7.13.0",
"@babel/traverse": "^7.13.15",
"@babel/types": "^7.13.14",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.1.2",
"lodash": "^4.17.19",
"semver": "^6.3.0",
"source-map": "^0.5.0"
},
@ -316,9 +315,9 @@
}
},
"@babel/parser": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.12.tgz",
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz",
"integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==",
"dev": true
},
"@babel/template": {
@ -333,26 +332,25 @@
}
},
"@babel/traverse": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.0.tgz",
"integrity": "sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz",
"integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.13.0",
"@babel/generator": "^7.13.9",
"@babel/helper-function-name": "^7.12.13",
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.13.0",
"@babel/types": "^7.13.0",
"@babel/parser": "^7.13.15",
"@babel/types": "^7.13.14",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.19"
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz",
"integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -494,34 +492,34 @@
}
},
"@babel/helper-compilation-targets": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz",
"integrity": "sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA==",
"version": "7.13.13",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz",
"integrity": "sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==",
"dev": true,
"requires": {
"@babel/compat-data": "^7.13.8",
"@babel/compat-data": "^7.13.12",
"@babel/helper-validator-option": "^7.12.17",
"browserslist": "^4.14.5",
"semver": "^6.3.0"
},
"dependencies": {
"browserslist": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz",
"integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==",
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.4.tgz",
"integrity": "sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001181",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.649",
"caniuse-lite": "^1.0.30001208",
"colorette": "^1.2.2",
"electron-to-chromium": "^1.3.712",
"escalade": "^3.1.1",
"node-releases": "^1.1.70"
"node-releases": "^1.1.71"
}
},
"electron-to-chromium": {
"version": "1.3.697",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.697.tgz",
"integrity": "sha512-VTAS+IWwGlfaL7VtfUMzFeV55PT/HglNFqQ6eW9E3PfjvPqhZfqJj+8dd9zrqrJYcouUfCgQw0OIse85Dz9V9Q==",
"version": "1.3.717",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz",
"integrity": "sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==",
"dev": true
},
"semver": {
@ -562,9 +560,9 @@
},
"dependencies": {
"@babel/types": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz",
"integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -596,9 +594,9 @@
},
"dependencies": {
"@babel/types": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz",
"integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -621,9 +619,9 @@
}
},
"@babel/helper-module-transforms": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.12.tgz",
"integrity": "sha512-7zVQqMO3V+K4JOOj40kxiCrMf6xlQAkewBB0eu2b03OO/Q21ZutOzjpfD79A5gtE/2OWi1nv625MrDlGlkbknQ==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz",
"integrity": "sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.13.12",
@ -632,8 +630,8 @@
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/helper-validator-identifier": "^7.12.11",
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.13.0",
"@babel/types": "^7.13.12"
"@babel/traverse": "^7.13.13",
"@babel/types": "^7.13.14"
},
"dependencies": {
"@babel/code-frame": {
@ -697,9 +695,9 @@
}
},
"@babel/parser": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.12.tgz",
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz",
"integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==",
"dev": true
},
"@babel/template": {
@ -714,26 +712,25 @@
}
},
"@babel/traverse": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.0.tgz",
"integrity": "sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz",
"integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.13.0",
"@babel/generator": "^7.13.9",
"@babel/helper-function-name": "^7.12.13",
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.13.0",
"@babel/types": "^7.13.0",
"@babel/parser": "^7.13.15",
"@babel/types": "^7.13.14",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.19"
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz",
"integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -827,9 +824,9 @@
},
"dependencies": {
"@babel/types": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz",
"integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -930,9 +927,9 @@
}
},
"@babel/parser": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.12.tgz",
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz",
"integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==",
"dev": true
},
"@babel/template": {
@ -947,26 +944,25 @@
}
},
"@babel/traverse": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.0.tgz",
"integrity": "sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz",
"integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.13.0",
"@babel/generator": "^7.13.9",
"@babel/helper-function-name": "^7.12.13",
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.13.0",
"@babel/types": "^7.13.0",
"@babel/parser": "^7.13.15",
"@babel/types": "^7.13.14",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.19"
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz",
"integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -1060,9 +1056,9 @@
},
"dependencies": {
"@babel/types": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz",
"integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -1177,9 +1173,9 @@
}
},
"@babel/parser": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.12.tgz",
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz",
"integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==",
"dev": true
},
"@babel/template": {
@ -1194,26 +1190,25 @@
}
},
"@babel/traverse": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.0.tgz",
"integrity": "sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==",
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz",
"integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.13.0",
"@babel/generator": "^7.13.9",
"@babel/helper-function-name": "^7.12.13",
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.13.0",
"@babel/types": "^7.13.0",
"@babel/parser": "^7.13.15",
"@babel/types": "^7.13.14",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.19"
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"version": "7.13.14",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz",
"integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -1437,26 +1432,26 @@
}
},
"@formatjs/ecma402-abstract": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.6.3.tgz",
"integrity": "sha512-7ijswObmYXabVy5GvcpKG29jbyJ9rGtFdRBdmdQvoDmMo0PwlOl/L08GtrjA4YWLAZ0j2owb2YrRLGNAvLBk+Q==",
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.6.5.tgz",
"integrity": "sha512-dhRWSoPPw8PhB5tSOEP9Gi5XZNFC2IkfP95Va70ouIuED0wBlsU1WmO4jDHITL7/kSNqvzKFTT+2S+6jHPq6jw==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
},
"@formatjs/intl-getcanonicallocales": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.7.tgz",
"integrity": "sha512-raPV3Dw7CBC9kPvKdgxkVGgwzYBsQDDG9qXGWblpj/zR+ZJ6Q2V+Co5jZhrviy6lq3qaM2T1Itc0ibvvil1tBw==",
"version": "1.5.9",
"resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.9.tgz",
"integrity": "sha512-bO0J3IaamFM3rU7noXo1bWSmPA8xuAL8NPk+k5Dy08ehiu/STT3sN+6DGLEvRCpb465CpjUWGCNknDFIcdu9hA==",
"dev": true,
"requires": {
"cldr-core": "38",
@ -1464,65 +1459,65 @@
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
},
"@formatjs/intl-locale": {
"version": "2.4.20",
"resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.20.tgz",
"integrity": "sha512-ZrVFxKab+W6jFP6WEYsNW0b7IlGYnCS20fdLN6u0LwPCPYRP5oqHBl0FFVD2+aNnQ1T/21Aol54fCr5LdN/49Q==",
"version": "2.4.22",
"resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.22.tgz",
"integrity": "sha512-/GECNZDORaz1sHsnhUZHTCmoPRux5w1D2dX1vHb2QY/f3YENsA4U/WqmIBHk+77RU/KTFbTQgxGcfejneAOPoA==",
"dev": true,
"requires": {
"@formatjs/ecma402-abstract": "1.6.3",
"@formatjs/intl-getcanonicallocales": "1.5.7",
"@formatjs/ecma402-abstract": "1.6.5",
"@formatjs/intl-getcanonicallocales": "1.5.9",
"cldr-core": "38",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
},
"@formatjs/intl-pluralrules": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.12.tgz",
"integrity": "sha512-jXXsWGQbBMvuhvxuG1AXBMMNMS1ZphSt/rWsGo6bE3KyWmddJnnVokeUD8E2sTtXoCJZoGUQkOxxjFa/gGLyxw==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.14.tgz",
"integrity": "sha512-smfNDGaVZwFqKKqQlJfuKO45+RJs+UqyMD/8OJpIRpnYSss29T6T4jM5jzI9ZcbuTc79axI4no8wyViLBuGFMQ==",
"dev": true,
"requires": {
"@formatjs/ecma402-abstract": "1.6.3",
"@formatjs/ecma402-abstract": "1.6.5",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
},
"@formatjs/intl-relativetimeformat": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.1.3.tgz",
"integrity": "sha512-uUbtr4NRKgHK66bxO98RQlXypfRA5cto6nsFpgwe5wf1SWLBqxcNoo+zdAIftdvHnxPC4QH6oykMf2aYLm+pbA==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.1.5.tgz",
"integrity": "sha512-vCZ54nn2sPfIie7KQwbwT2xjRurvdGfJ036wPQFnOpICdqe8/vFKaS/kYRSw42fREGh1sefVcH0DZAz/GZczyw==",
"dev": true,
"requires": {
"@formatjs/ecma402-abstract": "1.6.3",
"@formatjs/ecma402-abstract": "1.6.5",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
@ -2041,9 +2036,9 @@
}
},
"@polka/url": {
"version": "1.0.0-next.11",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.11.tgz",
"integrity": "sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==",
"version": "1.0.0-next.12",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
"integrity": "sha512-6RglhutqrGFMO1MNUXp95RBuYIuc8wTnMAV5MUhLmjTOy78ncwOw7RgeQ/HeymkKXRhZd0s2DNrM1rL7unk3MQ==",
"dev": true
},
"@scratch/paper": {
@ -4700,12 +4695,6 @@
"requires": {
"glob": "^7.1.3"
}
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
}
}
},
@ -4860,9 +4849,9 @@
"dev": true
},
"caniuse-lite": {
"version": "1.0.30001191",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz",
"integrity": "sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==",
"version": "1.0.30001208",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz",
"integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==",
"dev": true
},
"canvas-fit": {
@ -5350,9 +5339,9 @@
"dev": true
},
"colorette": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
"colormap": {
@ -12665,6 +12654,12 @@
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
"y18n": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
"integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==",
"dev": true
},
"yargs": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
@ -13411,6 +13406,12 @@
"signal-exit": "^3.0.2"
}
},
"y18n": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
"integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==",
"dev": true
},
"yargs": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
@ -14271,6 +14272,11 @@
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
"dev": true
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@ -15809,9 +15815,9 @@
}
},
"node-releases": {
"version": "1.1.70",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz",
"integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==",
"version": "1.1.71",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz",
"integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==",
"dev": true
},
"node-sass": {
@ -16247,12 +16253,6 @@
"strip-ansi": "^5.0.0"
}
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
@ -20797,12 +20797,6 @@
"strip-ansi": "^5.0.0"
}
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
@ -21019,16 +21013,16 @@
"dev": true
},
"browserslist": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz",
"integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==",
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.4.tgz",
"integrity": "sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001181",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.649",
"caniuse-lite": "^1.0.30001208",
"colorette": "^1.2.2",
"electron-to-chromium": "^1.3.712",
"escalade": "^3.1.1",
"node-releases": "^1.1.70"
"node-releases": "^1.1.71"
}
},
"chalk": {
@ -21142,9 +21136,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.698",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.698.tgz",
"integrity": "sha512-VEXDzYblnlT+g8Q3gedwzgKOso1evkeJzV8lih7lV8mL8eAnGVnKyC3KsFT6S+R5PQO4ffdr1PI16/ElibY/kQ==",
"version": "1.3.717",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz",
"integrity": "sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==",
"dev": true
},
"has-flag": {
@ -21364,6 +21358,19 @@
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
"dev": true
},
"scratch-l10n": {
"version": "3.11.20210324031512",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210324031512.tgz",
"integrity": "sha512-dyv2cjNWVHrL78XpW64uF2azYaUhMKLjfJVH6vSCPJxm3EKCvO6EoaUSlIAwhsGoMwgfxWZ8D74+IaifGJfnCQ==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2",
"babel-plugin-react-intl": "^3.0.1",
"react-intl": "^2.8.0",
"transifex": "1.6.6"
}
},
"scratch-storage": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.3.tgz",
@ -21431,9 +21438,9 @@
}
},
"scratch-l10n": {
"version": "3.11.20210324031512",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210324031512.tgz",
"integrity": "sha512-dyv2cjNWVHrL78XpW64uF2azYaUhMKLjfJVH6vSCPJxm3EKCvO6EoaUSlIAwhsGoMwgfxWZ8D74+IaifGJfnCQ==",
"version": "3.11.20210413031556",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210413031556.tgz",
"integrity": "sha512-mmD0wvRCgkVgx7scN14CQ6NPDqaL0uT//ZFQWpp5foTBuXokXPh9jRrJ+KScCeXK7jaXMLeJa6r4q1ivpnVXGQ==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
@ -21457,7 +21464,7 @@
"minilog": "3.1.0",
"parse-color": "1.0.0",
"prop-types": "^15.5.10",
"scratch-render-fonts": "^1.0.0-prerelease.20200507182347"
"scratch-render-fonts": "^1.0.0-prerelease.20210401210003"
},
"dependencies": {
"lodash.omit": {
@ -21482,9 +21489,9 @@
}
},
"scratch-render-fonts": {
"version": "1.0.0-prerelease.20200507182347",
"resolved": "https://registry.npmjs.org/scratch-render-fonts/-/scratch-render-fonts-1.0.0-prerelease.20200507182347.tgz",
"integrity": "sha512-tVt2s7lxsBhme9WKIZTnluMerdJVGEc80QSrDdIIzUvHXGCIYkLh6j7ytwXcyq2UsA34d93op9+I9Bh1SPkQkA==",
"version": "1.0.0-prerelease.20210401210003",
"resolved": "https://registry.npmjs.org/scratch-render-fonts/-/scratch-render-fonts-1.0.0-prerelease.20210401210003.tgz",
"integrity": "sha512-sgU+LIXTLKk4f7FZOv/B61dpvmfpnlXFf912T6T4GpOfzx99JPRhXPyErZWuwPz8NEzthkhpO7iF2AqgzUxJfA==",
"dev": true,
"requires": {
"base64-loader": "1.0.0"
@ -25223,12 +25230,6 @@
"xtend": "~4.0.1"
}
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@ -27478,9 +27479,9 @@
}
},
"webpack-bundle-analyzer": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz",
"integrity": "sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz",
"integrity": "sha512-j5m7WgytCkiVBoOGavzNokBOqxe6Mma13X1asfVYtKWM3wxBiRRu1u1iG0Iol5+qp9WgyhkMmBAcvjEfJ2bdDw==",
"dev": true,
"requires": {
"acorn": "^8.0.4",
@ -27495,9 +27496,9 @@
},
"dependencies": {
"acorn": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz",
"integrity": "sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==",
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.1.tgz",
"integrity": "sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g==",
"dev": true
},
"acorn-walk": {
@ -27558,12 +27559,6 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -27574,9 +27569,9 @@
}
},
"ws": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
"integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"dev": true
}
}
@ -27785,12 +27780,6 @@
"strip-ansi": "^5.0.0"
}
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
@ -28159,9 +28148,9 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yallist": {

View file

@ -19,9 +19,9 @@
"test:unit:jest": "npm run test:unit:jest:unit && npm run test:unit:jest:localization",
"test:unit:jest:unit": "jest ./test/unit/ --reporters=default",
"test:unit:jest:localization": "jest ./test/localization/*.test.js --reporters=default",
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/*.js --no-coverage -R classic",
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/ --no-coverage -R classic",
"test:unit:convertReportToXunit": "tap ./test/results/unit-raw.tap --no-coverage -R xunit > ./test/results/unit-tap-results.xml",
"test:coverage": "tap ./test/{unit-legacy,localization-legacy}/*.js --coverage --coverage-report=lcov",
"test:coverage": "tap ./test/{unit-legacy,localization-legacy}/ --coverage --coverage-report=lcov",
"build": "npm run clean && npm run translate && NODE_OPTIONS=--max_old_space_size=8000 webpack --bail",
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
"deploy": "npm run deploy:s3 && npm run deploy:fastly",
@ -53,6 +53,7 @@
"express": "4.16.1",
"express-http-proxy": "1.1.0",
"lodash.defaults": "4.0.1",
"lodash.get": "^4.4.2",
"react-helmet": "5.2.0",
"react-router-dom": "^5.2.0",
"scratch-parser": "^5.0.0",

View file

@ -2,129 +2,145 @@
{
"id": 1,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 2,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 3,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 4,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 5,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 6,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 7,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 8,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 9,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 10,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 11,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 12,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 13,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 14,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 15,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
},
{
"id": 16,
"type": "project",
"title": "Project",
"title": "Project Title",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": {"username": "project creator"},
"href": "#",
"stats": {}
}
]

191
src/redux/comments.js Normal file
View file

@ -0,0 +1,191 @@
const keyMirror = require('keymirror');
const mergeWith = require('lodash.mergewith');
const uniqBy = require('lodash.uniqby');
const COMMENT_LIMIT = 20;
module.exports.Status = keyMirror({
FETCHED: null,
NOT_FETCHED: null,
FETCHING: null,
ERROR: null
});
module.exports.getInitialState = () => ({
status: {
comments: module.exports.Status.NOT_FETCHED
},
comments: [],
replies: {},
moreCommentsToLoad: false
});
module.exports.commentsReducer = (state, action) => {
if (typeof state === 'undefined') {
state = module.exports.getInitialState();
}
switch (action.type) {
case 'RESET_TO_INTIAL_STATE':
return module.exports.getInitialState();
case 'RESET_COMMENTS':
return Object.assign({}, state, {
comments: [],
replies: {}
});
case 'SET_COMMENT_FETCH_STATUS':
return Object.assign({}, state, {
status: Object.assign({}, state.status, {
[action.infoType]: action.status
})
});
case 'SET_COMMENTS':
return Object.assign({}, state, {
comments: uniqBy(state.comments.concat(action.items), 'id')
});
case 'UPDATE_COMMENT':
if (action.topLevelCommentId) {
return Object.assign({}, state, {
replies: Object.assign({}, state.replies, {
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].map(comment => {
if (comment.id === action.commentId) {
return Object.assign({}, comment, action.comment);
}
return comment;
})
})
});
}
return Object.assign({}, state, {
comments: state.comments.map(comment => {
if (comment.id === action.commentId) {
return Object.assign({}, comment, action.comment);
}
return comment;
})
});
case 'ADD_NEW_COMMENT':
if (action.topLevelCommentId) {
return Object.assign({}, state, {
replies: Object.assign({}, state.replies, {
// Replies to comments go at the end of the thread
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].concat(action.comment)
})
});
}
// Reply to the top level project, put the reply at the beginning
return Object.assign({}, state, {
comments: [action.comment, ...state.comments],
replies: Object.assign({}, state.replies, {[action.comment.id]: []})
});
case 'UPDATE_ALL_REPLIES':
return Object.assign({}, state, {
replies: Object.assign({}, state.replies, {
[action.commentId]: state.replies[action.commentId].map(reply =>
Object.assign({}, reply, action.comment)
)
})
});
case 'SET_REPLIES':
return Object.assign({}, state, {
// Append new replies to the state.replies structure
replies: mergeWith({}, state.replies, action.replies, (replies, newReplies) => (
uniqBy((replies || []).concat(newReplies || []), 'id')
)),
// Also set the `moreRepliesToLoad` property on the top-level comments
comments: state.comments.map(comment => {
if (action.replies[comment.id]) {
return Object.assign({}, comment, {
moreRepliesToLoad: action.replies[comment.id].length === COMMENT_LIMIT
});
}
return comment;
})
});
case 'SET_MORE_COMMENTS_TO_LOAD':
return Object.assign({}, state, {
moreCommentsToLoad: action.moreCommentsToLoad
});
default:
return state;
}
};
module.exports.setFetchStatus = (type, status) => ({
type: 'SET_COMMENT_FETCH_STATUS',
infoType: type,
status: status
});
module.exports.setComments = items => ({
type: 'SET_COMMENTS',
items: items
});
module.exports.setReplies = replies => ({
type: 'SET_REPLIES',
replies: replies
});
module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({
type: 'UPDATE_COMMENT',
commentId: commentId,
topLevelCommentId: topLevelCommentId,
comment: {
visibility: 'deleted'
}
});
module.exports.setRepliesDeleted = commentId => ({
type: 'UPDATE_ALL_REPLIES',
commentId: commentId,
comment: {
visibility: 'deleted'
}
});
module.exports.setCommentReported = (commentId, topLevelCommentId) => ({
type: 'UPDATE_COMMENT',
commentId: commentId,
topLevelCommentId: topLevelCommentId,
comment: {
visibility: 'reported'
}
});
module.exports.setCommentRestored = (commentId, topLevelCommentId) => ({
type: 'UPDATE_COMMENT',
commentId: commentId,
topLevelCommentId: topLevelCommentId,
comment: {
visibility: 'visible'
}
});
module.exports.setRepliesRestored = commentId => ({
type: 'UPDATE_ALL_REPLIES',
commentId: commentId,
comment: {
visibility: 'visible'
}
});
module.exports.addNewComment = (comment, topLevelCommentId) => ({
type: 'ADD_NEW_COMMENT',
comment: comment,
topLevelCommentId: topLevelCommentId
});
module.exports.setMoreCommentsToLoad = moreCommentsToLoad => ({
type: 'SET_MORE_COMMENTS_TO_LOAD',
moreCommentsToLoad: moreCommentsToLoad
});
module.exports.resetComments = () => ({
type: 'RESET_COMMENTS'
});
// Selectors
module.exports.selectCommentCount = state => state.comments.comments.length;

View file

@ -1,14 +1,9 @@
const defaults = require('lodash.defaults');
const keyMirror = require('keymirror');
const eachLimit = require('async/eachLimit');
const mergeWith = require('lodash.mergewith');
const uniqBy = require('lodash.uniqby');
const api = require('../lib/api');
const log = require('../lib/log');
const COMMENT_LIMIT = 20;
module.exports.Status = keyMirror({
FETCHED: null,
NOT_FETCHED: null,
@ -19,7 +14,6 @@ module.exports.Status = keyMirror({
module.exports.getInitialState = () => ({
status: {
project: module.exports.Status.NOT_FETCHED,
comments: module.exports.Status.NOT_FETCHED,
faved: module.exports.Status.NOT_FETCHED,
loved: module.exports.Status.NOT_FETCHED,
original: module.exports.Status.NOT_FETCHED,
@ -33,8 +27,6 @@ module.exports.getInitialState = () => ({
},
projectInfo: {},
remixes: [],
comments: [],
replies: {},
faved: false,
loved: false,
original: {},
@ -42,7 +34,6 @@ module.exports.getInitialState = () => ({
projectStudios: [],
curatedStudios: [],
currentStudioIds: [],
moreCommentsToLoad: false,
projectNotAvailable: false,
visibilityInfo: {}
});
@ -96,76 +87,6 @@ module.exports.previewReducer = (state, action) => {
item !== action.studioId
))
});
case 'RESET_COMMENTS':
return Object.assign({}, state, {
comments: [],
replies: {}
});
case 'SET_COMMENTS':
return Object.assign({}, state, {
comments: uniqBy(state.comments.concat(action.items), 'id')
});
case 'UPDATE_COMMENT':
if (action.topLevelCommentId) {
return Object.assign({}, state, {
replies: Object.assign({}, state.replies, {
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].map(comment => {
if (comment.id === action.commentId) {
return Object.assign({}, comment, action.comment);
}
return comment;
})
})
});
}
return Object.assign({}, state, {
comments: state.comments.map(comment => {
if (comment.id === action.commentId) {
return Object.assign({}, comment, action.comment);
}
return comment;
})
});
case 'ADD_NEW_COMMENT':
if (action.topLevelCommentId) {
return Object.assign({}, state, {
replies: Object.assign({}, state.replies, {
// Replies to comments go at the end of the thread
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].concat(action.comment)
})
});
}
// Reply to the top level project, put the reply at the beginning
return Object.assign({}, state, {
comments: [action.comment, ...state.comments],
replies: Object.assign({}, state.replies, {[action.comment.id]: []})
});
case 'UPDATE_ALL_REPLIES':
return Object.assign({}, state, {
replies: Object.assign({}, state.replies, {
[action.commentId]: state.replies[action.commentId].map(reply =>
Object.assign({}, reply, action.comment)
)
})
});
case 'SET_REPLIES':
return Object.assign({}, state, {
// Append new replies to the state.replies structure
replies: mergeWith({}, state.replies, action.replies, (replies, newReplies) => (
uniqBy((replies || []).concat(newReplies || []), 'id')
)),
// Also set the `moreRepliesToLoad` property on the top-level comments
comments: state.comments.map(comment => {
if (action.replies[comment.id]) {
return Object.assign({}, comment, {
moreRepliesToLoad: action.replies[comment.id].length === COMMENT_LIMIT
});
}
return comment;
})
});
case 'SET_LOVED':
return Object.assign({}, state, {
loved: action.info
@ -182,10 +103,6 @@ module.exports.previewReducer = (state, action) => {
state = JSON.parse(JSON.stringify(state));
state.status.studioRequests[action.studioId] = action.status;
return state;
case 'SET_MORE_COMMENTS_TO_LOAD':
return Object.assign({}, state, {
moreCommentsToLoad: action.moreCommentsToLoad
});
case 'SET_VISIBILITY_INFO':
return Object.assign({}, state, {
visibilityInfo: action.visibilityInfo
@ -247,16 +164,6 @@ module.exports.setProjectStudios = items => ({
items: items
});
module.exports.setComments = items => ({
type: 'SET_COMMENTS',
items: items
});
module.exports.setReplies = replies => ({
type: 'SET_REPLIES',
replies: replies
});
module.exports.setCuratedStudios = items => ({
type: 'SET_CURATED_STUDIOS',
items: items
@ -284,64 +191,6 @@ module.exports.setStudioFetchStatus = (studioId, status) => ({
status: status
});
module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({
type: 'UPDATE_COMMENT',
commentId: commentId,
topLevelCommentId: topLevelCommentId,
comment: {
visibility: 'deleted'
}
});
module.exports.setRepliesDeleted = commentId => ({
type: 'UPDATE_ALL_REPLIES',
commentId: commentId,
comment: {
visibility: 'deleted'
}
});
module.exports.setCommentReported = (commentId, topLevelCommentId) => ({
type: 'UPDATE_COMMENT',
commentId: commentId,
topLevelCommentId: topLevelCommentId,
comment: {
visibility: 'reported'
}
});
module.exports.setCommentRestored = (commentId, topLevelCommentId) => ({
type: 'UPDATE_COMMENT',
commentId: commentId,
topLevelCommentId: topLevelCommentId,
comment: {
visibility: 'visible'
}
});
module.exports.setRepliesRestored = commentId => ({
type: 'UPDATE_ALL_REPLIES',
commentId: commentId,
comment: {
visibility: 'visible'
}
});
module.exports.addNewComment = (comment, topLevelCommentId) => ({
type: 'ADD_NEW_COMMENT',
comment: comment,
topLevelCommentId: topLevelCommentId
});
module.exports.setMoreCommentsToLoad = moreCommentsToLoad => ({
type: 'SET_MORE_COMMENTS_TO_LOAD',
moreCommentsToLoad: moreCommentsToLoad
});
module.exports.resetComments = () => ({
type: 'RESET_COMMENTS'
});
module.exports.setVisibilityInfo = visibilityInfo => ({
type: 'SET_VISIBILITY_INFO',
visibilityInfo: visibilityInfo
@ -462,94 +311,6 @@ module.exports.getFavedStatus = (id, username, token) => (dispatch => {
});
});
module.exports.getTopLevelComments = (id, offset, ownerUsername, isAdmin, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHING));
api({
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${id}/comments`,
authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT}
}, (err, body, res) => {
if (err) {
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
dispatch(module.exports.setError('No comment info'));
return;
}
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHED));
dispatch(module.exports.setComments(body));
dispatch(module.exports.getReplies(id, body.map(comment => comment.id), 0, ownerUsername, isAdmin, token));
// If we loaded a full page of comments, assume there are more to load.
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
// any more server query complexity, so seems worth it. In the case of a project with
// number of comments divisible by the COMMENT_LIMIT, the load more button will be
// clickable, but upon clicking it will go away.
dispatch(module.exports.setMoreCommentsToLoad(body.length === COMMENT_LIMIT));
});
});
module.exports.getCommentById = (projectId, commentId, ownerUsername, isAdmin, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHING));
api({
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${commentId}`,
authentication: token ? token : null
}, (err, body, res) => {
if (err) {
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
if (!body || res.statusCode >= 400) { // NotFound
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
dispatch(module.exports.setError('No comment info'));
return;
}
if (body.parent_id) {
// If the comment is a reply, load the parent
return dispatch(module.exports.getCommentById(projectId, body.parent_id, ownerUsername, isAdmin, token));
}
// If the comment is not a reply, show it as top level and load replies
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHED));
dispatch(module.exports.setComments([body]));
dispatch(module.exports.getReplies(projectId, [body.id], 0, ownerUsername, isAdmin, token));
});
});
module.exports.getReplies = (projectId, commentIds, offset, ownerUsername, isAdmin, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHING));
const fetchedReplies = {};
eachLimit(commentIds, 10, (parentId, callback) => {
api({
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${parentId}/replies`,
authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT}
}, (err, body, res) => {
if (err) {
return callback(`Error fetching comment replies: ${err}`);
}
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
return callback('No comment reply information');
}
fetchedReplies[parentId] = body;
callback(null, body);
});
}, err => {
if (err) {
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.ERROR));
dispatch(module.exports.setError(err));
return;
}
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHED));
dispatch(module.exports.setReplies(fetchedReplies));
});
});
module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
if (faved) {
@ -882,62 +643,6 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
});
});
module.exports.deleteComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
/* TODO fetching/fetched/error states updates for comment deleting */
api({
uri: `/proxy/comments/project/${projectId}/comment/${commentId}`,
authentication: token,
withCredentials: true,
method: 'DELETE',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(module.exports.setCommentDeleted(commentId, topLevelCommentId));
if (!topLevelCommentId) {
dispatch(module.exports.setRepliesDeleted(commentId));
}
});
});
module.exports.reportComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/project/${projectId}/comment/${commentId}/report`,
authentication: token,
withCredentials: true,
method: 'POST',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
// TODO use the reportId in the response for unreporting functionality
dispatch(module.exports.setCommentReported(commentId, topLevelCommentId));
});
});
module.exports.restoreComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/admin/project/${projectId}/comment/${commentId}/undelete`,
authentication: token,
withCredentials: true,
method: 'PUT',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(module.exports.setCommentRestored(commentId, topLevelCommentId));
if (!topLevelCommentId) {
dispatch(module.exports.setRepliesRestored(commentId));
}
});
});
module.exports.shareProject = (projectId, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
api({

View file

@ -0,0 +1,180 @@
const eachLimit = require('async/eachLimit');
const api = require('../lib/api');
const log = require('../lib/log');
const COMMENT_LIMIT = 20;
const {
addNewComment,
resetComments,
Status,
setFetchStatus,
setCommentDeleted,
setCommentReported,
setCommentRestored,
setMoreCommentsToLoad,
setComments,
setError,
setReplies,
setRepliesDeleted,
setRepliesRestored
} = require('../redux/comments.js');
const getReplies = (projectId, commentIds, offset, ownerUsername, isAdmin, token) => (dispatch => {
dispatch(setFetchStatus('replies', Status.FETCHING));
const fetchedReplies = {};
eachLimit(commentIds, 10, (parentId, callback) => {
api({
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${parentId}/replies`,
authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT}
}, (err, body, res) => {
if (err) {
return callback(`Error fetching comment replies: ${err}`);
}
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
return callback('No comment reply information');
}
fetchedReplies[parentId] = body;
callback(null, body);
});
}, err => {
if (err) {
dispatch(setFetchStatus('replies', Status.ERROR));
dispatch(setError(err));
return;
}
dispatch(setFetchStatus('replies', Status.FETCHED));
dispatch(setReplies(fetchedReplies));
});
});
const getTopLevelComments = (id, offset, ownerUsername, isAdmin, token) => (dispatch => {
dispatch(setFetchStatus('comments', Status.FETCHING));
api({
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${id}/comments`,
authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT}
}, (err, body, res) => {
if (err) {
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError(err));
return;
}
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError('No comment info'));
return;
}
dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments(body));
dispatch(getReplies(id, body.map(comment => comment.id), 0, ownerUsername, isAdmin, token));
// If we loaded a full page of comments, assume there are more to load.
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
// any more server query complexity, so seems worth it. In the case of a project with
// number of comments divisible by the COMMENT_LIMIT, the load more button will be
// clickable, but upon clicking it will go away.
dispatch(setMoreCommentsToLoad(body.length === COMMENT_LIMIT));
});
});
const getCommentById = (projectId, commentId, ownerUsername, isAdmin, token) => (dispatch => {
dispatch(setFetchStatus('comments', Status.FETCHING));
api({
uri: `${isAdmin ? '/admin' : `/users/${ownerUsername}`}/projects/${projectId}/comments/${commentId}`,
authentication: token ? token : null
}, (err, body, res) => {
if (err) {
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError(err));
return;
}
if (!body || res.statusCode >= 400) { // NotFound
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError('No comment info'));
return;
}
if (body.parent_id) {
// If the comment is a reply, load the parent
return dispatch(getCommentById(projectId, body.parent_id, ownerUsername, isAdmin, token));
}
// If the comment is not a reply, show it as top level and load replies
dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments([body]));
dispatch(getReplies(projectId, [body.id], 0, ownerUsername, isAdmin, token));
});
});
const deleteComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
/* TODO fetching/fetched/error states updates for comment deleting */
api({
uri: `/proxy/comments/project/${projectId}/comment/${commentId}`,
authentication: token,
withCredentials: true,
method: 'DELETE',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(setCommentDeleted(commentId, topLevelCommentId));
if (!topLevelCommentId) {
dispatch(setRepliesDeleted(commentId));
}
});
});
const reportComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/project/${projectId}/comment/${commentId}/report`,
authentication: token,
withCredentials: true,
method: 'POST',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
// TODO use the reportId in the response for unreporting functionality
dispatch(setCommentReported(commentId, topLevelCommentId));
});
});
const restoreComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/admin/project/${projectId}/comment/${commentId}/undelete`,
authentication: token,
withCredentials: true,
method: 'PUT',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(setCommentRestored(commentId, topLevelCommentId));
if (!topLevelCommentId) {
dispatch(setRepliesRestored(commentId));
}
});
});
module.exports = {
getTopLevelComments,
getCommentById,
getReplies,
deleteComment,
reportComment,
restoreComment,
// Re-export these specific action creators directly so the implementer
// does not need to go to two places for comment actions
addNewComment,
resetComments
};

View file

@ -1,5 +1,6 @@
const keyMirror = require('keymirror');
const defaults = require('lodash.defaults');
const get = require('lodash.get');
const {requestSession, requestSessionWithRetry} = require('../lib/session');
const messageCountActions = require('./message-count.js');
@ -118,3 +119,13 @@ module.exports.refreshSessionWithRetry = () => (dispatch => {
dispatch(module.exports.setSessionError(err));
});
});
// Selectors
module.exports.selectIsLoggedIn = state => get(state, ['session', 'session', 'user'], false);
module.exports.selectUsername = state => get(state, ['session', 'session', 'user', 'username'], null);
module.exports.selectToken = state => get(state, ['session', 'session', 'user', 'token'], null);
module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false);
module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false);
// NB logged out user id as NaN so that it can never be used in equality testing since NaN !== NaN
module.exports.selectUserId = state => get(state, ['session', 'session', 'user', 'id'], NaN);

View file

@ -0,0 +1,195 @@
const eachLimit = require('async/eachLimit');
const api = require('../lib/api');
const log = require('../lib/log');
const COMMENT_LIMIT = 20;
const {
addNewComment,
resetComments,
Status,
setFetchStatus,
setCommentDeleted,
setCommentReported,
setCommentRestored,
setMoreCommentsToLoad,
setComments,
setError,
setReplies,
setRepliesDeleted,
setRepliesRestored,
selectCommentCount
} = require('../redux/comments.js');
const {
selectIsAdmin,
selectToken
} = require('./session');
const {
selectStudioId
} = require('./studio');
const getReplies = (studioId, commentIds, offset, isAdmin, token) => (dispatch => {
dispatch(setFetchStatus('replies', Status.FETCHING));
const fetchedReplies = {};
eachLimit(commentIds, 10, (parentId, callback) => {
api({
uri: `${isAdmin ? '/admin' : ''}/studios/${studioId}/comments/${parentId}/replies`,
authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT}
}, (err, body, res) => {
if (err) {
return callback(`Error fetching comment replies: ${err}`);
}
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
return callback('No comment reply information');
}
fetchedReplies[parentId] = body;
callback(null, body);
});
}, err => {
if (err) {
dispatch(setFetchStatus('replies', Status.ERROR));
dispatch(setError(err));
return;
}
dispatch(setFetchStatus('replies', Status.FETCHED));
dispatch(setReplies(fetchedReplies));
});
});
const getTopLevelComments = () => ((dispatch, getState) => {
dispatch(setFetchStatus('comments', Status.FETCHING));
const state = getState();
const id = selectStudioId(state);
const offset = selectCommentCount(state);
const isAdmin = selectIsAdmin(state);
const token = selectToken(state);
api({
uri: `${isAdmin ? '/admin' : ''}/studios/${id}/comments`,
authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT}
}, (err, body, res) => {
if (err) {
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError(err));
return;
}
if (typeof body === 'undefined' || res.statusCode >= 400) { // NotFound
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError('No comment info'));
return;
}
dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments(body));
dispatch(getReplies(id, body.map(comment => comment.id), 0, isAdmin, token));
// If we loaded a full page of comments, assume there are more to load.
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
// any more server query complexity, so seems worth it. In the case of a project with
// number of comments divisible by the COMMENT_LIMIT, the load more button will be
// clickable, but upon clicking it will go away.
dispatch(setMoreCommentsToLoad(body.length === COMMENT_LIMIT));
});
});
const getCommentById = (studioId, commentId, isAdmin, token) => (dispatch => {
dispatch(setFetchStatus('comments', Status.FETCHING));
api({
uri: `${isAdmin ? '/admin' : ''}/studios/${studioId}/comments/${commentId}`,
authentication: token ? token : null
}, (err, body, res) => {
if (err) {
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError(err));
return;
}
if (!body || res.statusCode >= 400) { // NotFound
dispatch(setFetchStatus('comments', Status.ERROR));
dispatch(setError('No comment info'));
return;
}
if (body.parent_id) {
// If the comment is a reply, load the parent
return dispatch(getCommentById(studioId, body.parent_id, isAdmin, token));
}
// If the comment is not a reply, show it as top level and load replies
dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments([body]));
dispatch(getReplies(studioId, [body.id], 0, isAdmin, token));
});
});
const deleteComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
/* TODO fetching/fetched/error states updates for comment deleting */
api({
uri: `/proxy/comments/studio/${studioId}/comment/${commentId}`,
authentication: token,
withCredentials: true,
method: 'DELETE',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(setCommentDeleted(commentId, topLevelCommentId));
if (!topLevelCommentId) {
dispatch(setRepliesDeleted(commentId));
}
});
});
const reportComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/studio/${studioId}/comment/${commentId}/report`,
authentication: token,
withCredentials: true,
method: 'POST',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
// TODO use the reportId in the response for unreporting functionality
dispatch(setCommentReported(commentId, topLevelCommentId));
});
});
const restoreComment = (studioId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/admin/studio/${studioId}/comment/${commentId}/undelete`,
authentication: token,
withCredentials: true,
method: 'PUT',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(setCommentRestored(commentId, topLevelCommentId));
if (!topLevelCommentId) {
dispatch(setRepliesRestored(commentId));
}
});
});
module.exports = {
getTopLevelComments,
getCommentById,
getReplies,
deleteComment,
reportComment,
restoreComment,
// Re-export these specific action creators directly so the implementer
// does not need to go to two places for comment actions
addNewComment,
resetComments
};

View file

@ -3,6 +3,8 @@ const keyMirror = require('keymirror');
const api = require('../lib/api');
const log = require('../lib/log');
const {selectUserId, selectIsAdmin, selectIsSocial, selectUsername, selectToken} = require('./session');
const Status = keyMirror({
FETCHED: null,
NOT_FETCHED: null,
@ -18,6 +20,7 @@ const getInitialState = () => ({
commentingAllowed: false,
thumbnail: '',
followers: 0,
owner: null,
rolesStatus: Status.NOT_FETCHED,
manager: false,
@ -55,6 +58,8 @@ const studioReducer = (state, action) => {
}
};
// Action Creators
const setFetchStatus = (fetchType, fetchStatus, error) => ({
type: 'SET_FETCH_STATUS',
fetchType,
@ -72,8 +77,31 @@ const setRoles = roles => ({
roles: roles
});
const getInfo = studioId => (dispatch => {
// Selectors
// Fine-grain selector helpers - not exported, use the higher level selectors below
const isCreator = state => selectUserId(state) === state.studio.owner;
const isCurator = state => state.studio.curator;
const isManager = state => state.studio.manager || isCreator(state);
// Action-based permissions selectors
const selectCanEditInfo = state => selectIsAdmin(state) || isManager(state);
const selectCanAddProjects = state =>
isManager(state) ||
isCurator(state) ||
(selectIsSocial(state) && state.studio.openToAll);
// This isn't "canComment" since they could be muted, but comment composer handles that
const selectShowCommentComposer = state => selectIsSocial(state);
// Data selectors
const selectStudioId = state => state.studio.id;
// Thunks
const getInfo = () => ((dispatch, getState) => {
dispatch(setFetchStatus('infoStatus', Status.FETCHING));
const studioId = selectStudioId(getState());
api({uri: `/studios/${studioId}`}, (err, body, res) => {
if (err || typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(setFetchStatus('infoStatus', Status.ERROR, err));
@ -86,13 +114,18 @@ const getInfo = studioId => (dispatch => {
openToAll: body.open_to_all,
commentingAllowed: body.commenting_allowed,
updated: new Date(body.history.modified),
followers: body.stats.followers
followers: body.stats.followers,
owner: body.owner
}));
});
});
const getRoles = (studioId, username, token) => (dispatch => {
const getRoles = () => ((dispatch, getState) => {
dispatch(setFetchStatus('rolesStatus', Status.FETCHING));
const state = getState();
const studioId = selectStudioId(state);
const username = selectUsername(state);
const token = selectToken(state);
api({
uri: `/studios/${studioId}/users/${username}`,
authentication: token
@ -115,6 +148,14 @@ module.exports = {
getInitialState,
studioReducer,
Status,
// Thunks
getInfo,
getRoles
getRoles,
// Selectors
selectStudioId,
selectCanEditInfo,
selectCanAddProjects,
selectShowCommentComposer
};

View file

@ -4,12 +4,5 @@
"pattern": "^/components/?$",
"view": "components/components",
"title": "Components"
},
{
"name": "studio",
"pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
"routeAlias": "/studios-playground/?$",
"view": "studio/studio",
"title": "Studio Playground"
}
]

View file

@ -296,6 +296,13 @@
"view": "studentregistration/studentregistration",
"title": "Class Registration"
},
{
"name": "studio",
"pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
"routeAlias": "/studios-playground/?$",
"view": "studio/studio",
"title": "Studio Playground"
},
{
"name": "teacher-faq",
"pattern": "^/educators/faq/?$",

View file

@ -8,22 +8,78 @@ const Carousel = require('../../components/carousel/carousel.jsx');
const Form = require('../../components/forms/form.jsx');
const Input = require('../../components/forms/input.jsx');
const Spinner = require('../../components/spinner/spinner.jsx');
const Grid = require('../../components/grid/grid.jsx');
const TextArea = require('../../components/forms/textarea.jsx');
const SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
const Select = require('../../components/forms/select.jsx');
require('./components.scss');
const Components = () => (
<div className="components">
<div className="inner">
<h1>Nav Bubbles</h1>
<div className="subnavigation">
<SubNavigation>
<a href="">
<li className="active">
cats
</li>
</a>
<a href="">
<li>
also cats
</li>
</a>
<a href="">
<li>
not cats
</li>
</a>
</SubNavigation>
</div>
<h1>Grid</h1>
<Grid
showAvatar
/>
<h1>Button</h1>
<Button>I love button</Button>
<Button>I love buttons</Button>
<h1>Form</h1>
<Form>
<Input
maxLength="30"
name="test"
type="text"
/>
</Form>
<div className="form">
<Form>
<Select
label="Drop-down"
required
options={[
{
label: 'first option',
value: 1
},
{
label: 'second option',
value: 2
},
{
label: 'third option',
value: 3
}
]}
name="name"
value={1}
/>
<Input
label="Text input"
required
maxLength="30"
name="test"
/>
<TextArea
label="Text area"
name="textarea1"
required
/>
</Form>
</div>
<h1>Box Component</h1>
<Box
more="Cat Gifs"

View file

@ -5,6 +5,19 @@
margin: 0 0 10px 0;
}
.subnavigation {
li {
background-color: $active-gray;
&.active {
background-color: $ui-blue;
}
}
}
.form {
width: 200px;
}
.colors {
span {
display: inline-block;

View file

@ -110,7 +110,7 @@ class Comment extends React.Component {
highlighted,
id,
parentId,
projectId,
postURI,
replyUsername,
visibility
} = this.props;
@ -234,7 +234,7 @@ class Comment extends React.Component {
isReply
commenteeId={author.id}
parentId={parentId || id}
projectId={projectId}
postURI={postURI}
onAddComment={this.handlePostReply}
onCancel={this.handleToggleReplying}
/>
@ -285,7 +285,7 @@ Comment.propTypes = {
onReport: PropTypes.func,
onRestore: PropTypes.func,
parentId: PropTypes.number,
projectId: PropTypes.string,
postURI: PropTypes.string,
replyUsername: PropTypes.string,
visibility: PropTypes.string
};

View file

@ -79,7 +79,7 @@ class ComposeComment extends React.Component {
handlePost () {
this.setState({status: ComposeStatus.SUBMITTING});
api({
uri: `/proxy/comments/project/${this.props.projectId}`,
uri: this.props.postURI,
authentication: this.props.user.token,
withCredentials: true,
method: 'POST',
@ -434,7 +434,7 @@ ComposeComment.propTypes = {
onAddComment: PropTypes.func,
onCancel: PropTypes.func,
parentId: PropTypes.number,
projectId: PropTypes.string,
postURI: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.number,
username: PropTypes.string,

View file

@ -87,7 +87,7 @@ class TopLevelComment extends React.Component {
onReport,
onRestore,
replies,
projectId,
postURI,
visibility
} = this.props;
@ -97,7 +97,7 @@ class TopLevelComment extends React.Component {
<FlexRow className="comment-container">
<Comment
highlighted={highlightedCommentId === id}
projectId={projectId}
postURI={postURI}
onAddComment={this.handleAddComment}
{...{
author,
@ -138,7 +138,7 @@ class TopLevelComment extends React.Component {
id={reply.id}
key={reply.id}
parentId={id}
projectId={projectId}
postURI={postURI}
replyUsername={this.authorUsername(reply.commentee_id)}
visibility={reply.visibility}
onAddComment={this.handleAddComment}
@ -188,7 +188,7 @@ TopLevelComment.propTypes = {
onReport: PropTypes.func,
onRestore: PropTypes.func,
parentId: PropTypes.number,
projectId: PropTypes.string,
postURI: PropTypes.string,
replies: PropTypes.arrayOf(PropTypes.object),
visibility: PropTypes.string
};

View file

@ -581,7 +581,7 @@ const PreviewPresentation = ({
{projectInfo.comments_allowed ? (
isLoggedIn ? (
isShared && <ComposeComment
projectId={projectId}
postURI={`/proxy/comments/project/${projectId}`}
onAddComment={onAddComment}
/>
) : (
@ -613,7 +613,7 @@ const PreviewPresentation = ({
key={comment.id}
moreRepliesToLoad={comment.moreRepliesToLoad}
parentId={comment.parent_id}
projectId={projectId}
postURI={`/proxy/comments/project/${projectId}`}
replies={replies && replies[comment.id] ? replies[comment.id] : []}
visibility={comment.visibility}
onAddComment={onAddComment}

View file

@ -5,6 +5,7 @@ const Page = require('../../components/page/www/page.jsx');
const render = require('../../lib/render.jsx');
const previewActions = require('../../redux/preview.js');
const commentActions = require('../../redux/comments.js');
const isSupportedBrowser = require('../../lib/supported-browser').default;
const UnsupportedBrowser = require('./unsupported-browser.jsx');
@ -16,6 +17,7 @@ if (isSupportedBrowser()) {
document.getElementById('app'),
{
preview: previewActions.previewReducer,
comments: commentActions.commentsReducer,
...ProjectView.guiReducers
},
{

View file

@ -29,6 +29,7 @@ const Meta = require('./meta.jsx');
const sessionActions = require('../../redux/session.js');
const navigationActions = require('../../redux/navigation.js');
const previewActions = require('../../redux/preview.js');
const projectCommentActions = require('../../redux/project-comment-actions.js');
const frameless = require('../../lib/frameless');
@ -998,7 +999,7 @@ const mapStateToProps = state => {
canShare: userOwnsProject && state.permissions.social,
canToggleComments: userOwnsProject || isAdmin,
canUseBackpack: isLoggedIn,
comments: state.preview.comments,
comments: state.comments.comments,
enableCommunity: projectInfoPresent,
faved: state.preview.faved,
favedLoaded: state.preview.status.faved === previewActions.Status.FETCHED,
@ -1013,7 +1014,7 @@ const mapStateToProps = state => {
isShared: isShared,
loved: state.preview.loved,
lovedLoaded: state.preview.status.loved === previewActions.Status.FETCHED,
moreCommentsToLoad: state.preview.moreCommentsToLoad,
moreCommentsToLoad: state.comments.moreCommentsToLoad,
original: state.preview.original,
parent: state.preview.parent,
playerMode: state.scratchGui.mode.isPlayerOnly,
@ -1022,7 +1023,7 @@ const mapStateToProps = state => {
projectStudios: state.preview.projectStudios,
registrationOpen: state.navigation.registrationOpen,
remixes: state.preview.remixes,
replies: state.preview.replies,
replies: state.comments.replies,
sessionStatus: state.session.status, // check if used
useScratch3Registration: state.navigation.useScratch3Registration,
user: state.session.session.user,
@ -1034,16 +1035,16 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
handleAddComment: (comment, topLevelCommentId) => {
dispatch(previewActions.addNewComment(comment, topLevelCommentId));
dispatch(projectCommentActions.addNewComment(comment, topLevelCommentId));
},
handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token));
dispatch(projectCommentActions.deleteComment(projectId, commentId, topLevelCommentId, token));
},
handleReportComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.reportComment(projectId, commentId, topLevelCommentId, token));
dispatch(projectCommentActions.reportComment(projectId, commentId, topLevelCommentId, token));
},
handleRestoreComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.restoreComment(projectId, commentId, topLevelCommentId, token));
dispatch(projectCommentActions.restoreComment(projectId, commentId, topLevelCommentId, token));
},
handleOpenRegistration: event => {
event.preventDefault();
@ -1061,8 +1062,8 @@ const mapDispatchToProps = dispatch => ({
dispatch(navigationActions.toggleLoginOpen());
},
handleSeeAllComments: (id, ownerUsername, isAdmin, token) => {
dispatch(previewActions.resetComments());
dispatch(previewActions.getTopLevelComments(id, 0, ownerUsername, isAdmin, token));
dispatch(projectCommentActions.resetComments());
dispatch(projectCommentActions.getTopLevelComments(id, 0, ownerUsername, isAdmin, token));
},
handleUpdateProjectThumbnail: (id, blob) => {
dispatch(previewActions.updateProjectThumbnail(id, blob));
@ -1093,13 +1094,13 @@ const mapDispatchToProps = dispatch => ({
}
},
getTopLevelComments: (id, offset, ownerUsername, isAdmin, token) => {
dispatch(previewActions.getTopLevelComments(id, offset, ownerUsername, isAdmin, token));
dispatch(projectCommentActions.getTopLevelComments(id, offset, ownerUsername, isAdmin, token));
},
getCommentById: (projectId, commentId, ownerUsername, isAdmin, token) => {
dispatch(previewActions.getCommentById(projectId, commentId, ownerUsername, isAdmin, token));
dispatch(projectCommentActions.getCommentById(projectId, commentId, ownerUsername, isAdmin, token));
},
getMoreReplies: (projectId, commentId, offset, ownerUsername, isAdmin, token) => {
dispatch(previewActions.getReplies(projectId, [commentId], offset, ownerUsername, isAdmin, token));
dispatch(projectCommentActions.getReplies(projectId, [commentId], offset, ownerUsername, isAdmin, token));
},
getFavedStatus: (id, username, token) => {
dispatch(previewActions.getFavedStatus(id, username, token));
@ -1136,7 +1137,7 @@ const mapDispatchToProps = dispatch => ({
},
remixProject: () => {
dispatch(GUI.remixProject());
dispatch(previewActions.resetComments());
dispatch(projectCommentActions.resetComments());
},
setPlayer: player => {
dispatch(GUI.setPlayer(player));

View file

@ -4,7 +4,7 @@
"onePointFour.introNote": "{noteLabel} You can still share projects from 1.4 to the Scratch website. However, projects created in newer versions of Scratch cannot be opened in 1.4.",
"onePointFour.downloads": "Downloads",
"onePointFour.macTitle": "Mac OS X",
"onePointFour.macBody": "Compatible with Mac OSX 10.4 or later",
"onePointFour.macBody": "Compatible with Mac OSX 10.4 through 10.14",
"onePointFour.windowsTitle": "Windows",
"onePointFour.windowsBody": "Compatible with Windows 2000, XP, Vista, 7, and 8",
"onePointFour.windowsNetworkInstaller": "installer",

View file

@ -1,15 +1,88 @@
import React from 'react';
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import {useParams} from 'react-router-dom';
import {connect} from 'react-redux';
import {FormattedMessage} from 'react-intl';
const StudioComments = () => {
import Button from '../../components/forms/button.jsx';
import ComposeComment from '../preview/comment/compose-comment.jsx';
import TopLevelComment from '../preview/comment/top-level-comment.jsx';
import studioCommentActions from '../../redux/studio-comment-actions.js';
import {selectShowCommentComposer} from '../../redux/studio.js';
const StudioComments = ({
comments,
handleLoadMoreComments,
handleNewComment,
moreCommentsToLoad,
replies,
shouldShowCommentComposer
}) => {
const {studioId} = useParams();
useEffect(() => {
if (comments.length === 0) handleLoadMoreComments();
}, []); // Only runs once after the first render
return (
<div>
<h2>Comments</h2>
<p>Studio {studioId}</p>
<div>
{shouldShowCommentComposer &&
<ComposeComment
postURI={`/proxy/comments/studio/${studioId}`}
onAddComment={handleNewComment}
/>
}
{comments.map(comment => (
<TopLevelComment
author={comment.author}
canReply={shouldShowCommentComposer}
content={comment.content}
datetimeCreated={comment.datetime_created}
id={comment.id}
key={comment.id}
moreRepliesToLoad={comment.moreRepliesToLoad}
parentId={comment.parent_id}
postURI={`/proxy/comments/studio/${studioId}`}
replies={replies && replies[comment.id] ? replies[comment.id] : []}
visibility={comment.visibility}
onAddComment={handleNewComment}
/>
))}
{moreCommentsToLoad &&
<Button
className="button load-more-button"
onClick={handleLoadMoreComments}
>
<FormattedMessage id="general.loadMore" />
</Button>
}
</div>
</div>
);
};
export default StudioComments;
StudioComments.propTypes = {
comments: PropTypes.arrayOf(PropTypes.shape({})),
handleLoadMoreComments: PropTypes.func,
handleNewComment: PropTypes.func,
moreCommentsToLoad: PropTypes.bool,
replies: PropTypes.shape({}),
shouldShowCommentComposer: PropTypes.bool
};
export default connect(
state => ({
comments: state.comments.comments,
moreCommentsToLoad: state.comments.moreCommentsToLoad,
replies: state.comments.replies,
shouldShowCommentComposer: selectShowCommentComposer(state)
}),
{
handleLoadMoreComments: studioCommentActions.getTopLevelComments,
handleNewComment: studioCommentActions.addNewComment
}
)(StudioComments);

View file

@ -1,20 +1,19 @@
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import {useParams} from 'react-router-dom';
import {connect} from 'react-redux';
import Debug from './debug.jsx';
import {getInfo, getRoles} from '../../redux/studio';
const StudioInfo = ({username, studio, token, onLoadInfo, onLoadRoles}) => {
const {studioId} = useParams();
import {selectIsLoggedIn} from '../../redux/session';
import {getInfo, getRoles, selectCanEditInfo} from '../../redux/studio';
const StudioInfo = ({isLoggedIn, studio, canEditInfo, onLoadInfo, onLoadRoles}) => {
useEffect(() => { // Load studio info after first render
if (studioId) onLoadInfo(studioId);
}, [studioId]);
onLoadInfo();
}, []);
useEffect(() => { // Load roles info once the username is available
if (studioId && username && token) onLoadRoles(studioId, username, token);
}, [studioId, username, token]);
useEffect(() => { // Load roles info once the user is logged in is available
if (isLoggedIn) onLoadRoles();
}, [isLoggedIn]);
return (
<div>
@ -23,13 +22,17 @@ const StudioInfo = ({username, studio, token, onLoadInfo, onLoadRoles}) => {
label="Studio Info"
data={studio}
/>
<Debug
label="Studio Info Permissions"
data={{canEditInfo}}
/>
</div>
);
};
StudioInfo.propTypes = {
username: PropTypes.string,
token: PropTypes.string,
canEditInfo: PropTypes.bool,
isLoggedIn: PropTypes.bool,
studio: PropTypes.shape({
// Fill this in as the data is used, just for demo now
}),
@ -38,17 +41,13 @@ StudioInfo.propTypes = {
};
export default connect(
state => {
const user = state.session.session.user;
return {
studio: state.studio,
username: user && user.username,
token: user && user.token
};
},
dispatch => ({
onLoadInfo: studioId => dispatch(getInfo(studioId)),
onLoadRoles: (studioId, username, token) => dispatch(
getRoles(studioId, username, token))
})
state => ({
studio: state.studio,
isLoggedIn: selectIsLoggedIn(state),
canEditInfo: selectCanEditInfo(state)
}),
{
onLoadInfo: getInfo,
onLoadRoles: getRoles
}
)(StudioInfo);

View file

@ -7,10 +7,11 @@ import {projectFetcher} from './lib/fetchers';
import {projects} from './lib/redux-modules';
import Debug from './debug.jsx';
const {actions, selector} = projects;
const {actions, selector: projectsSelector} = projects;
import {selectCanAddProjects} from '../../redux/studio';
const StudioProjects = ({
items, error, loading, moreToLoad, onLoadMore
canAddProjects, items, error, loading, moreToLoad, onLoadMore
}) => {
const {studioId} = useParams();
@ -27,6 +28,10 @@ const StudioProjects = ({
label="Error"
data={error}
/>}
<Debug
label="Project Permissions"
data={{canAddProjects}}
/>
<div>
{items.map((item, index) =>
(<Debug
@ -48,6 +53,7 @@ const StudioProjects = ({
};
StudioProjects.propTypes = {
canAddProjects: PropTypes.bool,
items: PropTypes.array, // eslint-disable-line react/forbid-prop-types
loading: PropTypes.bool,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
@ -55,7 +61,10 @@ StudioProjects.propTypes = {
onLoadMore: PropTypes.func
};
const mapStateToProps = state => selector(state);
const mapStateToProps = state => ({
...projectsSelector(state),
canAddProjects: selectCanAddProjects(state)
});
const mapDispatchToProps = dispatch => ({
onLoadMore: (studioId, offset) => dispatch(

View file

@ -25,6 +25,7 @@ import {
} from './lib/redux-modules';
const {studioReducer} = require('../../redux/studio');
const {commentsReducer} = require('../../redux/comments');
const StudioShell = () => {
const match = useRouteMatch();
@ -75,6 +76,15 @@ render(
[curators.key]: curators.reducer,
[managers.key]: managers.reducer,
[activity.key]: activity.reducer,
studio: studioReducer
studio: studioReducer,
comments: commentsReducer
},
{
studio: {
// Include the studio id in the initial state to allow us
// to stop passing around the studio id in components
// when it is only needed for data fetching, not for rendering.
id: window.location.pathname.split('/')[2]
}
}
);

View file

@ -0,0 +1,51 @@
{
"studios": {
"isManager": {
"manager": true
},
"isCurator": {
"curator": true
},
"creator1": {
"owner": 1
},
"openToAll": {
"openToAll": true
}
},
"sessions": {
"user1Admin": {
"session": {
"user": {
"id": 1
},
"permissions": {
"admin": true
}
}
},
"user1Social": {
"session": {
"user": {
"id": 1
},
"permissions": {
"social": true
}
}
},
"user1": {
"session": {
"user": {
"id": 1,
"username": "user1-username",
"token": "user1-token"
},
"permissions": {
"admin": false,
"social": false
}
}
}
}
}

View file

@ -0,0 +1,151 @@
const tap = require('tap');
const Comments = require('../../../src/redux/comments');
const initialState = Comments.getInitialState();
const reducer = Comments.commentsReducer;
let state;
tap.tearDown(() => process.nextTick(process.exit));
tap.test('Reducer', t => {
t.type(reducer, 'function');
t.type(initialState, 'object');
// Reducers should return their default state when called without state
let undefinedState;
t.deepEqual(initialState, reducer(undefinedState, {type: 'fake action'}));
t.end();
});
tap.test('setFetchStatus', t => {
// initial value
t.equal(initialState.status.comments, Comments.Status.NOT_FETCHED);
state = reducer(initialState, Comments.setFetchStatus('comments', Comments.Status.FETCHING));
t.equal(state.status.comments, Comments.Status.FETCHING);
state = reducer(state, Comments.setFetchStatus('comments', Comments.Status.FETCHED));
t.equal(state.status.comments, Comments.Status.FETCHED);
t.end();
});
tap.test('setComments', t => {
// Initial value
t.deepEqual(initialState.comments, []);
state = reducer(initialState, Comments.setComments([{id: 1}, {id: 2}]));
state = reducer(state, Comments.setComments([{id: 3}, {id: 4}]));
t.deepEqual(state.comments, [{id: 1}, {id: 2}, {id: 3}, {id: 4}]);
t.end();
});
const commentState = {
comments: [
{id: 'id1', visibility: 'visible'},
{id: 'id2', visibility: 'visible'},
{id: 'id3', visibility: 'visible'}
],
replies: {
id1: [
{id: 'id4', visibility: 'visible'},
{id: 'id5', visibility: 'visible'}
]
}
};
tap.test('setComments, discards duplicates', t => {
state = reducer(commentState, Comments.setComments([{id: 'id1'}]));
// Does not increase the number of comments, still 3
t.equal(state.comments.length, 3);
t.end();
});
tap.test('setCommentDeleted, top level comment', t => {
state = reducer(commentState, Comments.setCommentDeleted('id2'));
t.equal(state.comments[1].visibility, 'deleted');
t.end();
});
tap.test('setCommentDeleted, reply comment', t => {
state = reducer(commentState, Comments.setCommentDeleted('id4', 'id1'));
t.equal(state.replies.id1[0].visibility, 'deleted');
t.end();
});
tap.test('setRepliesDeleted/Restored', t => {
state = reducer(commentState, Comments.setRepliesDeleted('id1'));
t.equal(state.replies.id1[0].visibility, 'deleted');
t.equal(state.replies.id1[1].visibility, 'deleted');
state = reducer(state, Comments.setRepliesRestored('id1'));
t.equal(state.replies.id1[0].visibility, 'visible');
t.equal(state.replies.id1[1].visibility, 'visible');
t.end();
});
tap.test('setCommentReported, top level comment', t => {
state = reducer(commentState, Comments.setCommentReported('id2'));
t.equal(state.comments[1].visibility, 'reported');
t.end();
});
tap.test('setCommentReported, reply comment', t => {
state = reducer(commentState, Comments.setCommentReported('id4', 'id1'));
t.equal(state.replies.id1[0].visibility, 'reported');
t.end();
});
tap.test('addNewComment, top level comment', t => {
state = reducer(commentState, Comments.addNewComment({id: 'new comment'}));
// Adds comment to beginning of list
t.equal(state.comments[0].id, 'new comment');
t.end();
});
tap.test('addNewComment, reply comment', t => {
state = reducer(commentState, Comments.addNewComment({id: 'new comment'}, 'id1'));
// Adds replies to the end of the replies list
t.equal(state.replies.id1[2].id, 'new comment');
t.end();
});
tap.test('setReplies', t => {
// setReplies should append new replies
state = reducer(commentState, Comments.setReplies({
id1: {id: 'id6'}
}));
t.equal(state.replies.id1[2].id, 'id6');
t.equal(state.comments[0].moreRepliesToLoad, false);
// setReplies should ignore duplicates, do the same as above again
t.equal(state.replies.id1.length, 3);
state = reducer(state, Comments.setReplies({id1: {id: 'id6'}}));
t.equal(state.replies.id1.length, 3);
// setReplies can add replies to a comment that didn't have any
state = reducer(state, Comments.setReplies({
id2: {id: 'id7'}
}));
t.equal(state.replies.id1.length, 3);
t.equal(state.replies.id2.length, 1);
t.equal(state.replies.id2[0].id, 'id7');
t.equal(state.comments[0].moreRepliesToLoad, false);
t.equal(state.comments[1].moreRepliesToLoad, false);
// Getting 20 (COMMENT_LIMIT) replies sets moreRepliesToLoad to true
state = reducer(state, Comments.setReplies({
id3: (new Array(20)).map((_, i) => ({id: `id${i + 1}`}))
}));
t.equal(state.comments[0].moreRepliesToLoad, false);
t.equal(state.comments[1].moreRepliesToLoad, false);
t.equal(state.comments[2].moreRepliesToLoad, true);
// Getting one more reply sets moreRepliesToLoad back to false
state = reducer(state, Comments.setReplies({
id3: {id: 'id21'}
}));
t.equal(state.comments[2].moreRepliesToLoad, false);
t.end();
});

View file

@ -54,123 +54,3 @@ tap.test('updateProjectInfo', t => {
});
t.end();
});
tap.test('setComments', t => {
// Initial value
t.deepEqual(initialState.comments, []);
state = reducer(initialState, Preview.setComments([{id: 1}, {id: 2}]));
state = reducer(state, Preview.setComments([{id: 3}, {id: 4}]));
t.deepEqual(state.comments, [{id: 1}, {id: 2}, {id: 3}, {id: 4}]);
t.end();
});
const commentState = {
comments: [
{id: 'id1', visibility: 'visible'},
{id: 'id2', visibility: 'visible'},
{id: 'id3', visibility: 'visible'}
],
replies: {
id1: [
{id: 'id4', visibility: 'visible'},
{id: 'id5', visibility: 'visible'}
]
}
};
tap.test('setComments, discards duplicates', t => {
state = reducer(commentState, Preview.setComments([{id: 'id1'}]));
// Does not increase the number of comments, still 3
t.equal(state.comments.length, 3);
t.end();
});
tap.test('setCommentDeleted, top level comment', t => {
state = reducer(commentState, Preview.setCommentDeleted('id2'));
t.equal(state.comments[1].visibility, 'deleted');
t.end();
});
tap.test('setCommentDeleted, reply comment', t => {
state = reducer(commentState, Preview.setCommentDeleted('id4', 'id1'));
t.equal(state.replies.id1[0].visibility, 'deleted');
t.end();
});
tap.test('setRepliesDeleted/Restored', t => {
state = reducer(commentState, Preview.setRepliesDeleted('id1'));
t.equal(state.replies.id1[0].visibility, 'deleted');
t.equal(state.replies.id1[1].visibility, 'deleted');
state = reducer(state, Preview.setRepliesRestored('id1'));
t.equal(state.replies.id1[0].visibility, 'visible');
t.equal(state.replies.id1[1].visibility, 'visible');
t.end();
});
tap.test('setCommentReported, top level comment', t => {
state = reducer(commentState, Preview.setCommentReported('id2'));
t.equal(state.comments[1].visibility, 'reported');
t.end();
});
tap.test('setCommentReported, reply comment', t => {
state = reducer(commentState, Preview.setCommentReported('id4', 'id1'));
t.equal(state.replies.id1[0].visibility, 'reported');
t.end();
});
tap.test('addNewComment, top level comment', t => {
state = reducer(commentState, Preview.addNewComment({id: 'new comment'}));
// Adds comment to beginning of list
t.equal(state.comments[0].id, 'new comment');
t.end();
});
tap.test('addNewComment, reply comment', t => {
state = reducer(commentState, Preview.addNewComment({id: 'new comment'}, 'id1'));
// Adds replies to the end of the replies list
t.equal(state.replies.id1[2].id, 'new comment');
t.end();
});
tap.test('setReplies', t => {
// setReplies should append new replies
state = reducer(commentState, Preview.setReplies({
id1: {id: 'id6'}
}));
t.equal(state.replies.id1[2].id, 'id6');
t.equal(state.comments[0].moreRepliesToLoad, false);
// setReplies should ignore duplicates, do the same as above again
t.equal(state.replies.id1.length, 3);
state = reducer(state, Preview.setReplies({id1: {id: 'id6'}}));
t.equal(state.replies.id1.length, 3);
// setReplies can add replies to a comment that didn't have any
state = reducer(state, Preview.setReplies({
id2: {id: 'id7'}
}));
t.equal(state.replies.id1.length, 3);
t.equal(state.replies.id2.length, 1);
t.equal(state.replies.id2[0].id, 'id7');
t.equal(state.comments[0].moreRepliesToLoad, false);
t.equal(state.comments[1].moreRepliesToLoad, false);
// Getting 20 (COMMENT_LIMIT) replies sets moreRepliesToLoad to true
state = reducer(state, Preview.setReplies({
id3: (new Array(20)).map((_, i) => ({id: `id${i + 1}`}))
}));
t.equal(state.comments[0].moreRepliesToLoad, false);
t.equal(state.comments[1].moreRepliesToLoad, false);
t.equal(state.comments[2].moreRepliesToLoad, true);
// Getting one more reply sets moreRepliesToLoad back to false
state = reducer(state, Preview.setReplies({
id3: {id: 'id21'}
}));
t.equal(state.comments[2].moreRepliesToLoad, false);
t.end();
});

View file

@ -0,0 +1,46 @@
import {
getInitialState, selectIsAdmin, selectIsSocial, selectUserId,
selectUsername, selectToken, sessionReducer, setSession
} from '../../../src/redux/session';
import {sessions} from '../../helpers/state-fixtures.json';
describe('session selectors', () => {
test('logged out', () => {
const state = {session: getInitialState()};
expect(selectIsAdmin(state)).toBe(false);
expect(selectIsSocial(state)).toBe(false);
expect(selectUserId(state)).toBeNaN();
expect(selectToken(state)).toBeNull();
expect(selectUsername(state)).toBeNull();
});
test('user data', () => {
let state = {session: getInitialState()};
const newSession = sessions.user1.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectUserId(state)).toBe(1);
expect(selectUsername(state)).toBe('user1-username');
expect(selectToken(state)).toBe('user1-token');
});
describe('permissions', () => {
test('selectIsAdmin', () => {
let state = {session: getInitialState()};
const newSession = sessions.user1Admin.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectIsAdmin(state)).toBe(true);
// Confirm that admin/social are totally separate and just read directly from the state
expect(selectIsSocial(state)).toBe(false);
});
test('selectIsSocial', () => {
let state = {session: getInitialState()};
const newSession = sessions.user1Social.session;
state.session = sessionReducer(state.session, setSession(newSession));
expect(selectIsSocial(state)).toBe(true);
// Confirm that admin/social are totally separate and just read directly from the state
expect(selectIsAdmin(state)).toBe(false);
});
});
});

View file

@ -0,0 +1,89 @@
import {
getInitialState as getInitialStudioState,
selectCanEditInfo,
selectCanAddProjects,
selectShowCommentComposer
} from '../../../src/redux/studio';
import {
getInitialState as getInitialSessionState
} from '../../../src/redux/session';
import {sessions, studios} from '../../helpers/state-fixtures.json';
describe('studio selectors', () => {
let state;
beforeEach(() => {
state = {
session: getInitialSessionState(),
studio: getInitialStudioState()
};
});
describe('studio info', () => {
test('is editable by admin', () => {
state.session = sessions.user1Admin;
expect(selectCanEditInfo(state)).toBe(true);
});
test('is editable by managers and studio creator', () => {
state.studio = studios.isManager;
expect(selectCanEditInfo(state)).toBe(true);
state.studio = studios.creator1;
state.session = sessions.user1;
expect(selectCanEditInfo(state)).toBe(true);
});
test('is not editable by curators', () => {
state.studio = studios.isCurator;
state.session = sessions.user1;
expect(selectCanEditInfo(state)).toBe(false);
});
test('is not editable by other logged in users', () => {
state.session = sessions.user1;
expect(selectCanEditInfo(state)).toBe(false);
});
test('is not editable by logged out users', () => {
expect(selectCanEditInfo(state)).toBe(false);
});
});
describe('studio projects', () => {
test('cannot be added by admin', () => {
state.session = sessions.user1Admin;
expect(selectCanAddProjects(state)).toBe(false);
});
test('can be added by managers and studio creator', () => {
state.studio = studios.isManager;
expect(selectCanAddProjects(state)).toBe(true);
state.studio = studios.creator1;
state.session = sessions.user1;
expect(selectCanAddProjects(state)).toBe(true);
});
test('can be added by curators', () => {
state.studio = studios.isCurator;
state.session = sessions.user1;
expect(selectCanAddProjects(state)).toBe(true);
});
test('can be added by social users if studio is openToAll', () => {
state.studio = studios.openToAll;
state.session = sessions.user1Social;
expect(selectCanAddProjects(state)).toBe(true);
});
test('cannot be added by social users if not openToAll', () => {
state.session = sessions.user1Social;
expect(selectCanAddProjects(state)).toBe(false);
});
});
describe('studio comments', () => {
test('show comment composer only for social users', () => {
expect(selectShowCommentComposer(state)).toBe(false);
state.session = sessions.user1;
expect(selectShowCommentComposer(state)).toBe(false);
state.session = sessions.user1Social;
expect(selectShowCommentComposer(state)).toBe(true);
});
});
});