mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 23:57:55 -05:00
Merge pull request #5340 from LLK/release/2021-05-05
[Master] Release 2021-05-05
This commit is contained in:
commit
b6b78ee533
30 changed files with 1913 additions and 454 deletions
|
@ -114,6 +114,16 @@ aliases:
|
|||
npm run test:smoke:convertReportToXunit
|
||||
- store_test_results:
|
||||
path: test/results
|
||||
- &update-translations
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: "setup"
|
||||
command: npm --production=false ci
|
||||
- run:
|
||||
name: "run i18n script"
|
||||
command: npm run i18n:push
|
||||
|
||||
jobs:
|
||||
build-staging:
|
||||
|
@ -132,6 +142,8 @@ jobs:
|
|||
<<: *integration_jest
|
||||
integration-production-tap:
|
||||
<<: *integration_tap
|
||||
update-translations:
|
||||
<<: *update-translations
|
||||
|
||||
workflows:
|
||||
build-test-deploy:
|
||||
|
@ -220,3 +232,19 @@ workflows:
|
|||
branches:
|
||||
only:
|
||||
- master
|
||||
Update-translations:
|
||||
triggers:
|
||||
- schedule: # every evening at 7pm EST (8pm EDT, Midnight UTC)
|
||||
cron: "0 0 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only: develop
|
||||
jobs:
|
||||
- update-translations:
|
||||
context:
|
||||
- scratch-www-all
|
||||
- scratch-www-staging
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
|
|
357
package-lock.json
generated
357
package-lock.json
generated
|
@ -219,26 +219,26 @@
|
|||
}
|
||||
},
|
||||
"@babel/compat-data": {
|
||||
"version": "7.13.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.15.tgz",
|
||||
"integrity": "sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz",
|
||||
"integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.16.tgz",
|
||||
"integrity": "sha512-sXHpixBiWWFti0AV2Zq7avpTasr6sIAu7Y396c608541qAU2ui4a193m0KSQmfPSKFZLnQ3cvlKDOm3XkuXm3Q==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.0.tgz",
|
||||
"integrity": "sha512-8YqpRig5NmIHlMLw09zMlPTvUVMILjqCOtVgu+TVNWEBvy9b5I3RRyhqnrV4hjgEK7n8P9OqvkWJAFmEL6Wwfw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.13.16",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-compilation-targets": "^7.13.16",
|
||||
"@babel/helper-module-transforms": "^7.13.14",
|
||||
"@babel/helpers": "^7.13.16",
|
||||
"@babel/parser": "^7.13.16",
|
||||
"@babel/helper-module-transforms": "^7.14.0",
|
||||
"@babel/helpers": "^7.14.0",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/template": "^7.12.13",
|
||||
"@babel/traverse": "^7.13.15",
|
||||
"@babel/types": "^7.13.16",
|
||||
"@babel/traverse": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
|
@ -257,12 +257,12 @@
|
|||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.16.tgz",
|
||||
"integrity": "sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz",
|
||||
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.13.16",
|
||||
"@babel/types": "^7.14.1",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
|
@ -297,20 +297,20 @@
|
|||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz",
|
||||
"integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.16.tgz",
|
||||
"integrity": "sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
|
@ -325,28 +325,28 @@
|
|||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.17.tgz",
|
||||
"integrity": "sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
|
||||
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.13.16",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.13.16",
|
||||
"@babel/types": "^7.13.17",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -490,22 +490,22 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"browserslist": {
|
||||
"version": "4.16.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.5.tgz",
|
||||
"integrity": "sha512-C2HAjrM1AI/djrpAUU/tr4pml1DqLIzJKSLDBXBrNErl9ZCCTXdhwxdJjYc16953+mBWf7Lw+uUJgpgb8cN71A==",
|
||||
"version": "4.16.6",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
|
||||
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001214",
|
||||
"caniuse-lite": "^1.0.30001219",
|
||||
"colorette": "^1.2.2",
|
||||
"electron-to-chromium": "^1.3.719",
|
||||
"electron-to-chromium": "^1.3.723",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^1.1.71"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.722",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.722.tgz",
|
||||
"integrity": "sha512-aAsc906l0RBsVTsGTK+KirVfey9eNtxyejdkbNzkISGxb7AFna3Kf0qvsp8tMttzBt9Bz3HddtYQ+++/PZtRYA==",
|
||||
"version": "1.3.726",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.726.tgz",
|
||||
"integrity": "sha512-dw7WmrSu/JwtACiBzth8cuKf62NKL1xVJuNvyOg0jvruN/n4NLtGYoTzciQquCPNaS2eR+BT5GrxHbslfc/w1w==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
|
@ -546,12 +546,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -573,12 +573,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -591,19 +591,19 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-module-transforms": {
|
||||
"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==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz",
|
||||
"integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.13.12",
|
||||
"@babel/helper-replace-supers": "^7.13.12",
|
||||
"@babel/helper-simple-access": "^7.13.12",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"@babel/template": "^7.12.13",
|
||||
"@babel/traverse": "^7.13.13",
|
||||
"@babel/types": "^7.13.14"
|
||||
"@babel/traverse": "^7.14.0",
|
||||
"@babel/types": "^7.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
|
@ -616,12 +616,12 @@
|
|||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.16.tgz",
|
||||
"integrity": "sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz",
|
||||
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.13.16",
|
||||
"@babel/types": "^7.14.1",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
|
@ -656,20 +656,20 @@
|
|||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz",
|
||||
"integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.16.tgz",
|
||||
"integrity": "sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
|
@ -684,28 +684,28 @@
|
|||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.17.tgz",
|
||||
"integrity": "sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
|
||||
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.13.16",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.13.16",
|
||||
"@babel/types": "^7.13.17",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -789,12 +789,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -834,12 +834,12 @@
|
|||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.16.tgz",
|
||||
"integrity": "sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz",
|
||||
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.13.16",
|
||||
"@babel/types": "^7.14.1",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
|
@ -874,20 +874,20 @@
|
|||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz",
|
||||
"integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.16.tgz",
|
||||
"integrity": "sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
|
@ -902,28 +902,28 @@
|
|||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.17.tgz",
|
||||
"integrity": "sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
|
||||
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.13.16",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.13.16",
|
||||
"@babel/types": "^7.13.17",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -1007,12 +1007,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -1034,9 +1034,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
|
||||
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
|
@ -1046,14 +1046,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"@babel/helpers": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.17.tgz",
|
||||
"integrity": "sha512-Eal4Gce4kGijo1/TGJdqp3WuhllaMLSrW6XcL0ulyUAQOuxHcCafZE8KHg9857gcTehsm/v7RcOx2+jp0Ryjsg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz",
|
||||
"integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "^7.12.13",
|
||||
"@babel/traverse": "^7.13.17",
|
||||
"@babel/types": "^7.13.17"
|
||||
"@babel/traverse": "^7.14.0",
|
||||
"@babel/types": "^7.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
|
@ -1066,12 +1066,12 @@
|
|||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.16.tgz",
|
||||
"integrity": "sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz",
|
||||
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.13.16",
|
||||
"@babel/types": "^7.14.1",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
|
@ -1106,20 +1106,20 @@
|
|||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz",
|
||||
"integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.16.tgz",
|
||||
"integrity": "sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
|
@ -1134,28 +1134,28 @@
|
|||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.17.tgz",
|
||||
"integrity": "sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==",
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
|
||||
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.13.16",
|
||||
"@babel/generator": "^7.14.0",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.13.16",
|
||||
"@babel/types": "^7.13.17",
|
||||
"@babel/parser": "^7.14.0",
|
||||
"@babel/types": "^7.14.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -1369,9 +1369,9 @@
|
|||
}
|
||||
},
|
||||
"@formatjs/ecma402-abstract": {
|
||||
"version": "1.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.6.5.tgz",
|
||||
"integrity": "sha512-dhRWSoPPw8PhB5tSOEP9Gi5XZNFC2IkfP95Va70ouIuED0wBlsU1WmO4jDHITL7/kSNqvzKFTT+2S+6jHPq6jw==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.0.tgz",
|
||||
"integrity": "sha512-0IQF4oDZdO8ruyrNJZuRle3F/YiGgRwTNrZyMI1N1X8GERZusOrXU9Stw+j/lyyfDWaJK44b+Qnri/qfLPCtGQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
|
@ -1404,17 +1404,26 @@
|
|||
}
|
||||
},
|
||||
"@formatjs/intl-locale": {
|
||||
"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==",
|
||||
"version": "2.4.23",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.23.tgz",
|
||||
"integrity": "sha512-5G0SjOsVxmX79dPaYk6KWxtdQ18UNK+E2JtAXvGxP8rSMqbJ/7cpDg95CU+YBXVKn6pRWefwqBsbjT5l+kK3yQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.6.5",
|
||||
"@formatjs/ecma402-abstract": "1.7.0",
|
||||
"@formatjs/intl-getcanonicallocales": "1.5.9",
|
||||
"cldr-core": "38",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.0.tgz",
|
||||
"integrity": "sha512-0IQF4oDZdO8ruyrNJZuRle3F/YiGgRwTNrZyMI1N1X8GERZusOrXU9Stw+j/lyyfDWaJK44b+Qnri/qfLPCtGQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
|
@ -1424,12 +1433,12 @@
|
|||
}
|
||||
},
|
||||
"@formatjs/intl-pluralrules": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.14.tgz",
|
||||
"integrity": "sha512-smfNDGaVZwFqKKqQlJfuKO45+RJs+UqyMD/8OJpIRpnYSss29T6T4jM5jzI9ZcbuTc79axI4no8wyViLBuGFMQ==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.17.tgz",
|
||||
"integrity": "sha512-SOX7lRrM1DQXZSzqoCztT+Gc6lnnoqGAVrX/YivQ7J8miPfxN8vgZhZN0MiEGPSLV0HJdO+AA+Xu0hUNJPlpnQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.6.5",
|
||||
"@formatjs/ecma402-abstract": "1.7.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -1442,12 +1451,12 @@
|
|||
}
|
||||
},
|
||||
"@formatjs/intl-relativetimeformat": {
|
||||
"version": "8.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.1.5.tgz",
|
||||
"integrity": "sha512-vCZ54nn2sPfIie7KQwbwT2xjRurvdGfJ036wPQFnOpICdqe8/vFKaS/kYRSw42fREGh1sefVcH0DZAz/GZczyw==",
|
||||
"version": "8.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.1.7.tgz",
|
||||
"integrity": "sha512-S8OoqOwiSqU/T0umkjB2lZcyWS4Dh8sWd4m8V+MsSnraeijNho/wSXQhJAuHx0b2Xvnlddt7a5RwyUnLADUepA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.6.5",
|
||||
"@formatjs/ecma402-abstract": "1.7.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -4786,9 +4795,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001218",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001218.tgz",
|
||||
"integrity": "sha512-0ASydOWSy3bB88FbDpJSTt+PfDwnMqrym3yRZfqG8EXSQ06OZhF+q5wgYP/EN+jJMERItNcDQUqMyNjzZ+r5+Q==",
|
||||
"version": "1.0.30001220",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001220.tgz",
|
||||
"integrity": "sha512-pjC2T4DIDyGAKTL4dMvGUQaMUHRmhvPpAgNNTa14jaBWHu+bLQgvpFqElxh9L4829Fdx0PlKiMp3wnYldRtECA==",
|
||||
"dev": true
|
||||
},
|
||||
"canvas-fit": {
|
||||
|
@ -20846,9 +20855,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-blocks": {
|
||||
"version": "0.1.0-prerelease.20210425032411",
|
||||
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210425032411.tgz",
|
||||
"integrity": "sha512-9z0biJJYYIQSNyvItH1Ys2V8KSlTrqZrjlb11ei26Z1tNg0dbGpDxj6fNxKbOz911kinRdUvsvBksDx42JS3Rg==",
|
||||
"version": "0.1.0-prerelease.20210505033858",
|
||||
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210505033858.tgz",
|
||||
"integrity": "sha512-qsHsxVsXvB7cZnoLirkl+HRjj+X7VcJh9Rh7UX8JoMEJsfUfTRJfo3wSt7zv4iKx6EK/QIStQNs9AACSPVaA7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exports-loader": "0.6.3",
|
||||
|
@ -20856,9 +20865,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-gui": {
|
||||
"version": "0.1.0-prerelease.20210427222734",
|
||||
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210427222734.tgz",
|
||||
"integrity": "sha512-xQsm1PEM5nK+0FK5fILEUiqMMYwb59q1fE5TE/tgUFzHXZwZzgqUq++Uvofax97EvbdxUPGLnDZ8rR86fGMARA==",
|
||||
"version": "0.1.0-prerelease.20210505040706",
|
||||
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210505040706.tgz",
|
||||
"integrity": "sha512-KyHm41UAlIKomysMzXqKMkibs6WsibC2jCX/AbsYR47UNtFJLhjvUyJKv05EbGo9GAr/b1uRglTRtiXA8SWsvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arraybuffer-loader": "^1.0.6",
|
||||
|
@ -20909,8 +20918,8 @@
|
|||
"redux": "3.7.2",
|
||||
"redux-throttle": "0.1.1",
|
||||
"scratch-audio": "0.1.0-prerelease.20200528195344",
|
||||
"scratch-blocks": "0.1.0-prerelease.20210425032411",
|
||||
"scratch-l10n": "3.11.20210425031513",
|
||||
"scratch-blocks": "0.1.0-prerelease.20210505033858",
|
||||
"scratch-l10n": "3.11.20210504031549",
|
||||
"scratch-paint": "0.2.0-prerelease.20210407203313",
|
||||
"scratch-render": "0.1.0-prerelease.20210325231800",
|
||||
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
|
||||
|
@ -20956,14 +20965,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.16.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.5.tgz",
|
||||
"integrity": "sha512-C2HAjrM1AI/djrpAUU/tr4pml1DqLIzJKSLDBXBrNErl9ZCCTXdhwxdJjYc16953+mBWf7Lw+uUJgpgb8cN71A==",
|
||||
"version": "4.16.6",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
|
||||
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001214",
|
||||
"caniuse-lite": "^1.0.30001219",
|
||||
"colorette": "^1.2.2",
|
||||
"electron-to-chromium": "^1.3.719",
|
||||
"electron-to-chromium": "^1.3.723",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^1.1.71"
|
||||
}
|
||||
|
@ -21079,9 +21088,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.723",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.723.tgz",
|
||||
"integrity": "sha512-L+WXyXI7c7+G1V8ANzRsPI5giiimLAUDC6Zs1ojHHPhYXb3k/iTABFmWjivEtsWrRQymjnO66/rO2ZTABGdmWg==",
|
||||
"version": "1.3.727",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz",
|
||||
"integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
|
@ -21302,9 +21311,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"scratch-l10n": {
|
||||
"version": "3.11.20210425031513",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210425031513.tgz",
|
||||
"integrity": "sha512-SX26P8zrUTIWyP7Q/1eojJ1e3t8HiNcZ/fTLHBz4Sjkba7ZkqwKq9T9UopX9KstaQJcAekwCyfhIoO1Ad+60kQ==",
|
||||
"version": "3.11.20210504031549",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210504031549.tgz",
|
||||
"integrity": "sha512-R6CfX0p8LegrsqKm5s+8cSlvzg8Ijma4zGOet22yVtJbtSEcmFmi3LbwV5cKKPjoDWobZMYUhrazmlBLswKPQw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/cli": "^7.1.2",
|
||||
|
@ -21391,9 +21400,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-l10n": {
|
||||
"version": "3.11.20210428031542",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210428031542.tgz",
|
||||
"integrity": "sha512-NV6iB2baj/NBVsYOjpLg2FNi/znX8YYNVKT/nu2LoDUxmGwhCA1vdBg93w+lnRv/7mcvJXu7TQVLnU8Dp1ESag==",
|
||||
"version": "3.11.20210505031459",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210505031459.tgz",
|
||||
"integrity": "sha512-1geGseNJ95UIyKaOWYIkdE0V2mSUxcrGR/wUE86727FqwYdo19nJyaXvzcv2mWWGdvDu3eV7ei3raD1ZXAniSA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/cli": "^7.1.2",
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
"redux-mock-store": "^1.2.3",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20210427222734",
|
||||
"scratch-gui": "0.1.0-prerelease.20210505040706",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const {selectUserId, selectIsAdmin, selectIsSocial, selectIsLoggedIn} = require('./session');
|
||||
const {selectUserId, selectIsAdmin, selectIsSocial, selectIsLoggedIn, selectUsername} = require('./session');
|
||||
|
||||
// Fine-grain selector helpers - not exported, use the higher level selectors below
|
||||
const isCreator = state => selectUserId(state) === state.studio.owner;
|
||||
|
@ -29,6 +29,27 @@ const selectCanFollowStudio = state => selectIsLoggedIn(state);
|
|||
const selectCanEditCommentsAllowed = state => selectIsAdmin(state) || isCreator(state);
|
||||
const selectCanEditOpenToAll = state => isManager(state);
|
||||
|
||||
const selectShowCuratorInvite = state => !!state.studio.invited;
|
||||
const selectCanInviteCurators = state => isManager(state);
|
||||
const selectCanRemoveCurators = state => isManager(state) || selectIsAdmin(state);
|
||||
const selectCanRemoveManager = (state, managerId) =>
|
||||
(selectIsAdmin(state) || isManager(state)) && managerId !== state.studio.owner;
|
||||
const selectCanPromoteCurators = state => isManager(state);
|
||||
|
||||
const selectCanRemoveProject = (state, creatorUsername, actorId) => {
|
||||
// Admins/managers can remove any projects
|
||||
if (isManager(state) || selectIsAdmin(state)) return true;
|
||||
// Project owners can always remove their projects
|
||||
if (selectUsername(state) === creatorUsername) {
|
||||
return true;
|
||||
}
|
||||
// Curators can remove projects they added
|
||||
if (isCurator(state)) {
|
||||
return selectUserId(state) === actorId;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export {
|
||||
selectCanEditInfo,
|
||||
selectCanAddProjects,
|
||||
|
@ -39,5 +60,11 @@ export {
|
|||
selectCanReportComment,
|
||||
selectCanRestoreComment,
|
||||
selectCanEditCommentsAllowed,
|
||||
selectCanEditOpenToAll
|
||||
selectCanEditOpenToAll,
|
||||
selectShowCuratorInvite,
|
||||
selectCanInviteCurators,
|
||||
selectCanRemoveCurators,
|
||||
selectCanRemoveManager,
|
||||
selectCanPromoteCurators,
|
||||
selectCanRemoveProject
|
||||
};
|
||||
|
|
137
src/redux/studio-report.js
Normal file
137
src/redux/studio-report.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
const keyMirror = require('keymirror');
|
||||
|
||||
const api = require('../lib/api');
|
||||
const {selectIsLoggedIn} = require('./session');
|
||||
const {selectStudioId} = require('./studio');
|
||||
|
||||
const Actions = keyMirror({
|
||||
OPEN_STUDIO_REPORT: null,
|
||||
CLOSE_STUDIO_REPORT: null,
|
||||
SET_STUDIO_REPORT_STATUS: null,
|
||||
SET_STUDIO_REPORT_FIELD: null
|
||||
});
|
||||
|
||||
const Status = keyMirror({
|
||||
IDLE: null,
|
||||
SUBMITTING: null,
|
||||
SUBMITTED: null
|
||||
});
|
||||
|
||||
const Fields = {
|
||||
TITLE: 'title',
|
||||
DESCRIPTION: 'description',
|
||||
THUMBNAIL: 'thumbnail'
|
||||
};
|
||||
|
||||
const Errors = keyMirror({
|
||||
GENERIC: null
|
||||
});
|
||||
|
||||
const getInitialState = () => ({
|
||||
status: Status.IDLE,
|
||||
field: Fields.TITLE,
|
||||
error: null,
|
||||
isOpen: false
|
||||
});
|
||||
|
||||
const studioReportReducer = (state, action) => {
|
||||
if (typeof state === 'undefined') {
|
||||
state = getInitialState();
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case Actions.OPEN_STUDIO_REPORT:
|
||||
return {
|
||||
...state,
|
||||
isOpen: true
|
||||
};
|
||||
case Actions.CLOSE_STUDIO_REPORT:
|
||||
return {
|
||||
...state, // Leaves the submitted status to prevent double submission
|
||||
isOpen: false
|
||||
};
|
||||
case Actions.SET_STUDIO_REPORT_STATUS:
|
||||
return {
|
||||
...state,
|
||||
status: action.status,
|
||||
error: typeof action.error === 'undefined' ? null : action.error
|
||||
};
|
||||
case Actions.SET_STUDIO_REPORT_FIELD:
|
||||
return {
|
||||
...state,
|
||||
field: action.field
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Selectors
|
||||
const selectStudioReportField = state => state.studioReport.field;
|
||||
const selectStudioReportOpen = state => state.studioReport.isOpen;
|
||||
const selectStudioReportSubmitting = state => state.studioReport.status === Status.SUBMITTING;
|
||||
const selectStudioReportSubmitted = state => state.studioReport.status === Status.SUBMITTED;
|
||||
const selectStudioReportError = state => state.studioReport.error;
|
||||
const selectCanReportStudio = state => !!selectIsLoggedIn(state); // TODO selectIsLoggedIn isn't returning bool?
|
||||
|
||||
// Action Creators
|
||||
const setReportStatus = (status, error) => ({
|
||||
type: Actions.SET_STUDIO_REPORT_STATUS,
|
||||
status,
|
||||
error
|
||||
});
|
||||
|
||||
const openStudioReport = () => ({
|
||||
type: Actions.OPEN_STUDIO_REPORT
|
||||
});
|
||||
|
||||
const closeStudioReport = () => ({
|
||||
type: Actions.CLOSE_STUDIO_REPORT
|
||||
});
|
||||
|
||||
const setStudioReportField = field => ({
|
||||
type: Actions.SET_STUDIO_REPORT_FIELD,
|
||||
field
|
||||
});
|
||||
|
||||
const submitStudioReport = () => ((dispatch, getState) => {
|
||||
dispatch(setReportStatus(Status.SUBMITTING));
|
||||
const studioId = selectStudioId(getState());
|
||||
const field = selectStudioReportField(getState());
|
||||
api({
|
||||
host: '',
|
||||
uri: `/site-api/galleries/all/${studioId}/report/`,
|
||||
method: 'POST',
|
||||
useCsrf: true,
|
||||
formData: {
|
||||
selected_field: field
|
||||
}
|
||||
}, (err, body, res) => {
|
||||
if (err || (body && body.success === false) || res.statusCode !== 200) {
|
||||
dispatch(setReportStatus(Status.IDLE, Errors.GENERIC));
|
||||
return;
|
||||
}
|
||||
dispatch(setReportStatus(Status.SUBMITTED));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
Errors,
|
||||
Fields,
|
||||
getInitialState,
|
||||
studioReportReducer,
|
||||
actions: {
|
||||
openStudioReport,
|
||||
closeStudioReport,
|
||||
setStudioReportField,
|
||||
submitStudioReport
|
||||
},
|
||||
selectors: {
|
||||
selectStudioReportField,
|
||||
selectStudioReportOpen,
|
||||
selectCanReportStudio,
|
||||
selectStudioReportSubmitting,
|
||||
selectStudioReportSubmitted,
|
||||
selectStudioReportError
|
||||
}
|
||||
};
|
|
@ -13,7 +13,7 @@ const Status = keyMirror({
|
|||
});
|
||||
|
||||
const getInitialState = () => ({
|
||||
infoStatus: Status.NOT_FETCHED,
|
||||
infoStatus: Status.FETCHING,
|
||||
title: '',
|
||||
description: '',
|
||||
openToAll: false,
|
||||
|
@ -38,12 +38,14 @@ const studioReducer = (state, action) => {
|
|||
case 'SET_INFO':
|
||||
return {
|
||||
...state,
|
||||
...action.info
|
||||
...action.info,
|
||||
infoStatus: Status.FETCHED
|
||||
};
|
||||
case 'SET_ROLES':
|
||||
return {
|
||||
...state,
|
||||
...action.roles
|
||||
...action.roles,
|
||||
rolesStatus: Status.FETCHED
|
||||
};
|
||||
case 'SET_FETCH_STATUS':
|
||||
if (action.error) {
|
||||
|
@ -95,14 +97,12 @@ const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCH
|
|||
|
||||
// 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));
|
||||
return;
|
||||
}
|
||||
dispatch(setFetchStatus('infoStatus', Status.FETCHED));
|
||||
dispatch(setInfo({
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
|
@ -130,7 +130,6 @@ const getRoles = () => ((dispatch, getState) => {
|
|||
dispatch(setFetchStatus('rolesStatus', Status.ERROR, err));
|
||||
return;
|
||||
}
|
||||
dispatch(setFetchStatus('rolesStatus', Status.FETCHED));
|
||||
dispatch(setRoles({
|
||||
manager: body.manager,
|
||||
curator: body.curator,
|
||||
|
@ -149,6 +148,7 @@ module.exports = {
|
|||
getInfo,
|
||||
getRoles,
|
||||
setInfo,
|
||||
setRoles,
|
||||
|
||||
// Selectors
|
||||
selectStudioId,
|
||||
|
|
|
@ -1,28 +1,9 @@
|
|||
const ITEM_LIMIT = 4;
|
||||
|
||||
const projectFetcher = (studioId, offset) =>
|
||||
fetch(`${process.env.API_HOST}/studios/${studioId}/projects?limit=${ITEM_LIMIT}&offset=${offset}`)
|
||||
.then(response => response.json())
|
||||
.then(data => ({items: data, moreToLoad: data.length === ITEM_LIMIT}));
|
||||
|
||||
const curatorFetcher = (studioId, offset) =>
|
||||
fetch(`${process.env.API_HOST}/studios/${studioId}/curators?limit=${ITEM_LIMIT}&offset=${offset}`)
|
||||
.then(response => response.json())
|
||||
.then(data => ({items: data, moreToLoad: data.length === ITEM_LIMIT}));
|
||||
|
||||
const managerFetcher = (studioId, offset) =>
|
||||
fetch(`${process.env.API_HOST}/studios/${studioId}/managers?limit=${ITEM_LIMIT}&offset=${offset}`)
|
||||
.then(response => response.json())
|
||||
.then(data => ({items: data, moreToLoad: data.length === ITEM_LIMIT}));
|
||||
|
||||
// TODO move this to studio-activity-actions, include pagination
|
||||
const activityFetcher = studioId =>
|
||||
fetch(`${process.env.API_HOST}/studios/${studioId}/activity`)
|
||||
.then(response => response.json())
|
||||
.then(data => ({items: data, moreToLoad: false})); // No pagination on the activity feed
|
||||
|
||||
export {
|
||||
activityFetcher,
|
||||
projectFetcher,
|
||||
curatorFetcher,
|
||||
managerFetcher
|
||||
activityFetcher
|
||||
};
|
||||
|
|
175
src/views/studio/lib/studio-member-actions.js
Normal file
175
src/views/studio/lib/studio-member-actions.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
import keyMirror from 'keymirror';
|
||||
|
||||
import api from '../../../lib/api';
|
||||
import {curators, managers} from './redux-modules';
|
||||
import {selectUsername} from '../../../redux/session';
|
||||
import {selectStudioId, setRoles} from '../../../redux/studio';
|
||||
|
||||
const Errors = keyMirror({
|
||||
NETWORK: null,
|
||||
SERVER: null,
|
||||
PERMISSION: null
|
||||
});
|
||||
|
||||
const normalizeError = (err, body, res) => {
|
||||
if (err) return Errors.NETWORK;
|
||||
if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
|
||||
if (res.statusCode !== 200) return Errors.SERVER;
|
||||
return null;
|
||||
};
|
||||
|
||||
const loadManagers = () => ((dispatch, getState) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
const managerCount = managers.selector(state).items.length;
|
||||
const managersPerPage = 20;
|
||||
api({
|
||||
uri: `/studios/${studioId}/managers/`,
|
||||
params: {limit: managersPerPage, offset: managerCount}
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return dispatch(managers.actions.error(error));
|
||||
dispatch(managers.actions.append(body, body.length === managersPerPage));
|
||||
});
|
||||
});
|
||||
|
||||
const loadCurators = () => ((dispatch, getState) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
const curatorCount = curators.selector(state).items.length;
|
||||
const curatorsPerPage = 20;
|
||||
api({
|
||||
uri: `/studios/${studioId}/curators/`,
|
||||
params: {limit: curatorsPerPage, offset: curatorCount}
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return dispatch(curators.actions.error(error));
|
||||
dispatch(curators.actions.append(body, body.length === curatorsPerPage));
|
||||
});
|
||||
});
|
||||
|
||||
const removeManager = username => ((dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
api({
|
||||
uri: `/site-api/users/curators-in/${studioId}/remove/`,
|
||||
method: 'PUT',
|
||||
withCredentials: true,
|
||||
useCsrf: true,
|
||||
params: {usernames: username}, // sic, ?usernames=<username>
|
||||
host: '' // Not handled by the API, use existing infrastructure
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return reject(error);
|
||||
|
||||
// Note `body` is undefined, this endpoint returns an html fragment
|
||||
const index = managers.selector(getState()).items
|
||||
.findIndex(v => v.username === username);
|
||||
if (index !== -1) dispatch(managers.actions.remove(index));
|
||||
// If you are removing yourself, update roles so you stop seeing the manager UI
|
||||
if (selectUsername(state) === username) {
|
||||
dispatch(setRoles({manager: false}));
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
}));
|
||||
|
||||
const removeCurator = username => ((dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
api({
|
||||
uri: `/site-api/users/curators-in/${studioId}/remove/`,
|
||||
method: 'PUT',
|
||||
withCredentials: true,
|
||||
useCsrf: true,
|
||||
params: {usernames: username}, // sic, ?usernames=<username>
|
||||
host: '' // Not handled by the API, use existing infrastructure
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return reject(error);
|
||||
|
||||
// Note `body` is undefined, this endpoint returns an html fragment
|
||||
const index = curators.selector(getState()).items
|
||||
.findIndex(v => v.username === username);
|
||||
if (index !== -1) dispatch(curators.actions.remove(index));
|
||||
return resolve();
|
||||
});
|
||||
}));
|
||||
|
||||
const inviteCurator = username => ((dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
api({
|
||||
uri: `/site-api/users/curators-in/${studioId}/invite_curator/`,
|
||||
method: 'PUT',
|
||||
withCredentials: true,
|
||||
useCsrf: true,
|
||||
params: {usernames: username}, // sic, ?usernames=<username>
|
||||
host: '' // Not handled by the API, use existing infrastructure
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return reject(error);
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(`successfully invited ${username}`);
|
||||
return resolve(username);
|
||||
});
|
||||
}));
|
||||
|
||||
const promoteCurator = username => ((dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
api({
|
||||
uri: `/site-api/users/curators-in/${studioId}/promote/`,
|
||||
method: 'PUT',
|
||||
withCredentials: true,
|
||||
useCsrf: true,
|
||||
params: {usernames: username}, // sic, ?usernames=<username>
|
||||
host: '' // Not handled by the API, use existing infrastructure
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return reject(error);
|
||||
const curatorList = curators.selector(getState()).items;
|
||||
const index = curatorList.findIndex(v => v.username === username);
|
||||
const curatorItem = curatorList[index];
|
||||
if (index !== -1) dispatch(curators.actions.remove(index));
|
||||
dispatch(managers.actions.create(curatorItem));
|
||||
return resolve();
|
||||
});
|
||||
}));
|
||||
|
||||
const acceptInvitation = () => ((dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const username = selectUsername(state);
|
||||
const studioId = selectStudioId(state);
|
||||
api({
|
||||
uri: `/site-api/users/curators-in/${studioId}/add/`,
|
||||
method: 'PUT',
|
||||
withCredentials: true,
|
||||
useCsrf: true,
|
||||
params: {usernames: username}, // sic, ?usernames=<username>
|
||||
host: '' // Not handled by the API, use existing infrastructure
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return reject(error);
|
||||
api({uri: `/users/${username}`}, (userErr, userBody, userRes) => {
|
||||
const userError = normalizeError(userErr, userBody, userRes);
|
||||
if (userError) return reject(userError);
|
||||
// Note: this assumes that the user items from the curator endpoint
|
||||
// are the same structure as the single user data returned from /users/:username
|
||||
dispatch(curators.actions.create(userBody));
|
||||
dispatch(setRoles({invited: false, curator: true}));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
export {
|
||||
Errors,
|
||||
loadManagers,
|
||||
loadCurators,
|
||||
inviteCurator,
|
||||
acceptInvitation,
|
||||
promoteCurator,
|
||||
removeCurator,
|
||||
removeManager
|
||||
};
|
105
src/views/studio/lib/studio-project-actions.js
Normal file
105
src/views/studio/lib/studio-project-actions.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
import keyMirror from 'keymirror';
|
||||
import api from '../../../lib/api';
|
||||
|
||||
import {selectToken} from '../../../redux/session';
|
||||
import {selectStudioId} from '../../../redux/studio';
|
||||
|
||||
import {projects} from './redux-modules';
|
||||
|
||||
const Errors = keyMirror({
|
||||
NETWORK: null,
|
||||
SERVER: null,
|
||||
PERMISSION: null
|
||||
});
|
||||
|
||||
const normalizeError = (err, body, res) => {
|
||||
if (err) return Errors.NETWORK;
|
||||
if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
|
||||
if (res.statusCode !== 200) return Errors.SERVER;
|
||||
return null;
|
||||
};
|
||||
|
||||
const loadProjects = () => ((dispatch, getState) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
const projectCount = projects.selector(state).items.length;
|
||||
const projectsPerPage = 20;
|
||||
api({
|
||||
uri: `/studios/${studioId}/projects/`,
|
||||
params: {limit: projectsPerPage, offset: projectCount}
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return dispatch(projects.actions.error(error));
|
||||
dispatch(projects.actions.append(body, body.length === projectsPerPage));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate a project list item matching the shape of the initial
|
||||
* project list request. The POST request that adds projects would
|
||||
* ideally respond with this format directly. For now, merge data
|
||||
* from the POST and a follow-up GET request for additional project data.
|
||||
*
|
||||
* @param {object} postBody - body of response to POST that adds the project
|
||||
* @param {object} infoBody - body of the follow-up GET for more project data.
|
||||
* @returns {object} project list item
|
||||
*/
|
||||
const generateProjectListItem = (postBody, infoBody) => ({
|
||||
// Fields from the POST to add the project to the studio
|
||||
id: postBody.projectId,
|
||||
actor_id: postBody.actorId,
|
||||
// Fields from followup GET for more project info
|
||||
title: infoBody.title,
|
||||
image: infoBody.image,
|
||||
creator_id: infoBody.author.id,
|
||||
username: infoBody.author.username,
|
||||
avatar: infoBody.author.profile.images
|
||||
});
|
||||
|
||||
const addProject = projectId => ((dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
const token = selectToken(state);
|
||||
api({
|
||||
uri: `/studios/${studioId}/project/${projectId}`,
|
||||
method: 'POST',
|
||||
authentication: token
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return reject(error);
|
||||
|
||||
// Would prefer if the POST returned the exact data / format we want...
|
||||
api({uri: `/projects/${projectId}`}, (infoErr, infoBody, infoRes) => {
|
||||
const infoError = normalizeError(infoErr, infoBody, infoRes);
|
||||
if (infoError) return reject(infoError);
|
||||
const newItem = generateProjectListItem(body, infoBody);
|
||||
dispatch(projects.actions.create(newItem));
|
||||
return resolve(newItem);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
const removeProject = projectId => ((dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const studioId = selectStudioId(state);
|
||||
const token = selectToken(state);
|
||||
api({
|
||||
uri: `/studios/${studioId}/project/${projectId}`,
|
||||
method: 'DELETE',
|
||||
authentication: token
|
||||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return reject(error);
|
||||
const index = projects.selector(getState()).items
|
||||
.findIndex(v => v.id === projectId);
|
||||
if (index !== -1) dispatch(projects.actions.remove(index));
|
||||
return resolve();
|
||||
});
|
||||
}));
|
||||
|
||||
export {
|
||||
Errors,
|
||||
loadProjects,
|
||||
addProject,
|
||||
removeProject
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
@ -9,6 +9,7 @@ import TopLevelComment from '../preview/comment/top-level-comment.jsx';
|
|||
import studioCommentActions from '../../redux/studio-comment-actions.js';
|
||||
import StudioCommentsAllowed from './studio-comments-allowed.jsx';
|
||||
|
||||
import {selectIsAdmin} from '../../redux/session';
|
||||
import {
|
||||
selectShowCommentComposer,
|
||||
selectCanDeleteComment,
|
||||
|
@ -22,6 +23,7 @@ import {selectStudioCommentsAllowed} from '../../redux/studio.js';
|
|||
const StudioComments = ({
|
||||
comments,
|
||||
commentsAllowed,
|
||||
isAdmin,
|
||||
handleLoadMoreComments,
|
||||
handleNewComment,
|
||||
moreCommentsToLoad,
|
||||
|
@ -35,12 +37,22 @@ const StudioComments = ({
|
|||
canRestoreComment,
|
||||
handleDeleteComment,
|
||||
handleRestoreComment,
|
||||
handleResetComments,
|
||||
handleReportComment,
|
||||
handleLoadMoreReplies
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (comments.length === 0) handleLoadMoreComments();
|
||||
}, []); // Only runs once after the first render
|
||||
}, [comments.length === 0]);
|
||||
|
||||
// The comments you see depend on your admin status
|
||||
// so reset them if isAdmin changes.
|
||||
const adminRef = useRef(isAdmin);
|
||||
useEffect(() => {
|
||||
const wasAdmin = adminRef.current;
|
||||
adminRef.current = isAdmin;
|
||||
if (isAdmin !== wasAdmin) handleResetComments();
|
||||
}, [isAdmin]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -93,6 +105,7 @@ const StudioComments = ({
|
|||
StudioComments.propTypes = {
|
||||
comments: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
commentsAllowed: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
handleLoadMoreComments: PropTypes.func,
|
||||
handleNewComment: PropTypes.func,
|
||||
moreCommentsToLoad: PropTypes.bool,
|
||||
|
@ -106,13 +119,19 @@ StudioComments.propTypes = {
|
|||
handleDeleteComment: PropTypes.func,
|
||||
handleRestoreComment: PropTypes.func,
|
||||
handleReportComment: PropTypes.func,
|
||||
handleResetComments: PropTypes.func,
|
||||
handleLoadMoreReplies: PropTypes.func,
|
||||
postURI: PropTypes.string
|
||||
};
|
||||
|
||||
export {
|
||||
StudioComments
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
comments: state.comments.comments,
|
||||
isAdmin: selectIsAdmin(state),
|
||||
moreCommentsToLoad: state.comments.moreCommentsToLoad,
|
||||
replies: state.comments.replies,
|
||||
commentsAllowed: selectStudioCommentsAllowed(state),
|
||||
|
@ -130,7 +149,7 @@ export default connect(
|
|||
handleDeleteComment: studioCommentActions.deleteComment,
|
||||
handleRestoreComment: studioCommentActions.restoreComment,
|
||||
handleReportComment: studioCommentActions.reportComment,
|
||||
handleLoadMoreReplies: studioCommentActions.getReplies
|
||||
|
||||
handleLoadMoreReplies: studioCommentActions.getReplies,
|
||||
handleResetComments: studioCommentActions.resetComments
|
||||
}
|
||||
)(StudioComments);
|
||||
|
|
45
src/views/studio/studio-curator-invite.jsx
Normal file
45
src/views/studio/studio-curator-invite.jsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {acceptInvitation} from './lib/studio-member-actions';
|
||||
|
||||
const StudioCuratorInvite = ({onSubmit}) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onSubmit()
|
||||
.catch(e => {
|
||||
setError(e);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}}
|
||||
>Accept invite</button>
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioCuratorInvite.propTypes = {
|
||||
onSubmit: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
const mapDispatchToProps = ({
|
||||
onSubmit: acceptInvitation
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StudioCuratorInvite);
|
53
src/views/studio/studio-curator-inviter.jsx
Normal file
53
src/views/studio/studio-curator-inviter.jsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {inviteCurator} from './lib/studio-member-actions';
|
||||
|
||||
const StudioCuratorInviter = ({onSubmit}) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="studio-adder-section">
|
||||
<h3>✦ Invite Curators</h3>
|
||||
<input
|
||||
disabled={submitting}
|
||||
type="text"
|
||||
placeholder="<username>"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onSubmit(value)
|
||||
.then(() => setValue(''))
|
||||
.catch(e => setError(e))
|
||||
.then(() => setSubmitting(false));
|
||||
}}
|
||||
>Invite</button>
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioCuratorInviter.propTypes = {
|
||||
onSubmit: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
const mapDispatchToProps = ({
|
||||
onSubmit: inviteCurator
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StudioCuratorInviter);
|
|
@ -1,77 +1,76 @@
|
|||
import React, {useEffect, useCallback} from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {useParams} from 'react-router-dom';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {curators, managers} from './lib/redux-modules';
|
||||
import {curatorFetcher, managerFetcher} from './lib/fetchers';
|
||||
import {curators} from './lib/redux-modules';
|
||||
import Debug from './debug.jsx';
|
||||
import {CuratorTile} from './studio-member-tile.jsx';
|
||||
import CuratorInviter from './studio-curator-inviter.jsx';
|
||||
import CuratorInvite from './studio-curator-invite.jsx';
|
||||
import {loadCurators} from './lib/studio-member-actions';
|
||||
import {selectCanInviteCurators, selectShowCuratorInvite} from '../../redux/studio-permissions';
|
||||
|
||||
const StudioCurators = () => {
|
||||
const {studioId} = useParams();
|
||||
return (
|
||||
<div>
|
||||
<h3>Managers</h3>
|
||||
<ManagerList studioId={studioId} />
|
||||
<hr />
|
||||
<h3>Curators</h3>
|
||||
<CuratorList studioId={studioId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MemberList = ({studioId, items, error, loading, moreToLoad, onLoadMore}) => {
|
||||
const StudioCurators = ({
|
||||
canInviteCurators, showCuratorInvite, items, error, loading, moreToLoad, onLoadMore
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (studioId && items.length === 0) onLoadMore(studioId, 0);
|
||||
}, [studioId]);
|
||||
|
||||
const handleLoadMore = useCallback(() => onLoadMore(studioId, items.length), [studioId, items.length]);
|
||||
if (items.length === 0) onLoadMore();
|
||||
}, []);
|
||||
|
||||
return (<React.Fragment>
|
||||
return (<div className="studio-members">
|
||||
<h2>Curators</h2>
|
||||
{canInviteCurators && <CuratorInviter />}
|
||||
{showCuratorInvite && <CuratorInvite />}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
{items.map((item, index) =>
|
||||
(<Debug
|
||||
label="Member"
|
||||
data={item}
|
||||
key={index}
|
||||
/>)
|
||||
)}
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={handleLoadMore}>
|
||||
<div className="studio-members-grid">
|
||||
{items.map(item =>
|
||||
(<CuratorTile
|
||||
key={item.username}
|
||||
username={item.username}
|
||||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
)}
|
||||
<div className="studio-members-load-more">
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={onLoadMore}>
|
||||
Load more
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</React.Fragment>);
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
|
||||
MemberList.propTypes = {
|
||||
studioId: PropTypes.string,
|
||||
items: PropTypes.array, // eslint-disable-line react/forbid-prop-types
|
||||
StudioCurators.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.id,
|
||||
username: PropTypes.string,
|
||||
profile: PropTypes.shape({
|
||||
images: PropTypes.shape({
|
||||
'90x90': PropTypes.string
|
||||
})
|
||||
})
|
||||
})),
|
||||
canInviteCurators: PropTypes.bool,
|
||||
showCuratorInvite: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
moreToLoad: PropTypes.bool,
|
||||
onLoadMore: PropTypes.func
|
||||
};
|
||||
|
||||
const ManagerList = connect(
|
||||
state => managers.selector(state),
|
||||
dispatch => ({
|
||||
onLoadMore: (studioId, offset) => dispatch(
|
||||
managers.actions.loadMore(managerFetcher.bind(null, studioId, offset)))
|
||||
})
|
||||
)(MemberList);
|
||||
|
||||
const CuratorList = connect(
|
||||
state => curators.selector(state),
|
||||
dispatch => ({
|
||||
onLoadMore: (studioId, offset) => dispatch(
|
||||
curators.actions.loadMore(curatorFetcher.bind(null, studioId, offset)))
|
||||
})
|
||||
)(MemberList);
|
||||
|
||||
export default StudioCurators;
|
||||
export default connect(
|
||||
state => ({
|
||||
...curators.selector(state),
|
||||
canInviteCurators: selectCanInviteCurators(state),
|
||||
showCuratorInvite: selectShowCuratorInvite(state)
|
||||
}),
|
||||
{
|
||||
onLoadMore: loadCurators
|
||||
}
|
||||
)(StudioCurators);
|
||||
|
|
|
@ -8,31 +8,29 @@ import {selectCanEditInfo} from '../../redux/studio-permissions';
|
|||
import {
|
||||
mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
|
||||
} from '../../redux/studio-mutations';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const StudioDescription = ({
|
||||
descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate
|
||||
}) => (
|
||||
<div>
|
||||
<h3>Description</h3>
|
||||
{isFetching ? (
|
||||
<h4>Fetching...</h4>
|
||||
) : (canEditInfo ? (
|
||||
<label>
|
||||
<textarea
|
||||
rows="5"
|
||||
cols="100"
|
||||
disabled={isMutating}
|
||||
defaultValue={description}
|
||||
onBlur={e => e.target.value !== description &&
|
||||
handleUpdate(e.target.value)}
|
||||
/>
|
||||
{descriptionError && <div>Error mutating description: {descriptionError}</div>}
|
||||
</label>
|
||||
) : (
|
||||
<div>{description}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const fieldClassName = classNames('studio-description', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<textarea
|
||||
rows="20"
|
||||
className={fieldClassName}
|
||||
disabled={isMutating || !canEditInfo || isFetching}
|
||||
defaultValue={description}
|
||||
onBlur={e => e.target.value !== description &&
|
||||
handleUpdate(e.target.value)}
|
||||
/>
|
||||
{descriptionError && <div>Error mutating description: {descriptionError}</div>}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
StudioDescription.propTypes = {
|
||||
descriptionError: PropTypes.string,
|
||||
|
|
|
@ -2,43 +2,42 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {selectIsFollowing, selectIsFetchingRoles} from '../../redux/studio';
|
||||
import {selectIsFollowing} from '../../redux/studio';
|
||||
import {selectCanFollowStudio} from '../../redux/studio-permissions';
|
||||
import {
|
||||
mutateFollowingStudio, selectIsMutatingFollowing, selectFollowingMutationError
|
||||
} from '../../redux/studio-mutations';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
const StudioFollow = ({
|
||||
canFollow,
|
||||
isFetching,
|
||||
isFollowing,
|
||||
isMutating,
|
||||
followingError,
|
||||
handleFollow
|
||||
}) => (
|
||||
<div>
|
||||
<h3>Following</h3>
|
||||
<div>
|
||||
}) => {
|
||||
if (!canFollow) return null;
|
||||
const fieldClassName = classNames('button', {
|
||||
'mod-mutating': isMutating
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<button
|
||||
disabled={isFetching || isMutating || !canFollow}
|
||||
className={fieldClassName}
|
||||
disabled={isMutating}
|
||||
onClick={() => handleFollow(!isFollowing)}
|
||||
>
|
||||
{isFetching ? (
|
||||
'Fetching...'
|
||||
) : (
|
||||
isFollowing ? 'Unfollow' : 'Follow'
|
||||
{isMutating ? '...' : (
|
||||
isFollowing ? 'Unfollow Studio' : 'Follow Studio'
|
||||
)}
|
||||
</button>
|
||||
{followingError && <div>Error mutating following: {followingError}</div>}
|
||||
{!canFollow && <div>Must be logged in to follow</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</React.Fragment >
|
||||
);
|
||||
};
|
||||
|
||||
StudioFollow.propTypes = {
|
||||
canFollow: PropTypes.bool,
|
||||
isFetching: PropTypes.bool,
|
||||
isFollowing: PropTypes.bool,
|
||||
isMutating: PropTypes.bool,
|
||||
followingError: PropTypes.string,
|
||||
|
@ -48,7 +47,6 @@ StudioFollow.propTypes = {
|
|||
export default connect(
|
||||
state => ({
|
||||
canFollow: selectCanFollowStudio(state),
|
||||
isFetching: selectIsFetchingRoles(state),
|
||||
isMutating: selectIsMutatingFollowing(state),
|
||||
isFollowing: selectIsFollowing(state),
|
||||
followingError: selectFollowingMutationError(state)
|
||||
|
|
|
@ -8,43 +8,40 @@ import {selectCanEditInfo} from '../../redux/studio-permissions';
|
|||
import {
|
||||
mutateStudioImage, selectIsMutatingImage, selectImageMutationError
|
||||
} from '../../redux/studio-mutations';
|
||||
import Spinner from '../../components/spinner/spinner.jsx';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const blankImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
const StudioImage = ({
|
||||
imageError, isFetching, isMutating, image, canEditInfo, handleUpdate
|
||||
}) => (
|
||||
<div>
|
||||
<h3>Image</h3>
|
||||
{isFetching ? (
|
||||
<h4>Fetching...</h4>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{width: '200px', height: '150px', border: '1px solid green'}}>
|
||||
{isMutating ?
|
||||
<Spinner color="blue" /> :
|
||||
<img
|
||||
style={{objectFit: 'contain'}}
|
||||
src={image}
|
||||
/>}
|
||||
</div>
|
||||
{canEditInfo &&
|
||||
<label>
|
||||
<input
|
||||
disabled={isMutating}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={e => {
|
||||
handleUpdate(e.target);
|
||||
e.target.value = '';
|
||||
}}
|
||||
/>
|
||||
{imageError && <div>Error mutating image: {imageError}</div>}
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const fieldClassName = classNames('studio-image', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating
|
||||
});
|
||||
const src = isMutating ? blankImage : (image || blankImage);
|
||||
return (
|
||||
<div className={fieldClassName}>
|
||||
<img
|
||||
style={{width: '300px', height: '225px', objectFit: 'cover'}}
|
||||
src={src}
|
||||
/>
|
||||
{canEditInfo && !isFetching &&
|
||||
<React.Fragment>
|
||||
<input
|
||||
disabled={isMutating}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={e => {
|
||||
handleUpdate(e.target);
|
||||
e.target.value = '';
|
||||
}}
|
||||
/>
|
||||
{imageError && <div>Error mutating image: {imageError}</div>}
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioImage.propTypes = {
|
||||
imageError: PropTypes.string,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import Debug from './debug.jsx';
|
||||
import StudioDescription from './studio-description.jsx';
|
||||
import StudioFollow from './studio-follow.jsx';
|
||||
import StudioTitle from './studio-title.jsx';
|
||||
|
@ -9,9 +8,10 @@ import StudioImage from './studio-image.jsx';
|
|||
|
||||
import {selectIsLoggedIn} from '../../redux/session';
|
||||
import {getInfo, getRoles} from '../../redux/studio';
|
||||
import StudioReport from './studio-report.jsx';
|
||||
|
||||
const StudioInfo = ({
|
||||
isLoggedIn, studio, onLoadInfo, onLoadRoles
|
||||
isLoggedIn, onLoadInfo, onLoadRoles
|
||||
}) => {
|
||||
useEffect(() => { // Load studio info after first render
|
||||
onLoadInfo();
|
||||
|
@ -22,30 +22,24 @@ const StudioInfo = ({
|
|||
}, [isLoggedIn]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Studio Info</h2>
|
||||
<React.Fragment>
|
||||
<StudioTitle />
|
||||
<StudioDescription />
|
||||
<StudioFollow />
|
||||
<StudioImage />
|
||||
<Debug
|
||||
label="Studio Info"
|
||||
data={studio}
|
||||
/>
|
||||
</div>
|
||||
<StudioDescription />
|
||||
<StudioReport />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
StudioInfo.propTypes = {
|
||||
isLoggedIn: PropTypes.bool,
|
||||
studio: PropTypes.shape({}), // TODO remove, just for <Debug />
|
||||
onLoadInfo: PropTypes.func,
|
||||
onLoadRoles: PropTypes.func
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
studio: state.studio,
|
||||
isLoggedIn: selectIsLoggedIn(state)
|
||||
}),
|
||||
{
|
||||
|
|
67
src/views/studio/studio-managers.jsx
Normal file
67
src/views/studio/studio-managers.jsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {managers} from './lib/redux-modules';
|
||||
import {loadManagers} from './lib/studio-member-actions';
|
||||
import Debug from './debug.jsx';
|
||||
import {ManagerTile} from './studio-member-tile.jsx';
|
||||
|
||||
|
||||
const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
|
||||
useEffect(() => {
|
||||
if (items.length === 0) onLoadMore();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="studio-members">
|
||||
<h2>Managers</h2>
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<div className="studio-members-grid">
|
||||
{items.map(item =>
|
||||
(<ManagerTile
|
||||
key={item.username}
|
||||
id={item.id}
|
||||
username={item.username}
|
||||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
)}
|
||||
<div className="studio-members-load-more">
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={onLoadMore}>
|
||||
Load more
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioManagers.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.id,
|
||||
username: PropTypes.string,
|
||||
profile: PropTypes.shape({
|
||||
images: PropTypes.shape({
|
||||
'90x90': PropTypes.string
|
||||
})
|
||||
})
|
||||
})),
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
moreToLoad: PropTypes.bool,
|
||||
onLoadMore: PropTypes.func
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => managers.selector(state),
|
||||
{
|
||||
onLoadMore: loadManagers
|
||||
}
|
||||
)(StudioManagers);
|
110
src/views/studio/studio-member-tile.jsx
Normal file
110
src/views/studio/studio-member-tile.jsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
selectCanRemoveCurators, selectCanRemoveManager, selectCanPromoteCurators
|
||||
} from '../../redux/studio-permissions';
|
||||
import {
|
||||
promoteCurator,
|
||||
removeCurator,
|
||||
removeManager
|
||||
} from './lib/studio-member-actions';
|
||||
|
||||
const StudioMemberTile = ({
|
||||
canRemove, canPromote, onRemove, onPromote, isCreator, // mapState props
|
||||
username, image // own props
|
||||
}) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const userUrl = `/users/${username}`;
|
||||
return (
|
||||
<div className="studio-member-tile">
|
||||
<a href={userUrl}>
|
||||
<img
|
||||
className="studio-member-image"
|
||||
src={image}
|
||||
/>
|
||||
</a>
|
||||
<div className="studio-member-info">
|
||||
<a
|
||||
href={userUrl}
|
||||
className="studio-member-name"
|
||||
>{username}</a>
|
||||
{isCreator && <div className="studio-member-role">Studio Creator</div>}
|
||||
</div>
|
||||
{canRemove &&
|
||||
<button
|
||||
className={classNames('studio-member-remove', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onRemove(username).catch(e => {
|
||||
setError(e);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}}
|
||||
>✕</button>
|
||||
}
|
||||
{canPromote &&
|
||||
<button
|
||||
className={classNames('studio-member-promote', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onPromote(username).catch(e => {
|
||||
setError(e);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}}
|
||||
>🆙</button>
|
||||
}
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioMemberTile.propTypes = {
|
||||
canRemove: PropTypes.bool,
|
||||
canPromote: PropTypes.bool,
|
||||
onRemove: PropTypes.func,
|
||||
onPromote: PropTypes.func,
|
||||
username: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
isCreator: PropTypes.bool
|
||||
};
|
||||
|
||||
const ManagerTile = connect(
|
||||
(state, ownProps) => ({
|
||||
canRemove: selectCanRemoveManager(state, ownProps.id),
|
||||
canPromote: false,
|
||||
isCreator: state.studio.owner === ownProps.id
|
||||
}),
|
||||
{
|
||||
onRemove: removeManager
|
||||
}
|
||||
)(StudioMemberTile);
|
||||
|
||||
const CuratorTile = connect(
|
||||
state => ({
|
||||
canRemove: selectCanRemoveCurators(state),
|
||||
canPromote: selectCanPromoteCurators(state)
|
||||
}),
|
||||
{
|
||||
onRemove: removeCurator,
|
||||
onPromote: promoteCurator
|
||||
}
|
||||
)(StudioMemberTile);
|
||||
|
||||
export {
|
||||
ManagerTile,
|
||||
CuratorTile
|
||||
};
|
53
src/views/studio/studio-project-adder.jsx
Normal file
53
src/views/studio/studio-project-adder.jsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {addProject} from './lib/studio-project-actions';
|
||||
|
||||
const StudioProjectAdder = ({onSubmit}) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="studio-adder-section">
|
||||
<h3>✦ Add Projects</h3>
|
||||
<input
|
||||
disabled={submitting}
|
||||
type="text"
|
||||
placeholder="<project id>"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onSubmit(value)
|
||||
.then(() => setValue(''))
|
||||
.catch(e => setError(e))
|
||||
.then(() => setSubmitting(false));
|
||||
}}
|
||||
>Add</button>
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioProjectAdder.propTypes = {
|
||||
onSubmit: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
const mapDispatchToProps = ({
|
||||
onSubmit: addProject
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StudioProjectAdder);
|
84
src/views/studio/studio-project-tile.jsx
Normal file
84
src/views/studio/studio-project-tile.jsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {selectCanRemoveProject} from '../../redux/studio-permissions';
|
||||
import {removeProject} from './lib/studio-project-actions';
|
||||
|
||||
const StudioProjectTile = ({
|
||||
canRemove, onRemove, // mapState props
|
||||
id, title, image, avatar, username // own props
|
||||
}) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const projectUrl = `/projects/${id}`;
|
||||
const userUrl = `/users/${username}`;
|
||||
return (
|
||||
<div className="studio-project-tile">
|
||||
<a href={projectUrl}>
|
||||
<img
|
||||
className="studio-project-image"
|
||||
src={image}
|
||||
/>
|
||||
</a>
|
||||
<div className="studio-project-bottom">
|
||||
<a href={userUrl}>
|
||||
<img
|
||||
className="studio-project-avatar"
|
||||
src={avatar}
|
||||
/>
|
||||
</a>
|
||||
<div className="studio-project-info">
|
||||
<a
|
||||
href={projectUrl}
|
||||
className="studio-project-title"
|
||||
>{title}</a>
|
||||
<a
|
||||
href={userUrl}
|
||||
className="studio-project-username"
|
||||
>{username}</a>
|
||||
</div>
|
||||
{canRemove &&
|
||||
<button
|
||||
className={classNames('studio-project-remove', {
|
||||
'mod-mutating': submitting
|
||||
})}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onRemove(id)
|
||||
.catch(e => {
|
||||
setError(e);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}}
|
||||
>✕</button>
|
||||
}
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioProjectTile.propTypes = {
|
||||
canRemove: PropTypes.bool,
|
||||
onRemove: PropTypes.func,
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
avatar: PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
canRemove: selectCanRemoveProject(state, ownProps.username, ownProps.addedBy)
|
||||
});
|
||||
|
||||
const mapDispatchToProps = ({
|
||||
onRemove: removeProject
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StudioProjectTile);
|
|
@ -1,54 +1,53 @@
|
|||
import React, {useEffect, useCallback} from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {useParams} from 'react-router-dom';
|
||||
import {connect} from 'react-redux';
|
||||
import StudioOpenToAll from './studio-open-to-all.jsx';
|
||||
|
||||
import {projectFetcher} from './lib/fetchers';
|
||||
import {projects} from './lib/redux-modules';
|
||||
import {selectCanAddProjects, selectCanEditOpenToAll} from '../../redux/studio-permissions';
|
||||
import Debug from './debug.jsx';
|
||||
|
||||
const {actions, selector: projectsSelector} = projects;
|
||||
import StudioProjectAdder from './studio-project-adder.jsx';
|
||||
import StudioProjectTile from './studio-project-tile.jsx';
|
||||
import {loadProjects} from './lib/studio-project-actions.js';
|
||||
|
||||
const StudioProjects = ({
|
||||
canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore
|
||||
}) => {
|
||||
const {studioId} = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (studioId && items.length === 0) onLoadMore(studioId, 0);
|
||||
}, [studioId]);
|
||||
|
||||
const handleLoadMore = useCallback(() => onLoadMore(studioId, items.length), [studioId, items.length]);
|
||||
|
||||
if (items.length === 0) onLoadMore();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="studio-projects">
|
||||
<h2>Projects</h2>
|
||||
{canEditOpenToAll && <StudioOpenToAll />}
|
||||
{canAddProjects && <StudioProjectAdder />}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<Debug
|
||||
label="Project Permissions"
|
||||
data={{canAddProjects}}
|
||||
/>
|
||||
<div>
|
||||
{items.map((item, index) =>
|
||||
(<Debug
|
||||
label="Project"
|
||||
data={item}
|
||||
key={index}
|
||||
<div className="studio-projects-grid">
|
||||
{items.map(item =>
|
||||
(<StudioProjectTile
|
||||
fetching={loading}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
title={item.title}
|
||||
image={item.image}
|
||||
avatar={item.avatar['90x90']}
|
||||
username={item.username}
|
||||
addedBy={item.actor_id}
|
||||
/>)
|
||||
)}
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={handleLoadMore}>
|
||||
<div className="studio-projects-load-more">
|
||||
{loading ? <small>Loading...</small> : (
|
||||
moreToLoad ?
|
||||
<button onClick={onLoadMore}>
|
||||
Load more
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</button> :
|
||||
<small>No more to load</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -57,22 +56,27 @@ const StudioProjects = ({
|
|||
StudioProjects.propTypes = {
|
||||
canAddProjects: PropTypes.bool,
|
||||
canEditOpenToAll: PropTypes.bool,
|
||||
items: PropTypes.array, // eslint-disable-line react/forbid-prop-types
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
avatar: PropTypes.shape({
|
||||
'90x90': PropTypes.string
|
||||
}),
|
||||
id: PropTypes.id,
|
||||
title: PropTypes.string,
|
||||
username: PropTypes.string
|
||||
})),
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
moreToLoad: PropTypes.bool,
|
||||
onLoadMore: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
...projectsSelector(state),
|
||||
canAddProjects: selectCanAddProjects(state),
|
||||
canEditOpenToAll: selectCanEditOpenToAll(state)
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onLoadMore: (studioId, offset) => dispatch(
|
||||
actions.loadMore(projectFetcher.bind(null, studioId, offset))
|
||||
)
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StudioProjects);
|
||||
export default connect(
|
||||
state => ({
|
||||
...projects.selector(state),
|
||||
canAddProjects: selectCanAddProjects(state),
|
||||
canEditOpenToAll: selectCanEditOpenToAll(state)
|
||||
}),
|
||||
{
|
||||
onLoadMore: loadProjects
|
||||
}
|
||||
)(StudioProjects);
|
||||
|
|
95
src/views/studio/studio-report.jsx
Normal file
95
src/views/studio/studio-report.jsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {
|
||||
Fields,
|
||||
actions,
|
||||
selectors
|
||||
} from '../../redux/studio-report';
|
||||
|
||||
const StudioReport = ({
|
||||
canReport,
|
||||
error,
|
||||
field,
|
||||
isOpen,
|
||||
isSubmitting,
|
||||
previouslyReported,
|
||||
handleSetField,
|
||||
handleOpen,
|
||||
handleClose,
|
||||
handleSubmit
|
||||
}) => (
|
||||
<div>
|
||||
<h3>Reporting</h3>
|
||||
{canReport && (
|
||||
<button onClick={handleOpen}>Report</button>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div style={{padding: '1rem', margin: '1rem', border: '1px solid green'}}>
|
||||
<div>Report Studio Modal</div>
|
||||
{previouslyReported ? (
|
||||
<React.Fragment>
|
||||
<div>Submitted the report!</div>
|
||||
<button onClick={handleClose}>Close</button>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<select
|
||||
value={field}
|
||||
onChange={e => handleSetField(e.target.value)}
|
||||
>
|
||||
<option value={Fields.TITLE}>Title</option>
|
||||
<option value={Fields.DESCRIPTION}>Description</option>
|
||||
<option value={Fields.THUMBNAIL}>Thumbnail</option>
|
||||
</select>
|
||||
{error && (
|
||||
<div>
|
||||
<div>There was an error. Try again later?</div>
|
||||
<div><code><pre>{error}</pre></code></div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button onClick={handleClose}>Cancel</button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
StudioReport.propTypes = {
|
||||
canReport: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
field: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
isSubmitting: PropTypes.bool,
|
||||
previouslyReported: PropTypes.bool,
|
||||
handleOpen: PropTypes.func,
|
||||
handleClose: PropTypes.func,
|
||||
handleSetField: PropTypes.func,
|
||||
handleSubmit: PropTypes.func
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
canReport: selectors.selectCanReportStudio(state),
|
||||
error: selectors.selectStudioReportError(state),
|
||||
field: selectors.selectStudioReportField(state),
|
||||
isOpen: selectors.selectStudioReportOpen(state),
|
||||
isSubmitting: selectors.selectStudioReportSubmitting(state),
|
||||
previouslyReported: selectors.selectStudioReportSubmitted(state)
|
||||
}),
|
||||
{
|
||||
handleOpen: actions.openStudioReport,
|
||||
handleClose: actions.closeStudioReport,
|
||||
handleSetField: actions.setStudioReportField,
|
||||
handleSubmit: actions.submitStudioReport
|
||||
}
|
||||
)(StudioReport);
|
|
@ -1,40 +1,41 @@
|
|||
import React from 'react';
|
||||
import {useRouteMatch, NavLink} from 'react-router-dom';
|
||||
import SubNavigation from '../../components/subnavigation/subnavigation.jsx';
|
||||
|
||||
const StudioTabNav = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SubNavigation
|
||||
align="left"
|
||||
className="studio-tab-nav"
|
||||
>
|
||||
<NavLink
|
||||
activeStyle={{textDecoration: 'underline'}}
|
||||
activeClassName="active"
|
||||
to={`${match.url}`}
|
||||
exact
|
||||
>
|
||||
Projects
|
||||
<li>Projects</li>
|
||||
</NavLink>
|
||||
|
|
||||
<NavLink
|
||||
activeStyle={{textDecoration: 'underline'}}
|
||||
activeClassName="active"
|
||||
to={`${match.url}/curators`}
|
||||
>
|
||||
Curators
|
||||
<li>Curators</li>
|
||||
</NavLink>
|
||||
|
|
||||
<NavLink
|
||||
activeStyle={{textDecoration: 'underline'}}
|
||||
activeClassName="active"
|
||||
to={`${match.url}/comments`}
|
||||
>
|
||||
Comments
|
||||
<li> Comments</li>
|
||||
</NavLink>
|
||||
|
|
||||
<NavLink
|
||||
activeStyle={{textDecoration: 'underline'}}
|
||||
activeClassName="active"
|
||||
to={`${match.url}/activity`}
|
||||
>
|
||||
Activity
|
||||
<li>Activity</li>
|
||||
</NavLink>
|
||||
</div>
|
||||
</SubNavigation>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,29 +6,28 @@ import {connect} from 'react-redux';
|
|||
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo} from '../../redux/studio-permissions';
|
||||
import {mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const StudioTitle = ({
|
||||
titleError, isFetching, isMutating, title, canEditInfo, handleUpdate
|
||||
}) => (
|
||||
<div>
|
||||
<h3>Title</h3>
|
||||
{isFetching ? (
|
||||
<h4>Fetching...</h4>
|
||||
) : (canEditInfo ? (
|
||||
<label>
|
||||
<input
|
||||
disabled={isMutating}
|
||||
defaultValue={title}
|
||||
onBlur={e => e.target.value !== title &&
|
||||
handleUpdate(e.target.value)}
|
||||
/>
|
||||
{titleError && <div>Error mutating title: {titleError}</div>}
|
||||
</label>
|
||||
) : (
|
||||
<div>{title}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const fieldClassName = classNames('studio-title', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<textarea
|
||||
className={fieldClassName}
|
||||
disabled={isMutating || !canEditInfo || isFetching}
|
||||
defaultValue={title}
|
||||
onBlur={e => e.target.value !== title &&
|
||||
handleUpdate(e.target.value)}
|
||||
/>
|
||||
{titleError && <div>Error mutating title: {titleError}</div>}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
StudioTitle.propTypes = {
|
||||
titleError: PropTypes.string,
|
||||
|
|
|
@ -13,6 +13,7 @@ import render from '../../lib/render.jsx';
|
|||
import StudioTabNav from './studio-tab-nav.jsx';
|
||||
import StudioProjects from './studio-projects.jsx';
|
||||
import StudioInfo from './studio-info.jsx';
|
||||
import StudioManagers from './studio-managers.jsx';
|
||||
import StudioCurators from './studio-curators.jsx';
|
||||
import StudioComments from './studio-comments.jsx';
|
||||
import StudioActivity from './studio-activity.jsx';
|
||||
|
@ -25,43 +26,50 @@ import {
|
|||
} from './lib/redux-modules';
|
||||
|
||||
const {getInitialState, studioReducer} = require('../../redux/studio');
|
||||
const {studioReportReducer} = require('../../redux/studio-report');
|
||||
const {commentsReducer} = require('../../redux/comments');
|
||||
const {studioMutationsReducer} = require('../../redux/studio-mutations');
|
||||
|
||||
import './studio.scss';
|
||||
|
||||
const StudioShell = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<div style={{maxWidth: '960px', margin: 'auto'}}>
|
||||
<StudioInfo />
|
||||
<hr />
|
||||
<StudioTabNav />
|
||||
<div>
|
||||
<Switch>
|
||||
<Route path={`${match.path}/curators`}>
|
||||
<StudioCurators />
|
||||
</Route>
|
||||
<Route path={`${match.path}/comments`}>
|
||||
<StudioComments />
|
||||
</Route>
|
||||
<Route path={`${match.path}/activity`}>
|
||||
<StudioActivity />
|
||||
</Route>
|
||||
<Route path={`${match.path}/projects`}>
|
||||
{/* We can force /projects back to / this way */}
|
||||
<Redirect to={match.url} />
|
||||
</Route>
|
||||
<Route path={match.path}>
|
||||
<StudioProjects />
|
||||
</Route>
|
||||
</Switch>
|
||||
<div className="studio-shell">
|
||||
<div className="studio-info">
|
||||
<StudioInfo />
|
||||
</div>
|
||||
<div className="studio-tabs">
|
||||
<StudioTabNav />
|
||||
<div>
|
||||
<Switch>
|
||||
<Route path={`${match.path}/curators`}>
|
||||
<StudioManagers />
|
||||
<StudioCurators />
|
||||
</Route>
|
||||
<Route path={`${match.path}/comments`}>
|
||||
<StudioComments />
|
||||
</Route>
|
||||
<Route path={`${match.path}/activity`}>
|
||||
<StudioActivity />
|
||||
</Route>
|
||||
<Route path={`${match.path}/projects`}>
|
||||
{/* We can force /projects back to / this way */}
|
||||
<Redirect to={match.url} />
|
||||
</Route>
|
||||
<Route path={match.path}>
|
||||
<StudioProjects />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<Page>
|
||||
<Page className="studio-page">
|
||||
<Router>
|
||||
<Switch>
|
||||
{/* Use variable studioPath to support /studio-playground/ or future route */}
|
||||
|
@ -77,9 +85,10 @@ render(
|
|||
[curators.key]: curators.reducer,
|
||||
[managers.key]: managers.reducer,
|
||||
[activity.key]: activity.reducer,
|
||||
comments: commentsReducer,
|
||||
studio: studioReducer,
|
||||
studioMutations: studioMutationsReducer,
|
||||
comments: commentsReducer
|
||||
studioReport: studioReportReducer
|
||||
},
|
||||
{
|
||||
studio: {
|
||||
|
|
266
src/views/studio/studio.scss
Normal file
266
src/views/studio/studio.scss
Normal file
|
@ -0,0 +1,266 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
$radius: 8px;
|
||||
|
||||
.studio-page {
|
||||
background-color: #E9F1FC;
|
||||
|
||||
#view {
|
||||
/* Reset some defaults on width and margin */
|
||||
background-color: transparent;
|
||||
max-width: 1240px;
|
||||
min-width: auto;
|
||||
margin: 50px auto;
|
||||
display: block;
|
||||
|
||||
.studio-shell {
|
||||
padding: 0 20px;
|
||||
display: grid;
|
||||
gap: 40px;
|
||||
|
||||
/* Side-by-side with fixed width sidebar */
|
||||
grid-template-columns: 300px minmax(0, 1fr);
|
||||
|
||||
/* Stack vertically at medium size and smaller */
|
||||
@media #{$medium-and-smaller} {
|
||||
& {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.studio-info {
|
||||
justify-self: center;
|
||||
width: 300px;
|
||||
height: fit-content;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
|
||||
.studio-title, .studio-description {
|
||||
background: transparent;
|
||||
margin: 0 -8px; /* Outset the border horizontally */
|
||||
padding: 5px 8px;
|
||||
border: 2px dashed $ui-blue-25percent;
|
||||
border-radius: $radius;
|
||||
resize: none;
|
||||
&:disabled { border-color: transparent; }
|
||||
}
|
||||
|
||||
.studio-title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.studio-description:disabled {
|
||||
background: $ui-blue-10percent;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-tab-nav {
|
||||
border-bottom: 1px solid $active-dark-gray;
|
||||
padding-bottom: 8px;
|
||||
li { background: $active-gray; }
|
||||
.active > li { background: $ui-blue; }
|
||||
}
|
||||
|
||||
.studio-projects {}
|
||||
.studio-projects-grid {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
@media #{$medium} {
|
||||
& { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
}
|
||||
@media #{$big} {
|
||||
& { grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||
}
|
||||
column-gap: 30px;
|
||||
row-gap: 20px;
|
||||
|
||||
.studio-projects-load-more {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-project-tile {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $ui-border;
|
||||
|
||||
.studio-project-image {
|
||||
max-width: 100%;
|
||||
background: #a0c6fc;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
.studio-project-bottom {
|
||||
display: flex;
|
||||
padding: 10px 6px 10px 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.studio-project-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.studio-project-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
overflow: hidden;
|
||||
margin: 0 8px;
|
||||
flex-grow: 1; /* Grow to fill available space */
|
||||
min-width: 0; /* Prevents within from expanding beyond bounds */
|
||||
}
|
||||
.studio-project-title {
|
||||
color: #4C97FF;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.studio-project-username {
|
||||
color: #575E75;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.studio-project-remove {
|
||||
color: $ui-blue;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-members {}
|
||||
.studio-members-grid {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
@media #{$medium} {
|
||||
& { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
}
|
||||
@media #{$big} {
|
||||
& { grid-template-columns: repeat(3, minmax(0,1fr)); }
|
||||
}
|
||||
column-gap: 30px;
|
||||
row-gap: 20px;
|
||||
.studio-members-load-more {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-member-tile {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $ui-border;
|
||||
|
||||
display: flex;
|
||||
padding: 10px 6px 10px 12px;
|
||||
justify-content: space-between;
|
||||
|
||||
.studio-member-image {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.studio-member-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
overflow: hidden;
|
||||
margin: 0 8px;
|
||||
flex-grow: 1; /* Grow to fill available space */
|
||||
min-width: 0; /* Prevents within from expanding beyond bounds */
|
||||
}
|
||||
.studio-member-name {
|
||||
color: #4C97FF;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.studio-member-role {
|
||||
color: #575E75;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.studio-member-remove, .studio-member-promote {
|
||||
color: $ui-blue;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-members + .studio-members {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.studio-adder-section {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h3 {
|
||||
color: #4C97FF;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-basis: 80%;
|
||||
flex-grow: 1;
|
||||
display: inline-block;
|
||||
margin: .5em 0;
|
||||
border: 1px solid $ui-border;
|
||||
border-radius: .5rem;
|
||||
padding: 1em 1.25em;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
input + button {
|
||||
margin-inline-start: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modification classes for different interaction states */
|
||||
.mod-fetching { /* When a field has no content to display yet */
|
||||
position: relative;
|
||||
min-height: 30px;
|
||||
&::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #a0c6fc;
|
||||
border-radius: $radius;
|
||||
}
|
||||
/* For elements that can't use :after, force reset some internals
|
||||
to get the same visual (e.g. for textareas)*/
|
||||
border-radius: $radius;
|
||||
background: #a0c6fc !important;
|
||||
color: #a0c6fc !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.mod-mutating { /* When a field has sent a change to the server */
|
||||
cursor: wait !important;
|
||||
opacity: .5;
|
||||
}
|
|
@ -6,6 +6,9 @@
|
|||
"isCurator": {
|
||||
"curator": true
|
||||
},
|
||||
"isInvited": {
|
||||
"invited": true
|
||||
},
|
||||
"creator1": {
|
||||
"owner": 1
|
||||
},
|
||||
|
@ -27,7 +30,8 @@
|
|||
"user1Social": {
|
||||
"session": {
|
||||
"user": {
|
||||
"id": 1
|
||||
"id": 1,
|
||||
"username": "user1-username"
|
||||
},
|
||||
"permissions": {
|
||||
"social": true
|
||||
|
|
66
test/unit/components/studio-comments.test.jsx
Normal file
66
test/unit/components/studio-comments.test.jsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import React from 'react';
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import {StudioComments} from '../../../src/views/studio/studio-comments.jsx';
|
||||
|
||||
describe('Studio comments', () => {
|
||||
test('if there are no comments, they get loaded', () => {
|
||||
const loadComments = jest.fn();
|
||||
const component = mountWithIntl(
|
||||
<StudioComments
|
||||
comments={[]}
|
||||
handleLoadMoreComments={loadComments}
|
||||
/>
|
||||
);
|
||||
expect(loadComments).toHaveBeenCalled();
|
||||
|
||||
// When updated to have comments, load is not called again
|
||||
loadComments.mockClear();
|
||||
component.setProps({comments: [{id: 123, author: {}}]});
|
||||
component.update();
|
||||
expect(loadComments).not.toHaveBeenCalled();
|
||||
|
||||
// When reset to have no comments again, load is called again
|
||||
loadComments.mockClear();
|
||||
component.setProps({comments: []});
|
||||
component.update();
|
||||
expect(loadComments).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('becoming an admin resets the comments', () => {
|
||||
const resetComments = jest.fn();
|
||||
const component = mountWithIntl(
|
||||
<StudioComments
|
||||
isAdmin={false}
|
||||
comments={[{id: 123, author: {}}]}
|
||||
handleResetComments={resetComments}
|
||||
/>
|
||||
);
|
||||
expect(resetComments).not.toHaveBeenCalled();
|
||||
|
||||
// When updated to isAdmin=true, reset is called
|
||||
resetComments.mockClear();
|
||||
component.setProps({isAdmin: true});
|
||||
component.update();
|
||||
expect(resetComments).toHaveBeenCalled();
|
||||
|
||||
// If updated back to isAdmin=false, reset is also called
|
||||
// not currently possible in the UI, but if it was, we'd want to clear comments
|
||||
resetComments.mockClear();
|
||||
component.setProps({isAdmin: false});
|
||||
component.update();
|
||||
expect(resetComments).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('being an admin on initial render doesnt reset comments', () => {
|
||||
// This ensures that comments don't get reloaded when changing tabs
|
||||
const resetComments = jest.fn();
|
||||
mountWithIntl(
|
||||
<StudioComments
|
||||
isAdmin
|
||||
comments={[{id: 123, author: {}}]}
|
||||
handleResetComments={resetComments}
|
||||
/>
|
||||
);
|
||||
expect(resetComments).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -8,11 +8,17 @@ import {
|
|||
selectCanRestoreComment,
|
||||
selectCanFollowStudio,
|
||||
selectCanEditCommentsAllowed,
|
||||
selectCanEditOpenToAll
|
||||
selectCanEditOpenToAll,
|
||||
selectShowCuratorInvite,
|
||||
selectCanInviteCurators,
|
||||
selectCanRemoveCurators,
|
||||
selectCanRemoveManager,
|
||||
selectCanPromoteCurators,
|
||||
selectCanRemoveProject
|
||||
} from '../../../src/redux/studio-permissions';
|
||||
|
||||
import {getInitialState as getInitialStudioState} from '../../../src/redux/studio';
|
||||
import {getInitialState as getInitialSessionState} from '../../../src/redux/session';
|
||||
import {getInitialState as getInitialSessionState, selectUserId, selectUsername} from '../../../src/redux/session';
|
||||
import {sessions, studios} from '../../helpers/state-fixtures.json';
|
||||
|
||||
let state;
|
||||
|
@ -42,6 +48,9 @@ const setStateByRole = (role) => {
|
|||
break;
|
||||
case 'logged out': // Default state set in beforeEach
|
||||
break;
|
||||
case 'invited':
|
||||
state.studio = studios.isInvited;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown user role in test: ' + role);
|
||||
}
|
||||
|
@ -98,6 +107,39 @@ describe('studio projects', () => {
|
|||
expect(selectCanAddProjects(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('can remove projects', () => {
|
||||
test.each([
|
||||
['admin', true],
|
||||
['curator', false], // false for projects that were not added by them, see below
|
||||
['manager', true],
|
||||
['creator', true],
|
||||
['logged in', false], // false for projects that are not theirs, see below
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveProject(state, 'not-me', 'not-me')).toBe(expected);
|
||||
});
|
||||
|
||||
test('curators can remove projects they added', () => {
|
||||
setStateByRole('curator');
|
||||
const addedBy = selectUserId(state);
|
||||
expect(selectCanRemoveProject(state, 'not-me', addedBy)).toBe(true);
|
||||
});
|
||||
|
||||
test('curators can also remove projects they own that they did not add', () => {
|
||||
setStateByRole('curator');
|
||||
const creator = selectUsername(state);
|
||||
expect(selectCanRemoveProject(state, creator, 'not-me')).toBe(true);
|
||||
});
|
||||
|
||||
test('logged in users can only remove projects they own', () => {
|
||||
setStateByRole('logged in');
|
||||
const creator = selectUsername(state);
|
||||
expect(selectCanRemoveProject(state, creator, 'not-me')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('studio comments', () => {
|
||||
|
@ -209,3 +251,97 @@ describe('studio comments', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('studio members', () => {
|
||||
describe('can accept invitation', () => {
|
||||
test.each([
|
||||
['admin', false],
|
||||
['curator', false],
|
||||
['manager', false],
|
||||
['creator', false],
|
||||
['invited', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectShowCuratorInvite(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('can promote curators', () => {
|
||||
test.each([
|
||||
['admin', false],
|
||||
['curator', false],
|
||||
['manager', true],
|
||||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanPromoteCurators(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('can remove curators', () => {
|
||||
test.each([
|
||||
['admin', true],
|
||||
['curator', false],
|
||||
['manager', true],
|
||||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveCurators(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('can remove managers', () => {
|
||||
test.each([
|
||||
['admin', true],
|
||||
['curator', false],
|
||||
['manager', true],
|
||||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveManager(state, '123')).toBe(expected);
|
||||
});
|
||||
|
||||
describe('nobody can remove the studio creator', () => {
|
||||
test.each([
|
||||
['admin', false],
|
||||
['curator', false],
|
||||
['manager', false],
|
||||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
state.studio.owner = 'the creator';
|
||||
expect(selectCanRemoveManager(state, 'the creator')).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('can invite curators', () => {
|
||||
test.each([
|
||||
['admin', false],
|
||||
['curator', false],
|
||||
['manager', true],
|
||||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanInviteCurators(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue