Merge branch 'develop' of https://github.com/LLK/scratch-www into studio-report-modal

# Conflicts:
#	src/views/studio/studio-report.jsx
This commit is contained in:
Eric Rosenbaum 2021-05-14 15:20:41 -04:00
commit 20a342c0a2
42 changed files with 793 additions and 328 deletions

309
package-lock.json generated
View file

@ -225,20 +225,20 @@
"dev": true "dev": true
}, },
"@babel/core": { "@babel/core": {
"version": "7.14.0", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.2.tgz",
"integrity": "sha512-8YqpRig5NmIHlMLw09zMlPTvUVMILjqCOtVgu+TVNWEBvy9b5I3RRyhqnrV4hjgEK7n8P9OqvkWJAFmEL6Wwfw==", "integrity": "sha512-OgC1mON+l4U4B4wiohJlQNUU3H73mpTyYY3j/c8U9dr9UagGGSm+WFpzjy/YLdoyjiG++c1kIDgxCo/mLwQJeQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.12.13", "@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.14.0", "@babel/generator": "^7.14.2",
"@babel/helper-compilation-targets": "^7.13.16", "@babel/helper-compilation-targets": "^7.13.16",
"@babel/helper-module-transforms": "^7.14.0", "@babel/helper-module-transforms": "^7.14.2",
"@babel/helpers": "^7.14.0", "@babel/helpers": "^7.14.0",
"@babel/parser": "^7.14.0", "@babel/parser": "^7.14.2",
"@babel/template": "^7.12.13", "@babel/template": "^7.12.13",
"@babel/traverse": "^7.14.0", "@babel/traverse": "^7.14.2",
"@babel/types": "^7.14.0", "@babel/types": "^7.14.2",
"convert-source-map": "^1.7.0", "convert-source-map": "^1.7.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@ -257,25 +257,25 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.14.1", "@babel/types": "^7.14.2",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"source-map": "^0.5.0" "source-map": "^0.5.0"
} }
}, },
"@babel/helper-function-name": { "@babel/helper-function-name": {
"version": "7.12.13", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz",
"integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-get-function-arity": "^7.12.13", "@babel/helper-get-function-arity": "^7.12.13",
"@babel/template": "^7.12.13", "@babel/template": "^7.12.13",
"@babel/types": "^7.12.13" "@babel/types": "^7.14.2"
} }
}, },
"@babel/helper-get-function-arity": { "@babel/helper-get-function-arity": {
@ -308,9 +308,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
"dev": true "dev": true
}, },
"@babel/template": { "@babel/template": {
@ -325,25 +325,25 @@
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.14.0", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz",
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.12.13", "@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.14.0", "@babel/generator": "^7.14.2",
"@babel/helper-function-name": "^7.12.13", "@babel/helper-function-name": "^7.14.2",
"@babel/helper-split-export-declaration": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.14.0", "@babel/parser": "^7.14.2",
"@babel/types": "^7.14.0", "@babel/types": "^7.14.2",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0" "globals": "^11.1.0"
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
@ -546,9 +546,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/types": { "@babel/types": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
@ -573,9 +573,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/types": { "@babel/types": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
@ -591,9 +591,9 @@
} }
}, },
"@babel/helper-module-transforms": { "@babel/helper-module-transforms": {
"version": "7.14.0", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz",
"integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==", "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-module-imports": "^7.13.12", "@babel/helper-module-imports": "^7.13.12",
@ -602,8 +602,8 @@
"@babel/helper-split-export-declaration": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13",
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
"@babel/template": "^7.12.13", "@babel/template": "^7.12.13",
"@babel/traverse": "^7.14.0", "@babel/traverse": "^7.14.2",
"@babel/types": "^7.14.0" "@babel/types": "^7.14.2"
}, },
"dependencies": { "dependencies": {
"@babel/code-frame": { "@babel/code-frame": {
@ -616,25 +616,25 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.14.1", "@babel/types": "^7.14.2",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"source-map": "^0.5.0" "source-map": "^0.5.0"
} }
}, },
"@babel/helper-function-name": { "@babel/helper-function-name": {
"version": "7.12.13", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz",
"integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-get-function-arity": "^7.12.13", "@babel/helper-get-function-arity": "^7.12.13",
"@babel/template": "^7.12.13", "@babel/template": "^7.12.13",
"@babel/types": "^7.12.13" "@babel/types": "^7.14.2"
} }
}, },
"@babel/helper-get-function-arity": { "@babel/helper-get-function-arity": {
@ -667,9 +667,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
"dev": true "dev": true
}, },
"@babel/template": { "@babel/template": {
@ -684,25 +684,25 @@
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.14.0", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz",
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.12.13", "@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.14.0", "@babel/generator": "^7.14.2",
"@babel/helper-function-name": "^7.12.13", "@babel/helper-function-name": "^7.14.2",
"@babel/helper-split-export-declaration": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.14.0", "@babel/parser": "^7.14.2",
"@babel/types": "^7.14.0", "@babel/types": "^7.14.2",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0" "globals": "^11.1.0"
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
@ -789,9 +789,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/types": { "@babel/types": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
@ -834,25 +834,25 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.14.1", "@babel/types": "^7.14.2",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"source-map": "^0.5.0" "source-map": "^0.5.0"
} }
}, },
"@babel/helper-function-name": { "@babel/helper-function-name": {
"version": "7.12.13", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz",
"integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-get-function-arity": "^7.12.13", "@babel/helper-get-function-arity": "^7.12.13",
"@babel/template": "^7.12.13", "@babel/template": "^7.12.13",
"@babel/types": "^7.12.13" "@babel/types": "^7.14.2"
} }
}, },
"@babel/helper-get-function-arity": { "@babel/helper-get-function-arity": {
@ -885,9 +885,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
"dev": true "dev": true
}, },
"@babel/template": { "@babel/template": {
@ -902,25 +902,25 @@
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.14.0", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz",
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.12.13", "@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.14.0", "@babel/generator": "^7.14.2",
"@babel/helper-function-name": "^7.12.13", "@babel/helper-function-name": "^7.14.2",
"@babel/helper-split-export-declaration": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.14.0", "@babel/parser": "^7.14.2",
"@babel/types": "^7.14.0", "@babel/types": "^7.14.2",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0" "globals": "^11.1.0"
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
@ -1007,9 +1007,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/types": { "@babel/types": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
@ -1066,25 +1066,25 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
"integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.14.1", "@babel/types": "^7.14.2",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"source-map": "^0.5.0" "source-map": "^0.5.0"
} }
}, },
"@babel/helper-function-name": { "@babel/helper-function-name": {
"version": "7.12.13", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz",
"integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-get-function-arity": "^7.12.13", "@babel/helper-get-function-arity": "^7.12.13",
"@babel/template": "^7.12.13", "@babel/template": "^7.12.13",
"@babel/types": "^7.12.13" "@babel/types": "^7.14.2"
} }
}, },
"@babel/helper-get-function-arity": { "@babel/helper-get-function-arity": {
@ -1117,9 +1117,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
"integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
"dev": true "dev": true
}, },
"@babel/template": { "@babel/template": {
@ -1134,25 +1134,25 @@
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.14.0", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz",
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.12.13", "@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.14.0", "@babel/generator": "^7.14.2",
"@babel/helper-function-name": "^7.12.13", "@babel/helper-function-name": "^7.14.2",
"@babel/helper-split-export-declaration": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.14.0", "@babel/parser": "^7.14.2",
"@babel/types": "^7.14.0", "@babel/types": "^7.14.2",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0" "globals": "^11.1.0"
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.14.1", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
"integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.0", "@babel/helper-validator-identifier": "^7.14.0",
@ -1368,10 +1368,27 @@
} }
} }
}, },
"@formatjs/ecma402-abstract": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz",
"integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
},
"@formatjs/intl-getcanonicallocales": { "@formatjs/intl-getcanonicallocales": {
"version": "1.5.10", "version": "1.5.11",
"resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.10.tgz", "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.11.tgz",
"integrity": "sha512-tFqGxZ9HkAzphupybyCKdWHzL1ge/sY8TtzEK57Hs3RCxrv/y+VxIPrE+Izw2oCFowQBz76cyi0zT6PjHuWArA==", "integrity": "sha512-S+D4P8BSZDVTooR0AkqJUWMF6BKMyaBgM/XJiXiuOhVNg44ZYwwsD4PhnTGEbLpTpAcJX1Leway1SLfzrH8YOw==",
"dev": true, "dev": true,
"requires": { "requires": {
"cldr-core": "38", "cldr-core": "38",
@ -1387,26 +1404,17 @@
} }
}, },
"@formatjs/intl-locale": { "@formatjs/intl-locale": {
"version": "2.4.24", "version": "2.4.25",
"resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.24.tgz", "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.25.tgz",
"integrity": "sha512-+JOwvBRFS/GFuJlWiWbfAzBng0A+ANoGV1LRseXK+4uzp4Sn35GD8M/dfgU1lp2R2dTWpYie2yyoHe4k4aHF6w==", "integrity": "sha512-UgoU8TlMhjkiIJCRIgMs6JzTwYcVH9BnpL0j8Vy3A1zCXYDsRPu7TKpEQ5mrc8qkCqxz5LWZfocJqnluw/G1JQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@formatjs/ecma402-abstract": "1.7.1", "@formatjs/ecma402-abstract": "1.7.1",
"@formatjs/intl-getcanonicallocales": "1.5.10", "@formatjs/intl-getcanonicallocales": "1.5.11",
"cldr-core": "38", "cldr-core": "38",
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
"dependencies": { "dependencies": {
"@formatjs/ecma402-abstract": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz",
"integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
}
},
"tslib": { "tslib": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
@ -1416,24 +1424,15 @@
} }
}, },
"@formatjs/intl-pluralrules": { "@formatjs/intl-pluralrules": {
"version": "4.0.18", "version": "4.0.19",
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.18.tgz", "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.19.tgz",
"integrity": "sha512-qRFITPsNoeXfsiGc97pp8mVgqcC7aQNuXsiJjY9LpXVTkYNfjUP4ZpbYXflM4xoWCXMJNz3ilsrQhZWXy9td5g==", "integrity": "sha512-ArxXyH1NVLHKjiEVG0Lg8BMjGJoC7M+0FZnr2ln5uhrosQoctKmTrTXpezhqw0NOKyo05jIQY/keMlfRgEpIsw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@formatjs/ecma402-abstract": "1.7.1", "@formatjs/ecma402-abstract": "1.7.1",
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
"dependencies": { "dependencies": {
"@formatjs/ecma402-abstract": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.7.1.tgz",
"integrity": "sha512-FjewVLB2DVEVCvvC7IMffzXVhysvi442i6ed0H7qcrT6xtUpO4vr0oZgpOmsv6D9I4Io0GVebIuySwteS/k3gg==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
}
},
"tslib": { "tslib": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
@ -20856,9 +20855,9 @@
} }
}, },
"scratch-blocks": { "scratch-blocks": {
"version": "0.1.0-prerelease.20210510043314", "version": "0.1.0-prerelease.20210514034551",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210510043314.tgz", "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210514034551.tgz",
"integrity": "sha512-wXogEeojamKgRyBxkScpchd3sDp6sZsIxYcYNtWdPIbMpNacytMzC6hXYjFfq/BUZs4Vmnnu1x2+NKKX6N8WuA==", "integrity": "sha512-OfQNiJG8lidyXBGTeWXpC6caSVroAxfmkK1mxOFdGoIW3adUJTrl6np5RXwD58ByYnerxt+AldR3kIOyNAdBrg==",
"dev": true, "dev": true,
"requires": { "requires": {
"exports-loader": "0.6.3", "exports-loader": "0.6.3",
@ -20866,9 +20865,9 @@
} }
}, },
"scratch-gui": { "scratch-gui": {
"version": "0.1.0-prerelease.20210510170231", "version": "0.1.0-prerelease.20210514040957",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210510170231.tgz", "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210514040957.tgz",
"integrity": "sha512-RS12FvNZR1CoEUxtvRpHIrDQVJkPJWrIYkNS5Bjr/ZtixVN1fdRB/zHtfZ0LIbw+TOVABSyn81kKWGW011J73g==", "integrity": "sha512-qjn3YtszSnj0iI/Bd9LXJBuudyg2T2QwJ0TOl9628/wuwexZsLxEERyHl8o9vhErUaiJ2TLeL5nTYWbMbhdA9A==",
"dev": true, "dev": true,
"requires": { "requires": {
"arraybuffer-loader": "^1.0.6", "arraybuffer-loader": "^1.0.6",
@ -20919,13 +20918,13 @@
"redux": "3.7.2", "redux": "3.7.2",
"redux-throttle": "0.1.1", "redux-throttle": "0.1.1",
"scratch-audio": "0.1.0-prerelease.20200528195344", "scratch-audio": "0.1.0-prerelease.20200528195344",
"scratch-blocks": "0.1.0-prerelease.20210510043314", "scratch-blocks": "0.1.0-prerelease.20210514034551",
"scratch-l10n": "3.11.20210510031549", "scratch-l10n": "3.11.20210513031505",
"scratch-paint": "0.2.0-prerelease.20210407203313", "scratch-paint": "0.2.0-prerelease.20210407203313",
"scratch-render": "0.1.0-prerelease.20210325231800", "scratch-render": "0.1.0-prerelease.20210325231800",
"scratch-render-fonts": "1.0.0-prerelease.20210401210003", "scratch-render-fonts": "1.0.0-prerelease.20210401210003",
"scratch-storage": "1.3.4", "scratch-storage": "1.3.4",
"scratch-svg-renderer": "0.2.0-prerelease.20210408171934", "scratch-svg-renderer": "0.2.0-prerelease.20210511195415",
"scratch-vm": "0.2.0-prerelease.20210510162256", "scratch-vm": "0.2.0-prerelease.20210510162256",
"startaudiocontext": "1.2.1", "startaudiocontext": "1.2.1",
"style-loader": "^0.23.0", "style-loader": "^0.23.0",
@ -21312,9 +21311,9 @@
"dev": true "dev": true
}, },
"scratch-l10n": { "scratch-l10n": {
"version": "3.11.20210510031549", "version": "3.11.20210513031505",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210510031549.tgz", "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210513031505.tgz",
"integrity": "sha512-WeoSa4zxtOcAGeaASqVcJcZu/QWoWtHohsP71ICbNcBpcAqnpPV1AhdGNrOQyiwuj0Vy/6CBuH9QYpS0IF+AkA==", "integrity": "sha512-BZagwqCz1/HmCeYURZGyY+wvw7h644PyO18ZWaFTtneYorukWNv3fT1ARmwwvCwzCY4FZwJ7ksogxTlQSFirDw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/cli": "^7.1.2", "@babel/cli": "^7.1.2",
@ -21401,9 +21400,9 @@
} }
}, },
"scratch-l10n": { "scratch-l10n": {
"version": "3.11.20210511031531", "version": "3.11.20210514031523",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210511031531.tgz", "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210514031523.tgz",
"integrity": "sha512-E+Gm8pSNgZhVEsdRXshkhk6MnKeQdbqXx3KSctaJ9wBibxZwRX3kB+6vvkt75zESfCuQ5f3iXdB6s2KavZzHaA==", "integrity": "sha512-SBsH0mYZJ35/X/eHizJWLE26xtFItjb2++hMoC4X1ZfkeYazBZYv3baxS50VfzGsZcWgVEKpQ2sMePctSwBN7Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/cli": "^7.1.2", "@babel/cli": "^7.1.2",
@ -21637,9 +21636,9 @@
} }
}, },
"scratch-svg-renderer": { "scratch-svg-renderer": {
"version": "0.2.0-prerelease.20210408171934", "version": "0.2.0-prerelease.20210511195415",
"resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20210408171934.tgz", "resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20210511195415.tgz",
"integrity": "sha512-kc59fyZlJ58ooW86VQo9oqXNzpR48RH7vObehekVyPq4FMRENwtv9gCZ5XitLPNsLEheFCJdfRVPkVsMAjhPYQ==", "integrity": "sha512-zeT93lfMeJNWhj8cLfNeDWTZT/fDS2Fnz6btCJpvE5AAyel+8VE1Y9hBb1OJ+ap8vjA1O31TnDApIylRmA/g5w==",
"dev": true, "dev": true,
"requires": { "requires": {
"base64-js": "1.2.1", "base64-js": "1.2.1",

View file

@ -126,7 +126,7 @@
"redux-mock-store": "^1.2.3", "redux-mock-store": "^1.2.3",
"redux-thunk": "2.0.1", "redux-thunk": "2.0.1",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20210510170231", "scratch-gui": "0.1.0-prerelease.20210514040957",
"scratch-l10n": "latest", "scratch-l10n": "latest",
"selenium-webdriver": "3.6.0", "selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0", "slick-carousel": "1.6.0",

View file

@ -6,7 +6,7 @@ const React = require('react');
require('./button.scss'); require('./button.scss');
const Button = props => { const Button = props => {
const classes = classNames('button', props.className, {'close-button': props.isCloseType}); const classes = classNames('button', props.className, {'forms-close-button': props.isCloseType});
return ( return (
<button <button

View file

@ -54,7 +54,7 @@ $pass-bg: $ui-aqua;
} }
} }
.close-button { .forms-close-button {
padding: 0; padding: 0;
position: absolute; position: absolute;
@ -69,6 +69,6 @@ $pass-bg: $ui-aqua;
line-height: 2rem; line-height: 2rem;
} }
.close-button img { .forms-close-button img {
padding-top: 0.5rem; padding-top: 0.5rem;
} }

View file

@ -8,6 +8,7 @@ const ToggleSlider = props => (
<label className={classNames('toggle-switch', props.className)} > <label className={classNames('toggle-switch', props.className)} >
<input <input
checked={props.checked} checked={props.checked}
disabled={props.disabled}
type="checkbox" type="checkbox"
onChange={props.onChange} onChange={props.onChange}
/> />
@ -17,6 +18,7 @@ const ToggleSlider = props => (
ToggleSlider.propTypes = { ToggleSlider.propTypes = {
checked: PropTypes.bool, checked: PropTypes.bool,
disabled: PropTypes.bool,
className: PropTypes.string, className: PropTypes.string,
onChange: PropTypes.func onChange: PropTypes.func
}; };

View file

@ -22,7 +22,7 @@ const ValidationMessage = props => (
ValidationMessage.propTypes = { ValidationMessage.propTypes = {
className: PropTypes.string, className: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,
mode: PropTypes.string mode: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
}; };
module.exports = ValidationMessage; module.exports = ValidationMessage;

View file

@ -0,0 +1,9 @@
<svg width="21" height="19" viewBox="0 0 21 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4923 14.0441C11.4621 14.0441 12.2482 14.7672 12.2482 15.659C12.2482 16.5509 11.4621 17.2739 10.4923 17.2739C9.52252 17.2739 8.73636 16.5509 8.73636 15.659C8.73636 14.7672 9.52252 14.0441 10.4923 14.0441ZM10.4923 7.58453C11.4621 7.58453 12.2482 8.30755 12.2482 9.19943C12.2482 10.0913 11.4621 10.8143 10.4923 10.8143C9.52252 10.8143 8.73636 10.0913 8.73636 9.19943C8.73636 8.30755 9.52252 7.58453 10.4923 7.58453ZM12.2482 2.73983C12.2482 1.84795 11.4621 1.12493 10.4923 1.12493C9.52252 1.12493 8.73636 1.84795 8.73636 2.73983C8.73636 3.63172 9.52252 4.35474 10.4923 4.35474C11.4621 4.35474 12.2482 3.63172 12.2482 2.73983Z" fill="#575E75" fill-opacity="0.6"/>
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="8" y="1" width="5" height="17">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4923 14.0441C11.4621 14.0441 12.2482 14.7672 12.2482 15.659C12.2482 16.5509 11.4621 17.2739 10.4923 17.2739C9.52252 17.2739 8.73636 16.5509 8.73636 15.659C8.73636 14.7672 9.52252 14.0441 10.4923 14.0441ZM10.4923 7.58453C11.4621 7.58453 12.2482 8.30755 12.2482 9.19943C12.2482 10.0913 11.4621 10.8143 10.4923 10.8143C9.52252 10.8143 8.73636 10.0913 8.73636 9.19943C8.73636 8.30755 9.52252 7.58453 10.4923 7.58453ZM12.2482 2.73983C12.2482 1.84795 11.4621 1.12493 10.4923 1.12493C9.52252 1.12493 8.73636 1.84795 8.73636 2.73983C8.73636 3.63172 9.52252 4.35474 10.4923 4.35474C11.4621 4.35474 12.2482 3.63172 12.2482 2.73983Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<rect x="0.93222" y="18.1711" width="17.9433" height="19.5104" transform="rotate(-90 0.93222 18.1711)" fill="#575E75" fill-opacity="0.6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,43 @@
/* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Dropdown from '../dropdown/dropdown.jsx';
import overflowIcon from './overflow-icon.svg';
import './overflow-menu.scss';
const OverflowMenu = ({children, dropdownAs, className}) => {
const [open, setOpen] = useState(false);
return (
<div className={classNames('overflow-menu-container', className)}>
<button
className="overflow-menu-trigger ignore-react-onclickoutside"
onClick={() => setOpen(!open)}
>
<img src={overflowIcon} />
</button>
{open && <Dropdown
isOpen
as={dropdownAs}
className="overflow-menu-dropdown"
onRequestClose={() => setOpen(false)}
>
{children}
</Dropdown>}
</div>
);
};
OverflowMenu.propTypes = {
children: PropTypes.node,
dropdownAs: PropTypes.string,
className: PropTypes.string
};
OverflowMenu.defaultProps = {
dropdownAs: 'ul'
};
export default OverflowMenu;

View file

@ -0,0 +1,50 @@
.overflow-menu-container {
display: flex;
position: relative;
.overflow-menu-trigger {
background: transparent;
border: none;
display: flex;
align-items: center;
}
.overflow-menu-dropdown {
border: 1px solid rgba(0, 0, 0, 0.15);
box-sizing: border-box;
box-shadow: 0px 2px 8px rgba(87, 94, 117, 0.5);
border-radius: 8px;
padding: 0;
margin: 30px 0 0 0;
right: unset; /* default dropdown aligns right edges, but we want left edges */
left: 0;
z-index: 1;
/* Include default styling for <li><button />... list */
li {
margin: 0;
padding: 0;
& + li {
border-top: 1px solid rgba(0, 0, 0, 0.15);
}
button {
display: flex;
align-items: center;
color: white;
font-weight: bold;
padding: 5px 10px;
background: none;
border: none;
width: 100%;
text-align: left;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
& > img {
margin: 0 10px 0 0;
}
}
}
}
}

View file

@ -127,6 +127,7 @@ module.exports.selectUsername = state => get(state, ['session', 'session', 'user
module.exports.selectToken = state => get(state, ['session', 'session', 'user', 'token'], null); module.exports.selectToken = state => get(state, ['session', 'session', 'user', 'token'], null);
module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false); module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false);
module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false); module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false);
module.exports.selectIsEducator = state => get(state, ['session', 'session', 'permissions', 'educator'], false);
// NB logged out user id as NaN so that it can never be used in equality testing since NaN !== NaN // NB logged out user id as NaN so that it can never be used in equality testing since NaN !== NaN
module.exports.selectUserId = state => get(state, ['session', 'session', 'user', 'id'], NaN); module.exports.selectUserId = state => get(state, ['session', 'session', 'user', 'id'], NaN);

View file

@ -18,7 +18,7 @@ const Errors = keyMirror({
INAPPROPRIATE: null, INAPPROPRIATE: null,
PERMISSION: null, PERMISSION: null,
THUMBNAIL_TOO_LARGE: null, THUMBNAIL_TOO_LARGE: null,
THUMBNAIL_MISSING: null, THUMBNAIL_INVALID: null,
TEXT_TOO_LONG: null, TEXT_TOO_LONG: null,
REQUIRED_FIELD: null, REQUIRED_FIELD: null,
UNHANDLED: null UNHANDLED: null
@ -111,7 +111,7 @@ const normalizeError = (err, body, res) => {
switch (body.errors[0]) { switch (body.errors[0]) {
case 'inappropriate-generic': return Errors.INAPPROPRIATE; case 'inappropriate-generic': return Errors.INAPPROPRIATE;
case 'thumbnail-too-large': return Errors.THUMBNAIL_TOO_LARGE; case 'thumbnail-too-large': return Errors.THUMBNAIL_TOO_LARGE;
case 'thumbnail-missing': return Errors.THUMBNAIL_MISSING; case 'image-invalid': return Errors.THUMBNAIL_INVALID;
case 'editable-text-too-long': return Errors.TEXT_TOO_LONG; case 'editable-text-too-long': return Errors.TEXT_TOO_LONG;
case 'This field is required.': return Errors.REQUIRED_FIELD; case 'This field is required.': return Errors.REQUIRED_FIELD;
default: return Errors.UNHANDLED; default: return Errors.UNHANDLED;

View file

@ -3,7 +3,7 @@ const keyMirror = require('keymirror');
const api = require('../lib/api'); const api = require('../lib/api');
const log = require('../lib/log'); const log = require('../lib/log');
const {selectUsername, selectToken} = require('./session'); const {selectUsername, selectToken, selectIsEducator} = require('./session');
const Status = keyMirror({ const Status = keyMirror({
FETCHED: null, FETCHED: null,
@ -22,6 +22,9 @@ const getInitialState = () => ({
followers: 0, followers: 0,
owner: null, owner: null,
// BEWARE: classroomId is only loaded if the user is an educator
classroomId: null,
rolesStatus: Status.NOT_FETCHED, rolesStatus: Status.NOT_FETCHED,
manager: false, manager: false,
curator: false, curator: false,
@ -95,6 +98,7 @@ const selectStudioLoadFailed = state => state.studio.infoStatus === Status.ERROR
const selectIsFetchingInfo = state => state.studio.infoStatus === Status.FETCHING; const selectIsFetchingInfo = state => state.studio.infoStatus === Status.FETCHING;
const selectIsFollowing = state => state.studio.following; const selectIsFollowing = state => state.studio.following;
const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCHING; const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCHING;
const selectClassroomId = state => state.studio.classroomId;
// Thunks // Thunks
const getInfo = () => ((dispatch, getState) => { const getInfo = () => ((dispatch, getState) => {
@ -138,6 +142,14 @@ const getRoles = () => ((dispatch, getState) => {
invited: body.invited invited: body.invited
})); }));
}); });
// Since the user is now loaded, it's a good time to check if the studio is part of a classroom
if (selectIsEducator(state)) {
api({uri: `/studios/${studioId}/classroom`}, (err, body, res) => {
// No error states for inability/problems loading classroom, just swallow them
if (!err && res.statusCode === 200 && body) dispatch(setInfo({classroomId: body.id}));
});
}
}); });
module.exports = { module.exports = {
@ -161,5 +173,6 @@ module.exports = {
selectStudioLoadFailed, selectStudioLoadFailed,
selectIsFetchingInfo, selectIsFetchingInfo,
selectIsFetchingRoles, selectIsFetchingRoles,
selectIsFollowing selectIsFollowing,
selectClassroomId
}; };

View file

@ -12,12 +12,31 @@ const Grid = require('../../components/grid/grid.jsx');
const TextArea = require('../../components/forms/textarea.jsx'); const TextArea = require('../../components/forms/textarea.jsx');
const SubNavigation = require('../../components/subnavigation/subnavigation.jsx'); const SubNavigation = require('../../components/subnavigation/subnavigation.jsx');
const Select = require('../../components/forms/select.jsx'); const Select = require('../../components/forms/select.jsx');
const OverflowMenu = require('../../components/overflow-menu/overflow-menu.jsx').default;
const exampleIcon = require('./example-icon.svg');
require('./components.scss'); require('./components.scss');
const Components = () => ( const Components = () => (
<div className="components"> <div className="components">
<div className="inner"> <div className="inner">
<h1>Overflow Menu</h1>
<div className="example-tile">
<OverflowMenu>
<li>
<button>
<img src={exampleIcon} />
Remove
</button>
</li>
<li>
<button>
<img src={exampleIcon} />
Upgrade!
</button>
</li>
</OverflowMenu>
</div>
<h1>Nav Bubbles</h1> <h1>Nav Bubbles</h1>
<div className="subnavigation"> <div className="subnavigation">
<SubNavigation> <SubNavigation>

View file

@ -18,6 +18,17 @@
width: 200px; width: 200px;
} }
.example-tile {
width: 200px;
height: 50px;
border: 1px solid $ui-border;
border-radius: 8px;
padding: 10px;
display: flex;
justify-content: flex-end;
align-items: center
}
.colors { .colors {
span { span {
display: inline-block; display: inline-block;

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
<title>Sound/General/Delete</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M4.54751641,6.99994966 L15.4523042,6.99994966 C15.7284466,6.99994966 15.9523042,7.22380729 15.9523042,7.49994966 C15.9523042,7.51506367 15.9516189,7.5301699 15.9502504,7.54522183 L15.1651793,16.1801783 C15.0715275,17.2102489 14.207924,17.9989808 13.1736049,17.9990897 L6.82662224,17.9997575 C5.79213514,17.9998663 4.92828345,17.2110677 4.83462539,16.180829 L4.04956981,7.54521753 C4.02456905,7.27020922 4.22724022,7.02700381 4.50224854,7.00200306 C4.51729904,7.00063483 4.53240384,6.99994966 4.54751641,6.99994966 Z M7.33333333,4 L7.88603796,2.34188612 C7.95409498,2.13771505 8.14516441,2 8.36037961,2 L11.6396204,2 C11.8548356,2 12.045905,2.13771505 12.113962,2.34188612 L12.6666667,4 L16.5,4 C16.7761424,4 17,4.22385763 17,4.5 L17,5.5 C17,5.77614237 16.7761424,6 16.5,6 L3.5,6 C3.22385763,6 3,5.77614237 3,5.5 L3,4.5 C3,4.22385763 3.22385763,4 3.5,4 L7.33333333,4 Z M8.38742589,4 L11.6125741,4 L11.2792408,3 L8.72075922,3 L8.38742589,4 Z M10,11.7204812 L11.5952436,10.1252376 C11.7905057,9.92997548 12.1070882,9.92997548 12.3023504,10.1252376 L12.3747624,10.1976496 C12.5700245,10.3929118 12.5700245,10.7094943 12.3747624,10.9047564 L10.7795188,12.5 L12.3747624,14.0952436 C12.5700245,14.2905057 12.5700245,14.6070882 12.3747624,14.8023504 L12.3023504,14.8747624 C12.1070882,15.0700245 11.7905057,15.0700245 11.5952436,14.8747624 L10,13.2795188 L8.40475641,14.8747624 C8.20949427,15.0700245 7.89291178,15.0700245 7.69764963,14.8747624 L7.62523762,14.8023504 C7.42997548,14.6070882 7.42997548,14.2905057 7.62523762,14.0952436 L9.22048121,12.5 L7.62523762,10.9047564 C7.42997548,10.7094943 7.42997548,10.3929118 7.62523762,10.1976496 L7.69764963,10.1252376 C7.89291178,9.92997548 8.20949427,9.92997548 8.40475641,10.1252376 L10,11.7204812 Z" id="path-1"></path>
</defs>
<g id="Sound/General/Delete" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Trash-Can"></g>
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -364,7 +364,7 @@ const Developers = () => (
<a href="https://github.com/LLK/">GitHub</a> <a href="https://github.com/LLK/">GitHub</a>
), ),
contactUsLink: ( contactUsLink: (
<a href="https://scratch.mit.edu/contact-us/"> <a href="/contact-us">
<FormattedMessage id="general.contactUs" /> <FormattedMessage id="general.contactUs" />
</a> </a>
) )

View file

@ -1,7 +1,7 @@
{ {
"developers.hereLinkText": "here", "developers.hereLinkText": "here",
"developers.title": "Scratch for Developers", "developers.title": "Scratch for Developers",
"developers.introLinkText": "Scratch Team at MIT", "developers.introLinkText": "Scratch Team",
"developers.intro": "On this page, youll find information about open source projects created and maintained by the {introLink}, as well as our thoughts on best practices for designing learning experiences for children.", "developers.intro": "On this page, youll find information about open source projects created and maintained by the {introLink}, as well as our thoughts on best practices for designing learning experiences for children.",
"developers.projectsTitle": "Projects", "developers.projectsTitle": "Projects",
"developers.principlesTitle": "Principles", "developers.principlesTitle": "Principles",

View file

@ -159,6 +159,10 @@
.avatar { .avatar {
margin-right: .5rem; margin-right: .5rem;
border-radius: 4px;
box-shadow: 0px 0px 0px 1px rgba(77, 151, 255, 0.25);
width: 3rem;
height: 3rem;
} }
.comment-body { .comment-body {

View file

@ -165,6 +165,13 @@ class Splash extends React.Component {
shouldShowHOCMiddleBanner () { shouldShowHOCMiddleBanner () {
return false; // we did not use this middle banner in last HoC return false; // we did not use this middle banner in last HoC
} }
shouldShowIntro () {
return (
this.props.sessionStatus === sessionActions.Status.FETCHED && // done fetching session
Object.keys(this.props.user).length === 0 && // no user session found
this.shouldShowHOCTopBanner() !== true
);
}
shouldShowDonateBanner () { shouldShowDonateBanner () {
return ( return (
this.state.dismissedDonateBanner === false && this.state.dismissedDonateBanner === false &&
@ -180,7 +187,7 @@ class Splash extends React.Component {
const showDonateBanner = this.shouldShowDonateBanner() || false; const showDonateBanner = this.shouldShowDonateBanner() || false;
const showHOCTopBanner = this.shouldShowHOCTopBanner() || false; const showHOCTopBanner = this.shouldShowHOCTopBanner() || false;
const showHOCMiddleBanner = this.shouldShowHOCMiddleBanner() || false; const showHOCMiddleBanner = this.shouldShowHOCMiddleBanner() || false;
const showIntro = showHOCTopBanner !== true; const showIntro = this.shouldShowIntro() || false;
const showWelcome = this.shouldShowWelcome(); const showWelcome = this.shouldShowWelcome();
const homepageRefreshStatus = this.getHomepageRefreshStatus(); const homepageRefreshStatus = this.getHomepageRefreshStatus();

View file

@ -0,0 +1,4 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.2531 7.47107C5.2531 8.16594 5.51512 8.80522 5.9491 9.28437C4.50749 10.1253 3.54431 11.8131 3.54431 13.7407C3.54431 14.5562 4.19249 15.1128 5.00253 15.4536C5.82565 15.7998 6.90529 15.9673 7.97081 15.9673C9.03633 15.9673 10.116 15.7998 10.9391 15.4536C11.7491 15.1128 12.3973 14.5562 12.3973 13.7407V13.7342C12.3973 13.6496 12.3973 13.5472 12.3834 13.4418C12.3334 12.4083 12.0039 11.4543 11.4801 10.6824C11.0859 10.1059 10.5795 9.62615 9.99447 9.28522C10.4318 8.80663 10.6887 8.1706 10.6887 7.47107C10.6887 5.96646 9.47307 4.75 7.97416 4.75C6.46912 4.75 5.2531 5.96602 5.2531 7.47107Z" fill="#FFFFFF"/>
<path d="M15.0868 5.71516C14.0615 5.71516 13.2332 6.54343 13.2332 7.56872C13.2332 8.00598 13.385 8.40622 13.6373 8.72277C13.1654 9.02039 12.7758 9.45561 12.5108 9.97982C12.4698 10.0609 12.476 10.1578 12.527 10.233C13.0003 10.9304 13.3012 11.7992 13.3451 12.7461C13.3501 12.8539 13.4236 12.9463 13.5274 12.9753C14.3677 13.2105 15.4588 13.2317 16.3459 13.0414C16.7892 12.9463 17.2012 12.7943 17.5086 12.5735C17.8194 12.3501 18.0443 12.0384 18.0443 11.637C18.0443 10.393 17.4408 9.29929 16.5294 8.72307C16.7819 8.40648 16.9338 8.00612 16.9338 7.56872C16.9338 6.5441 16.1062 5.71516 15.0868 5.71516Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
<title>Sound/General/Delete</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M4.54751641,6.99994966 L15.4523042,6.99994966 C15.7284466,6.99994966 15.9523042,7.22380729 15.9523042,7.49994966 C15.9523042,7.51506367 15.9516189,7.5301699 15.9502504,7.54522183 L15.1651793,16.1801783 C15.0715275,17.2102489 14.207924,17.9989808 13.1736049,17.9990897 L6.82662224,17.9997575 C5.79213514,17.9998663 4.92828345,17.2110677 4.83462539,16.180829 L4.04956981,7.54521753 C4.02456905,7.27020922 4.22724022,7.02700381 4.50224854,7.00200306 C4.51729904,7.00063483 4.53240384,6.99994966 4.54751641,6.99994966 Z M7.33333333,4 L7.88603796,2.34188612 C7.95409498,2.13771505 8.14516441,2 8.36037961,2 L11.6396204,2 C11.8548356,2 12.045905,2.13771505 12.113962,2.34188612 L12.6666667,4 L16.5,4 C16.7761424,4 17,4.22385763 17,4.5 L17,5.5 C17,5.77614237 16.7761424,6 16.5,6 L3.5,6 C3.22385763,6 3,5.77614237 3,5.5 L3,4.5 C3,4.22385763 3.22385763,4 3.5,4 L7.33333333,4 Z M8.38742589,4 L11.6125741,4 L11.2792408,3 L8.72075922,3 L8.38742589,4 Z M10,11.7204812 L11.5952436,10.1252376 C11.7905057,9.92997548 12.1070882,9.92997548 12.3023504,10.1252376 L12.3747624,10.1976496 C12.5700245,10.3929118 12.5700245,10.7094943 12.3747624,10.9047564 L10.7795188,12.5 L12.3747624,14.0952436 C12.5700245,14.2905057 12.5700245,14.6070882 12.3747624,14.8023504 L12.3023504,14.8747624 C12.1070882,15.0700245 11.7905057,15.0700245 11.5952436,14.8747624 L10,13.2795188 L8.40475641,14.8747624 C8.20949427,15.0700245 7.89291178,15.0700245 7.69764963,14.8747624 L7.62523762,14.8023504 C7.42997548,14.6070882 7.42997548,14.2905057 7.62523762,14.0952436 L9.22048121,12.5 L7.62523762,10.9047564 C7.42997548,10.7094943 7.42997548,10.3929118 7.62523762,10.1976496 L7.69764963,10.1252376 C7.89291178,9.92997548 8.20949427,9.92997548 8.40475641,10.1252376 L10,11.7204812 Z" id="path-1"></path>
</defs>
<g id="Sound/General/Delete" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Trash-Can"></g>
<g id="White" mask="url(#mask-2)" fill="#FFFFFF">
<rect id="Color" x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -7,16 +7,26 @@
"studio.title": "Title", "studio.title": "Title",
"studio.description": "Description", "studio.description": "Description",
"studio.thumbnail": "Thumbnail", "studio.thumbnail": "Thumbnail",
"studio.updateErrors.generic": "Something went wrong updating the studio.",
"studio.updateErrors.inappropriate": "That seems inappropriate. Please be respectful.",
"studio.updateErrors.textTooLong": "That is too long.",
"studio.updateErrors.requiredField": "This cannot be blank.",
"studio.updateErrors.thumbnailTooLarge": "Maximum file size is 512 KB and less than 500x500 pixels.",
"studio.updateErrors.thumbnailInvalid": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.",
"studio.projectsHeader": "Projects", "studio.projectsHeader": "Projects",
"studio.addProjectsHeader": "Add Projects", "studio.addProjectsHeader": "Add Projects",
"studio.addProject": "Add", "studio.addProject": "Add",
"studio.addProjectPlaceholder": "Project URL",
"studio.openToAll": "Anyone can add projects",
"studio.projectsEmptyCanAdd1": "Your studio is looking a little empty.", "studio.projectsEmptyCanAdd1": "Your studio is looking a little empty.",
"studio.projectsEmptyCanAdd2": "Add your first project!", "studio.projectsEmptyCanAdd2": "Add your first project!",
"studio.projectsEmpty1": "This studio has no projects yet.", "studio.projectsEmpty1": "This studio has no projects yet.",
"studio.projectsEmpty2": "Suggest projects you want to add in the comments!", "studio.projectsEmpty2": "Suggest projects you want to add in the comments!",
"studio.browseProjects": "Browse Projects", "studio.browseProjects": "Browse Projects",
"studio.projectErrors.checkUrl": "Could not add project. Check the URL and try again.",
"studio.creatorRole": "Studio Creator", "studio.creatorRole": "Studio Creator",
@ -28,17 +38,30 @@
"studio.curatorsHeader": "Curators", "studio.curatorsHeader": "Curators",
"studio.inviteCuratorsHeader": "Invite Curators", "studio.inviteCuratorsHeader": "Invite Curators",
"studio.inviteCurator": "Invite", "studio.inviteCurator": "Invite",
"studio.inviteCuratorPlaceholder": "Scratch Username",
"studio.curatorAcceptInvite": "Accept Invite", "studio.curatorAcceptInvite": "Accept Invite",
"studio.curatorsEmptyCanAdd1": "You dont have curators right now.", "studio.curatorsEmptyCanAdd1": "You dont have curators right now.",
"studio.curatorsEmptyCanAdd2": "Add some curators to collaborate with!", "studio.curatorsEmptyCanAdd2": "Add some curators to collaborate with!",
"studio.curatorsEmpty1": "This studio has no curators right now.", "studio.curatorsEmpty1": "This studio has no curators right now.",
"studio.curatorErrors.generic": "Could not invite curator.",
"studio.curatorErrors.alreadyCurator": "They are already part of the studio.",
"studio.curatorErrors.unknownUsername": "Could not invite a curator with that username.",
"studio.curatorErrors.tooFast": "You are adding curators too fast.",
"studio.remove": "Remove",
"studio.promote": "Promote",
"studio.commentsHeader": "Comments", "studio.commentsHeader": "Comments",
"studio.comments.toggleOff": "Commenting off",
"studio.comments.toggleOn": "Commenting on",
"studio.comments.turnedOff": "Sorry, comment posting has been turned off for this studio.",
"studio.sharedFilter": "Shared", "studio.sharedFilter": "Shared",
"studio.favoritedFilter": "Favorited", "studio.favoritedFilter": "Favorited",
"studio.recentFilter": "Recent", "studio.recentFilter": "Recent",
"studio.studentsFilter": "Students",
"studio.activityHeader": "Activity",
"studio.activityAddProjectToStudio": "{profileLink} added the project {projectLink}", "studio.activityAddProjectToStudio": "{profileLink} added the project {projectLink}",
"studio.activityRemoveProjectStudio": "{profileLink} removed the project {projectLink}", "studio.activityRemoveProjectStudio": "{profileLink} removed the project {projectLink}",
"studio.activityUpdateStudio": "{profileLink} made edits to the title, thumbnail, or description", "studio.activityUpdateStudio": "{profileLink} made edits to the title, thumbnail, or description",

View file

@ -11,13 +11,15 @@ const Errors = keyMirror({
SERVER: null, SERVER: null,
PERMISSION: null, PERMISSION: null,
UNKNOWN_PROJECT: null, UNKNOWN_PROJECT: null,
RATE_LIMIT: null RATE_LIMIT: null,
DUPLICATE: null
}); });
const normalizeError = (err, body, res) => { const normalizeError = (err, body, res) => {
if (err) return Errors.NETWORK; if (err) return Errors.NETWORK;
if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION; if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
if (res.statusCode === 404) return Errors.UNKNOWN_PROJECT; if (res.statusCode === 404) return Errors.UNKNOWN_PROJECT;
if (res.statusCode === 409) return Errors.DUPLICATE;
if (res.statusCode === 429) return Errors.RATE_LIMIT; if (res.statusCode === 429) return Errors.RATE_LIMIT;
if (res.statusCode !== 200) return Errors.SERVER; if (res.statusCode !== 200) return Errors.SERVER;
return null; return null;
@ -60,10 +62,24 @@ const generateProjectListItem = (postBody, infoBody) => ({
avatar: infoBody.author.profile.images avatar: infoBody.author.profile.images
}); });
const addProject = projectId => ((dispatch, getState) => new Promise((resolve, reject) => { const addProject = projectIdOrUrl => ((dispatch, getState) => new Promise((resolve, reject) => {
// Strings are passed by the open input, numbers by the project browser
let projectId = projectIdOrUrl;
if (typeof projectIdOrUrl === 'string') {
const matches = projectIdOrUrl.match(/(\d+)/g);
if (!matches) return reject(Errors.UNKNOWN_PROJECT);
// Take the last match, in case we are on localhost and there are port numbers, e.g.
projectId = parseInt(matches[matches.length - 1], 10);
}
const state = getState(); const state = getState();
const studioId = selectStudioId(state); const studioId = selectStudioId(state);
const token = selectToken(state); const token = selectToken(state);
// Check for existing duplicates before going to the server
if (projects.selector(state).items.filter(p => p.id === projectId).length !== 0) {
return reject(Errors.DUPLICATE);
}
api({ api({
uri: `/studios/${studioId}/project/${projectId}`, uri: `/studios/${studioId}/project/${projectId}`,
method: 'POST', method: 'POST',

View file

@ -1,6 +1,7 @@
import keyMirror from 'keymirror'; import keyMirror from 'keymirror';
import api from '../../../lib/api'; import api from '../../../lib/api';
import {selectUsername} from '../../../redux/session'; import {selectToken, selectUsername} from '../../../redux/session';
import {selectClassroomId} from '../../../redux/studio';
import {userProjects, projects} from './redux-modules'; import {userProjects, projects} from './redux-modules';
const Errors = keyMirror({ const Errors = keyMirror({
@ -12,13 +13,25 @@ const Errors = keyMirror({
const Filters = keyMirror({ const Filters = keyMirror({
SHARED: null, SHARED: null,
FAVORITED: null, FAVORITED: null,
RECENT: null RECENT: null,
STUDENTS: null
}); });
const Uris = { const Endpoints = {
[Filters.SHARED]: username => `/users/${username}/projects`, [Filters.SHARED]: state => ({
[Filters.FAVORITED]: username => `/users/${username}/favorites`, uri: `/users/${selectUsername(state)}/projects`
[Filters.RECENT]: username => `/users/${username}/recent` }),
[Filters.FAVORITED]: state => ({
uri: `/users/${selectUsername(state)}/favorites`
}),
[Filters.RECENT]: state => ({
uri: `/users/${selectUsername(state)}/projects/recentlyviewed`,
authentication: selectToken(state)
}),
[Filters.STUDENTS]: state => ({
uri: `/classrooms/${selectClassroomId(state)}/projects`,
authentication: selectToken(state)
})
}; };
const normalizeError = (err, body, res) => { const normalizeError = (err, body, res) => {
@ -30,14 +43,17 @@ const normalizeError = (err, body, res) => {
const loadUserProjects = type => ((dispatch, getState) => { const loadUserProjects = type => ((dispatch, getState) => {
const state = getState(); const state = getState();
const username = selectUsername(state);
const projectCount = userProjects.selector(state).items.length; const projectCount = userProjects.selector(state).items.length;
const projectsPerPage = 20; const projectsPerPage = 20;
const opts = {
...Endpoints[type](state),
params: {
limit: projectsPerPage,
offset: projectCount
}
};
dispatch(userProjects.actions.loading()); dispatch(userProjects.actions.loading());
api({ api(opts, (err, body, res) => {
uri: Uris[type](username),
params: {limit: projectsPerPage, offset: projectCount}
}, (err, body, res) => {
const error = normalizeError(err, body, res); const error = normalizeError(err, body, res);
if (error) return dispatch(userProjects.actions.error(error)); if (error) return dispatch(userProjects.actions.error(error));
const moreToLoad = body.length === projectsPerPage; const moreToLoad = body.length === projectsPerPage;

View file

@ -5,6 +5,7 @@ import {connect} from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {selectClassroomId} from '../../../redux/studio';
import {addProject, removeProject} from '../lib/studio-project-actions'; import {addProject, removeProject} from '../lib/studio-project-actions';
import {userProjects} from '../lib/redux-modules'; import {userProjects} from '../lib/redux-modules';
import {Filters, loadUserProjects, clearUserProjects} from '../lib/user-projects-actions'; import {Filters, loadUserProjects, clearUserProjects} from '../lib/user-projects-actions';
@ -16,10 +17,11 @@ import SubNavigation from '../../../components/subnavigation/subnavigation.jsx';
import UserProjectsTile from './user-projects-tile.jsx'; import UserProjectsTile from './user-projects-tile.jsx';
import './user-projects-modal.scss'; import './user-projects-modal.scss';
import {selectIsEducator} from '../../../redux/session';
const UserProjectsModal = ({ const UserProjectsModal = ({
items, error, loading, moreToLoad, onLoadMore, onClear, items, error, loading, moreToLoad, showStudentsFilter,
onAdd, onRemove, onRequestClose onLoadMore, onClear, onAdd, onRemove, onRequestClose
}) => { }) => {
const [filter, setFilter] = useState(Filters.SHARED); const [filter, setFilter] = useState(Filters.SHARED);
@ -60,6 +62,14 @@ const UserProjectsModal = ({
> >
<FormattedMessage id="studio.recentFilter" /> <FormattedMessage id="studio.recentFilter" />
</li> </li>
{showStudentsFilter &&
<li
className={classNames({active: filter === Filters.STUDENTS})}
onClick={() => setFilter(Filters.STUDENTS)}
>
<FormattedMessage id="studio.studentsFilter" />
</li>
}
</SubNavigation> </SubNavigation>
<ModalInnerContent className="user-projects-modal-content"> <ModalInnerContent className="user-projects-modal-content">
{error && <div>Error loading {filter}: {error}</div>} {error && <div>Error loading {filter}: {error}</div>}
@ -75,15 +85,18 @@ const UserProjectsModal = ({
onRemove={onRemove} onRemove={onRemove}
/> />
))} ))}
{moreToLoad &&
<div className="studio-projects-load-more"> <div className="studio-projects-load-more">
{loading ? <small>Loading...</small> : ( <button
moreToLoad ? className={classNames('button', {
<button onClick={() => onLoadMore(filter)}> 'mod-mutating': loading
})}
onClick={onLoadMore}
>
<FormattedMessage id="general.loadMore" /> <FormattedMessage id="general.loadMore" />
</button> : </button>
<small>No more to load</small>
)}
</div> </div>
}
</div> </div>
</ModalInnerContent> </ModalInnerContent>
</Modal> </Modal>
@ -91,6 +104,7 @@ const UserProjectsModal = ({
}; };
UserProjectsModal.propTypes = { UserProjectsModal.propTypes = {
showStudentsFilter: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.shape({ items: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.id, id: PropTypes.id,
image: PropTypes.string, image: PropTypes.string,
@ -108,7 +122,8 @@ UserProjectsModal.propTypes = {
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
...userProjects.selector(state) ...userProjects.selector(state),
showStudentsFilter: selectIsEducator(state) && selectClassroomId(state)
}); });
const mapDispatchToProps = ({ const mapDispatchToProps = ({

View file

@ -22,11 +22,14 @@
.user-projects-modal-content { .user-projects-modal-content {
padding: 0 30px 30px; padding: 0 30px 30px;
background: #E9F1FC; background: #E9F1FC;
max-height: 80vh; max-height: calc(100vh - 200px);
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain; overscroll-behavior: contain;
border-bottom-left-radius: 12px; border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px; border-bottom-right-radius: 12px;
@media #{$intermediate-and-smaller} {
& { max-height: calc(100vh - 105px); }
}
} }
} }

View file

@ -177,7 +177,9 @@ const StudioActivity = ({items, loading, error, moreToLoad, onLoadMore}) => {
return ( return (
<div className="studio-activity"> <div className="studio-activity">
<h2>Activity</h2> <div className="studio-header-container">
<h2><FormattedMessage id="studio.activityHeader" /></h2>
</div>
{loading && <div>Loading...</div>} {loading && <div>Loading...</div>}
{error && <Debug {error && <Debug
label="Error" label="Error"

View file

@ -2,12 +2,16 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import {selectStudioCommentsAllowed, selectIsFetchingInfo} from '../../redux/studio'; import {selectStudioCommentsAllowed, selectIsFetchingInfo} from '../../redux/studio';
import { import {
mutateStudioCommentsAllowed, selectIsMutatingCommentsAllowed, selectCommentsAllowedMutationError mutateStudioCommentsAllowed, selectIsMutatingCommentsAllowed, selectCommentsAllowedMutationError
} from '../../redux/studio-mutations'; } from '../../redux/studio-mutations';
import ToggleSlider from '../../components/forms/toggle-slider.jsx';
const StudioCommentsAllowed = ({ const StudioCommentsAllowed = ({
commentsAllowedError, isFetching, isMutating, commentsAllowed, handleUpdate commentsAllowedError, isFetching, isMutating, commentsAllowed, handleUpdate
}) => ( }) => (
@ -16,16 +20,20 @@ const StudioCommentsAllowed = ({
<h4>Fetching...</h4> <h4>Fetching...</h4>
) : ( ) : (
<div> <div>
<label> {commentsAllowed ? (
<input <FormattedMessage id="studio.comments.toggleOn" />
) : (
<FormattedMessage id="studio.comments.toggleOff" />
)}
<ToggleSlider
disabled={isMutating} disabled={isMutating}
type="checkbox"
checked={commentsAllowed} checked={commentsAllowed}
className={classNames('comments-allowed-input', {
'mod-mutating': isMutating
})}
onChange={e => handleUpdate(e.target.checked)} onChange={e => handleUpdate(e.target.checked)}
/> />
<span>{commentsAllowed ? 'Comments allowed' : 'Comments not allowed'}</span>
{commentsAllowedError && <div>Error mutating commentsAllowed: {commentsAllowedError}</div>} {commentsAllowedError && <div>Error mutating commentsAllowed: {commentsAllowedError}</div>}
</label>
</div> </div>
)} )}
</div> </div>

View file

@ -56,9 +56,11 @@ const StudioComments = ({
return ( return (
<div> <div>
<div className="studio-header-container">
<h2><FormattedMessage id="studio.commentsHeader" /></h2> <h2><FormattedMessage id="studio.commentsHeader" /></h2>
{canEditCommentsAllowed && <StudioCommentsAllowed />} {canEditCommentsAllowed && <StudioCommentsAllowed />}
<div> </div>
<div className="studio-compose-container">
{shouldShowCommentComposer && commentsAllowed && {shouldShowCommentComposer && commentsAllowed &&
<ComposeComment <ComposeComment
postURI={postURI} postURI={postURI}

View file

@ -3,12 +3,24 @@ import React, {useState} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
import {inviteCurator} from './lib/studio-member-actions'; import {Errors, inviteCurator} from './lib/studio-member-actions';
import FlexRow from '../../components/flex-row/flex-row.jsx'; import ValidationMessage from '../../components/forms/validation-message.jsx';
const StudioCuratorInviter = ({onSubmit}) => { const errorToMessageId = error => {
switch (error) {
case Errors.NETWORK: return 'studio.curatorErrors.generic';
case Errors.SERVER: return 'studio.curatorErrors.generic';
case Errors.PERMISSION: return 'studio.curatorErrors.generic';
case Errors.DUPLICATE: return 'studio.curatorErrors.alreadyCurator';
case Errors.UNKNOWN_USERNAME: return 'studio.curatorErrors.unknownUsername';
case Errors.RATE_LIMIT: return 'studio.curatorErrors.tooFast';
default: return 'studio.curatorErrors.generic';
}
};
const StudioCuratorInviter = ({intl, onSubmit}) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@ -23,11 +35,19 @@ const StudioCuratorInviter = ({onSubmit}) => {
return ( return (
<div className="studio-adder-section"> <div className="studio-adder-section">
<h3><FormattedMessage id="studio.inviteCuratorsHeader" /></h3> <h3><FormattedMessage id="studio.inviteCuratorsHeader" /></h3>
<FlexRow> <div className="studio-adder-row">
{error && <div className="studio-adder-error">
<ValidationMessage
mode="error"
className="validation-left"
message={<FormattedMessage id={errorToMessageId(error)} />}
/>
</div>}
<input <input
className={classNames({'mod-form-error': error})}
disabled={submitting} disabled={submitting}
type="text" type="text"
placeholder="<username>" placeholder={intl.formatMessage({id: 'studio.inviteCuratorPlaceholder'})}
value={value} value={value}
onKeyDown={e => e.key === 'Enter' && submit()} onKeyDown={e => e.key === 'Enter' && submit()}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
@ -36,17 +56,17 @@ const StudioCuratorInviter = ({onSubmit}) => {
className={classNames('button', { className={classNames('button', {
'mod-mutating': submitting 'mod-mutating': submitting
})} })}
disabled={submitting} disabled={submitting || value === ''}
onClick={submit} onClick={submit}
><FormattedMessage id="studio.inviteCurator" /></button> ><FormattedMessage id="studio.inviteCurator" /></button>
{error && <div>{error}</div>} </div>
</FlexRow>
</div> </div>
); );
}; };
StudioCuratorInviter.propTypes = { StudioCuratorInviter.propTypes = {
onSubmit: PropTypes.func onSubmit: PropTypes.func,
intl: intlShape
}; };
const mapStateToProps = () => ({}); const mapStateToProps = () => ({});
@ -55,4 +75,4 @@ const mapDispatchToProps = ({
onSubmit: inviteCurator onSubmit: inviteCurator
}); });
export default connect(mapStateToProps, mapDispatchToProps)(StudioCuratorInviter); export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(StudioCuratorInviter));

View file

@ -20,7 +20,9 @@ const StudioCurators = ({
}, []); }, []);
return (<div className="studio-members"> return (<div className="studio-members">
<div className="studio-header-container">
<h2><FormattedMessage id="studio.curatorsHeader" /></h2> <h2><FormattedMessage id="studio.curatorsHeader" /></h2>
</div>
{canInviteCurators && <CuratorInviter />} {canInviteCurators && <CuratorInviter />}
{showCuratorInvite && <CuratorInvite />} {showCuratorInvite && <CuratorInvite />}
{error && <Debug {error && <Debug

View file

@ -2,23 +2,36 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio'; import {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo} from '../../redux/studio-permissions'; import {selectCanEditInfo} from '../../redux/studio-permissions';
import { import {
mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError Errors, mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
} from '../../redux/studio-mutations'; } from '../../redux/studio-mutations';
import classNames from 'classnames';
import ValidationMessage from '../../components/forms/validation-message.jsx';
const errorToMessageId = error => {
switch (error) {
case Errors.INAPPROPRIATE: return 'studio.updateErrors.inappropriate';
case Errors.TEXT_TOO_LONG: return 'studio.updateErrors.textTooLong';
case Errors.REQUIRED_FIELD: return 'studio.updateErrors.requiredField';
default: return 'studio.updateErrors.generic';
}
};
const StudioDescription = ({ const StudioDescription = ({
descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate
}) => { }) => {
const fieldClassName = classNames('studio-description', { const fieldClassName = classNames('studio-description', {
'mod-fetching': isFetching, 'mod-fetching': isFetching,
'mod-mutating': isMutating 'mod-mutating': isMutating,
'mod-form-error': !!descriptionError
}); });
return ( return (
<React.Fragment> <div className="studio-info-section">
<textarea <textarea
rows="20" rows="20"
className={fieldClassName} className={fieldClassName}
@ -27,8 +40,11 @@ const StudioDescription = ({
onBlur={e => e.target.value !== description && onBlur={e => e.target.value !== description &&
handleUpdate(e.target.value)} handleUpdate(e.target.value)}
/> />
{descriptionError && <div>Error mutating description: {descriptionError}</div>} {descriptionError && <ValidationMessage
</React.Fragment> mode="error"
message={<FormattedMessage id={errorToMessageId(descriptionError)} />}
/>}
</div>
); );
}; };

View file

@ -19,7 +19,7 @@ const StudioFollow = ({
handleFollow handleFollow
}) => { }) => {
if (!canFollow) return null; if (!canFollow) return null;
const fieldClassName = classNames('button', { const fieldClassName = classNames('button', 'studio-follow-button', {
'mod-mutating': isMutating 'mod-mutating': isMutating
}); });
return ( return (

View file

@ -2,19 +2,30 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {selectStudioImage, selectIsFetchingInfo} from '../../redux/studio'; import {selectStudioImage, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo} from '../../redux/studio-permissions'; import {selectCanEditInfo} from '../../redux/studio-permissions';
import { import {
mutateStudioImage, selectIsMutatingImage, selectImageMutationError Errors, mutateStudioImage, selectIsMutatingImage, selectImageMutationError
} from '../../redux/studio-mutations'; } from '../../redux/studio-mutations';
import classNames from 'classnames';
import ValidationMessage from '../../components/forms/validation-message.jsx';
const errorToMessageId = error => {
switch (error) {
case Errors.THUMBNAIL_INVALID: return 'studio.updateErrors.thumbnailInvalid';
case Errors.THUMBNAIL_TOO_LARGE: return 'studio.updateErrors.thumbnailTooLarge';
default: return 'studio.updateErrors.generic';
}
};
const blankImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; const blankImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const StudioImage = ({ const StudioImage = ({
imageError, isFetching, isMutating, image, canEditInfo, handleUpdate imageError, isFetching, isMutating, image, canEditInfo, handleUpdate
}) => { }) => {
const fieldClassName = classNames('studio-image', { const fieldClassName = classNames('studio-info-section', {
'mod-fetching': isFetching, 'mod-fetching': isFetching,
'mod-mutating': isMutating 'mod-mutating': isMutating
}); });
@ -36,7 +47,10 @@ const StudioImage = ({
e.target.value = ''; e.target.value = '';
}} }}
/> />
{imageError && <div>Error mutating image: {imageError}</div>} {imageError && <ValidationMessage
mode="error"
message={<FormattedMessage id={errorToMessageId(imageError)} />}
/>}
</React.Fragment> </React.Fragment>
} }
</div> </div>

View file

@ -17,7 +17,9 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
return ( return (
<div className="studio-members"> <div className="studio-members">
<div className="studio-header-container">
<h2><FormattedMessage id="studio.managersHeader" /></h2> <h2><FormattedMessage id="studio.managersHeader" /></h2>
</div>
{error && <Debug {error && <Debug
label="Error" label="Error"
data={error} data={error}

View file

@ -14,6 +14,10 @@ import {
removeManager removeManager
} from './lib/studio-member-actions'; } from './lib/studio-member-actions';
import OverflowMenu from '../../components/overflow-menu/overflow-menu.jsx';
import removeIcon from './icons/remove-icon.svg';
import promoteIcon from './icons/curator-icon.svg';
const StudioMemberTile = ({ const StudioMemberTile = ({
canRemove, canPromote, onRemove, onPromote, isCreator, // mapState props canRemove, canPromote, onRemove, onPromote, isCreator, // mapState props
username, image // own props username, image // own props
@ -36,25 +40,11 @@ const StudioMemberTile = ({
>{username}</a> >{username}</a>
{isCreator && <div className="studio-member-role"><FormattedMessage id="studio.creatorRole" /></div>} {isCreator && <div className="studio-member-role"><FormattedMessage id="studio.creatorRole" /></div>}
</div> </div>
{canRemove && {(canRemove || canPromote) &&
<OverflowMenu>
{canPromote && <li>
<button <button
className={classNames('studio-member-remove', { className={classNames({
'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 'mod-mutating': submitting
})} })}
disabled={submitting} disabled={submitting}
@ -66,7 +56,31 @@ const StudioMemberTile = ({
setSubmitting(false); setSubmitting(false);
}); });
}} }}
>🆙</button> >
<img src={promoteIcon} />
<FormattedMessage id="studio.promote" />
</button>
</li>}
{canRemove && <li>
<button
className={classNames({
'mod-mutating': submitting
})}
disabled={submitting}
onClick={() => {
setSubmitting(true);
setError(null);
onRemove(username).catch(e => {
setError(e);
setSubmitting(false);
});
}}
>
<img src={removeIcon} />
<FormattedMessage id="studio.remove" />
</button>
</li>}
</OverflowMenu>
} }
{error && <div>{error}</div>} {error && <div>{error}</div>}
</div> </div>

View file

@ -2,12 +2,16 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import {selectStudioOpenToAll, selectIsFetchingInfo} from '../../redux/studio'; import {selectStudioOpenToAll, selectIsFetchingInfo} from '../../redux/studio';
import { import {
mutateStudioOpenToAll, selectIsMutatingOpenToAll, selectOpenToAllMutationError mutateStudioOpenToAll, selectIsMutatingOpenToAll, selectOpenToAllMutationError
} from '../../redux/studio-mutations'; } from '../../redux/studio-mutations';
import ToggleSlider from '../../components/forms/toggle-slider.jsx';
const StudioOpenToAll = ({ const StudioOpenToAll = ({
openToAllError, isFetching, isMutating, openToAll, handleUpdate openToAllError, isFetching, isMutating, openToAll, handleUpdate
}) => ( }) => (
@ -16,16 +20,16 @@ const StudioOpenToAll = ({
<h4>Fetching...</h4> <h4>Fetching...</h4>
) : ( ) : (
<div> <div>
<label> <FormattedMessage id="studio.openToAll" />
<input <ToggleSlider
disabled={isMutating} disabled={isMutating}
type="checkbox"
checked={openToAll} checked={openToAll}
className={classNames('open-to-all-input', {
'mod-mutating': isMutating
})}
onChange={e => handleUpdate(e.target.checked)} onChange={e => handleUpdate(e.target.checked)}
/> />
<span>{openToAll ? 'Open to all' : 'Not open to all'}</span>
{openToAllError && <div>Error mutating openToAll: {openToAllError}</div>} {openToAllError && <div>Error mutating openToAll: {openToAllError}</div>}
</label>
</div> </div>
)} )}
</div> </div>

View file

@ -3,13 +3,13 @@ import React, {useState} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
import {addProject} from './lib/studio-project-actions'; import {addProject} from './lib/studio-project-actions';
import UserProjectsModal from './modals/user-projects-modal.jsx'; import UserProjectsModal from './modals/user-projects-modal.jsx';
import FlexRow from '../../components/flex-row/flex-row.jsx'; import ValidationMessage from '../../components/forms/validation-message.jsx';
const StudioProjectAdder = ({onSubmit}) => { const StudioProjectAdder = ({intl, onSubmit}) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@ -25,11 +25,19 @@ const StudioProjectAdder = ({onSubmit}) => {
return ( return (
<div className="studio-adder-section"> <div className="studio-adder-section">
<h3><FormattedMessage id="studio.addProjectsHeader" /></h3> <h3><FormattedMessage id="studio.addProjectsHeader" /></h3>
<FlexRow> <div className="studio-adder-row">
{error && <div className="studio-adder-error">
<ValidationMessage
mode="error"
className="validation-left"
message={<FormattedMessage id="studio.projectErrors.checkUrl" />}
/>
</div>}
<input <input
className={classNames({'mod-form-error': error})}
disabled={submitting} disabled={submitting}
type="text" type="text"
placeholder="<project id>" placeholder={intl.formatMessage({id: 'studio.addProjectPlaceholder'})}
value={value} value={value}
onKeyDown={e => e.key === 'Enter' && submit()} onKeyDown={e => e.key === 'Enter' && submit()}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
@ -38,10 +46,9 @@ const StudioProjectAdder = ({onSubmit}) => {
className={classNames('button', { className={classNames('button', {
'mod-mutating': submitting 'mod-mutating': submitting
})} })}
disabled={submitting} disabled={submitting || value === ''}
onClick={submit} onClick={submit}
><FormattedMessage id="studio.addProject" /></button> ><FormattedMessage id="studio.addProject" /></button>
{error && <div>{error}</div>}
<div className="studio-adder-vertical-divider" /> <div className="studio-adder-vertical-divider" />
<button <button
className="button" className="button"
@ -50,13 +57,14 @@ const StudioProjectAdder = ({onSubmit}) => {
<FormattedMessage id="studio.browseProjects" /> <FormattedMessage id="studio.browseProjects" />
</button> </button>
{modalOpen && <UserProjectsModal onRequestClose={() => setModalOpen(false)} />} {modalOpen && <UserProjectsModal onRequestClose={() => setModalOpen(false)} />}
</FlexRow> </div>
</div> </div>
); );
}; };
StudioProjectAdder.propTypes = { StudioProjectAdder.propTypes = {
onSubmit: PropTypes.func onSubmit: PropTypes.func,
intl: intlShape
}; };
const mapStateToProps = () => ({}); const mapStateToProps = () => ({});
@ -65,4 +73,4 @@ const mapDispatchToProps = ({
onSubmit: addProject onSubmit: addProject
}); });
export default connect(mapStateToProps, mapDispatchToProps)(StudioProjectAdder); export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(StudioProjectAdder));

View file

@ -3,10 +3,14 @@ import React, {useState} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {selectCanRemoveProject} from '../../redux/studio-permissions'; import {selectCanRemoveProject} from '../../redux/studio-permissions';
import {removeProject} from './lib/studio-project-actions'; import {removeProject} from './lib/studio-project-actions';
import OverflowMenu from '../../components/overflow-menu/overflow-menu.jsx';
import removeIcon from './icons/remove-icon.svg';
const StudioProjectTile = ({ const StudioProjectTile = ({
canRemove, onRemove, // mapState props canRemove, onRemove, // mapState props
id, title, image, avatar, username // own props id, title, image, avatar, username // own props
@ -41,8 +45,10 @@ const StudioProjectTile = ({
>{username}</a> >{username}</a>
</div> </div>
{canRemove && {canRemove &&
<OverflowMenu>
<li>
<button <button
className={classNames('studio-project-remove', { className={classNames({
'mod-mutating': submitting 'mod-mutating': submitting
})} })}
disabled={submitting} disabled={submitting}
@ -55,9 +61,13 @@ const StudioProjectTile = ({
setSubmitting(false); setSubmitting(false);
}); });
}} }}
></button> >
<img src={removeIcon} />
<FormattedMessage id="studio.remove" />
</button></li>
</OverflowMenu>
} }
{error && <div>{error}</div>} {error && <div>{error}</div>} {/* TODO where do these errors go? */}
</div> </div>
</div> </div>
); );

View file

@ -21,8 +21,10 @@ const StudioProjects = ({
return ( return (
<div className="studio-projects"> <div className="studio-projects">
<div className="studio-header-container">
<h2><FormattedMessage id="studio.projectsHeader" /></h2> <h2><FormattedMessage id="studio.projectsHeader" /></h2>
{canEditOpenToAll && <StudioOpenToAll />} {canEditOpenToAll && <StudioOpenToAll />}
</div>
{canAddProjects && <StudioProjectAdder />} {canAddProjects && <StudioProjectAdder />}
{error && <Debug {error && <Debug
label="Error" label="Error"

View file

@ -2,21 +2,38 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio'; import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo} from '../../redux/studio-permissions'; import {selectCanEditInfo} from '../../redux/studio-permissions';
import {mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations'; import {Errors, mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
import classNames from 'classnames'; import ValidationMessage from '../../components/forms/validation-message.jsx';
/*
TODO
- no newlines in studio title
- Correct display in read-only mode
- validation message
*/
const errorToMessageId = error => {
switch (error) {
case Errors.INAPPROPRIATE: return 'studio.updateErrors.inappropriate';
case Errors.TEXT_TOO_LONG: return 'studio.updateErrors.textTooLong';
case Errors.REQUIRED_FIELD: return 'studio.updateErrors.requiredField';
default: return 'studio.updateErrors.generic';
}
};
const StudioTitle = ({ const StudioTitle = ({
titleError, isFetching, isMutating, title, canEditInfo, handleUpdate titleError, isFetching, isMutating, title, canEditInfo, handleUpdate
}) => { }) => {
const fieldClassName = classNames('studio-title', { const fieldClassName = classNames('studio-title', {
'mod-fetching': isFetching, 'mod-fetching': isFetching,
'mod-mutating': isMutating 'mod-mutating': isMutating,
'mod-form-error': !!titleError
}); });
return ( return (
<React.Fragment> <div className="studio-info-section">
<textarea <textarea
className={fieldClassName} className={fieldClassName}
disabled={isMutating || !canEditInfo || isFetching} disabled={isMutating || !canEditInfo || isFetching}
@ -24,8 +41,11 @@ const StudioTitle = ({
onBlur={e => e.target.value !== title && onBlur={e => e.target.value !== title &&
handleUpdate(e.target.value)} handleUpdate(e.target.value)}
/> />
{titleError && <div>Error mutating title: {titleError}</div>} {titleError && <ValidationMessage
</React.Fragment> mode="error"
message={<FormattedMessage id={errorToMessageId(titleError)} />}
/>}
</div>
); );
}; };

View file

@ -13,6 +13,14 @@ $radius: 8px;
min-width: auto; min-width: auto;
margin: 50px auto; margin: 50px auto;
display: block; display: block;
padding-top: 40px;
/* WAT Why does everything center at smaller widths??!! */
@media #{$intermediate-and-smaller} {
& {
text-align: unset !important;
}
}
.studio-shell { .studio-shell {
padding: 0 20px; padding: 0 20px;
@ -47,17 +55,37 @@ $radius: 8px;
border: 2px dashed $ui-blue-25percent; border: 2px dashed $ui-blue-25percent;
border-radius: $radius; border-radius: $radius;
resize: none; resize: none;
width: 300px;
&:disabled { border-color: transparent; } &:disabled { border-color: transparent; }
} }
.studio-info-section {
position: relative;
.validation-message {
margin-top: .5rem;
box-sizing: border-box;
}
}
.studio-title { .studio-title {
font-size: 28px; font-size: 28px;
font-weight: 500; font-weight: 700;
} }
.studio-title:disabled {
padding: 0;
margin: 0;
}
.studio-description:disabled { .studio-description:disabled {
background: $ui-blue-10percent; background: $ui-blue-10percent;
} }
.studio-follow-button {
padding-top: 14px;
padding-bottom: 14px;
font-size: 14px;
margin: 0;
}
} }
.studio-report-modal { .studio-report-modal {
@ -110,6 +138,7 @@ $radius: 8px;
.studio-tab-nav { .studio-tab-nav {
border-bottom: 1px solid $active-dark-gray; border-bottom: 1px solid $active-dark-gray;
padding-bottom: 8px; padding-bottom: 8px;
font-size: 14px;
li { background: rgba(0, 0, 0, 0.15); } li { background: rgba(0, 0, 0, 0.15); }
.active > li { background: $ui-blue; } .active > li { background: $ui-blue; }
} }
@ -150,7 +179,7 @@ $radius: 8px;
} }
.studio-project-bottom { .studio-project-bottom {
display: flex; display: flex;
padding: 10px 6px 10px 12px; padding: 6px 4px 6px 10px;
justify-content: space-between; justify-content: space-between;
} }
.studio-project-avatar { .studio-project-avatar {
@ -267,32 +296,52 @@ $radius: 8px;
color: #4C97FF; color: #4C97FF;
} }
.flex-row { .studio-adder-row {
margin: 0 -6px; display: flex;
& > * { flex-wrap: wrap-reverse; /* so error goes below at small sizes */
margin: 0 6px;
.studio-adder-error {
position: relative;
.validation-message {
transform: none;
width: 200px;
}
@media #{$intermediate-and-smaller} {
& {
width: 100%;
margin-top: .5rem;
.validation-message {
max-width: 100%;
width: 100%;
box-sizing: border-box;
}
}
} }
} }
input { input {
flex-grow: 1; flex-grow: 1;
display: inline-block; display: inline-block;
margin: .5em 0;
border: 1px solid $ui-border; border: 1px solid $ui-border;
border-radius: .5rem; border-radius: .5rem;
padding: 1em 1.25em; padding: 1em 1.25em;
font-size: .8rem; font-size: .8rem;
margin-inline-end: 6px;
} }
button { button {
flex-grow: 0; flex-grow: 0;
margin: 0;
} }
.studio-adder-vertical-divider { .studio-adder-vertical-divider {
margin: 0 6px;
border: 1px solid $ui-border; border: 1px solid $ui-border;
align-self: stretch; align-self: stretch;
} }
} }
}
.studio-activity { .studio-activity {
.studio-messages-list { .studio-messages-list {
@ -314,6 +363,23 @@ $radius: 8px;
} }
} }
.studio-header-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-top: 20px;
padding-bottom: 10px;
h2 {
font-size: 28px;
}
}
.studio-compose-container {
padding-top: 8px;
}
.studio-empty { .studio-empty {
grid-column: 1 / -1; /* take up all columns */ grid-column: 1 / -1; /* take up all columns */
text-align: center; text-align: center;
@ -362,3 +428,7 @@ $radius: 8px;
.mod-clickable { .mod-clickable {
cursor: pointer; cursor: pointer;
} }
.mod-form-error { /* When a field contains a value is causing an error */
border-color: $ui-orange !important;
}