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

View file

@ -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.20210510170231",
"scratch-gui": "0.1.0-prerelease.20210514040957",
"scratch-l10n": "latest",
"selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0",

View file

@ -6,7 +6,7 @@ const React = require('react');
require('./button.scss');
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 (
<button

View file

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

View file

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

View file

@ -22,7 +22,7 @@ const ValidationMessage = props => (
ValidationMessage.propTypes = {
className: PropTypes.string,
message: PropTypes.string,
mode: PropTypes.string
mode: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
};
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.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], 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
module.exports.selectUserId = state => get(state, ['session', 'session', 'user', 'id'], NaN);

View file

@ -18,7 +18,7 @@ const Errors = keyMirror({
INAPPROPRIATE: null,
PERMISSION: null,
THUMBNAIL_TOO_LARGE: null,
THUMBNAIL_MISSING: null,
THUMBNAIL_INVALID: null,
TEXT_TOO_LONG: null,
REQUIRED_FIELD: null,
UNHANDLED: null
@ -111,7 +111,7 @@ const normalizeError = (err, body, res) => {
switch (body.errors[0]) {
case 'inappropriate-generic': return Errors.INAPPROPRIATE;
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 'This field is required.': return Errors.REQUIRED_FIELD;
default: return Errors.UNHANDLED;

View file

@ -3,7 +3,7 @@ const keyMirror = require('keymirror');
const api = require('../lib/api');
const log = require('../lib/log');
const {selectUsername, selectToken} = require('./session');
const {selectUsername, selectToken, selectIsEducator} = require('./session');
const Status = keyMirror({
FETCHED: null,
@ -22,6 +22,9 @@ const getInitialState = () => ({
followers: 0,
owner: null,
// BEWARE: classroomId is only loaded if the user is an educator
classroomId: null,
rolesStatus: Status.NOT_FETCHED,
manager: false,
curator: false,
@ -95,6 +98,7 @@ const selectStudioLoadFailed = state => state.studio.infoStatus === Status.ERROR
const selectIsFetchingInfo = state => state.studio.infoStatus === Status.FETCHING;
const selectIsFollowing = state => state.studio.following;
const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCHING;
const selectClassroomId = state => state.studio.classroomId;
// Thunks
const getInfo = () => ((dispatch, getState) => {
@ -138,6 +142,14 @@ const getRoles = () => ((dispatch, getState) => {
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 = {
@ -161,5 +173,6 @@ module.exports = {
selectStudioLoadFailed,
selectIsFetchingInfo,
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 SubNavigation = require('../../components/subnavigation/subnavigation.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');
const Components = () => (
<div className="components">
<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>
<div className="subnavigation">
<SubNavigation>

View file

@ -18,6 +18,17 @@
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 {
span {
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>
),
contactUsLink: (
<a href="https://scratch.mit.edu/contact-us/">
<a href="/contact-us">
<FormattedMessage id="general.contactUs" />
</a>
)

View file

@ -1,7 +1,7 @@
{
"developers.hereLinkText": "here",
"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.projectsTitle": "Projects",
"developers.principlesTitle": "Principles",

View file

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

View file

@ -165,6 +165,13 @@ class Splash extends React.Component {
shouldShowHOCMiddleBanner () {
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 () {
return (
this.state.dismissedDonateBanner === false &&
@ -180,7 +187,7 @@ class Splash extends React.Component {
const showDonateBanner = this.shouldShowDonateBanner() || false;
const showHOCTopBanner = this.shouldShowHOCTopBanner() || false;
const showHOCMiddleBanner = this.shouldShowHOCMiddleBanner() || false;
const showIntro = showHOCTopBanner !== true;
const showIntro = this.shouldShowIntro() || false;
const showWelcome = this.shouldShowWelcome();
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.description": "Description",
"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.addProjectsHeader": "Add Projects",
"studio.addProject": "Add",
"studio.addProjectPlaceholder": "Project URL",
"studio.openToAll": "Anyone can add projects",
"studio.projectsEmptyCanAdd1": "Your studio is looking a little empty.",
"studio.projectsEmptyCanAdd2": "Add your first project!",
"studio.projectsEmpty1": "This studio has no projects yet.",
"studio.projectsEmpty2": "Suggest projects you want to add in the comments!",
"studio.browseProjects": "Browse Projects",
"studio.projectErrors.checkUrl": "Could not add project. Check the URL and try again.",
"studio.creatorRole": "Studio Creator",
@ -28,17 +38,30 @@
"studio.curatorsHeader": "Curators",
"studio.inviteCuratorsHeader": "Invite Curators",
"studio.inviteCurator": "Invite",
"studio.inviteCuratorPlaceholder": "Scratch Username",
"studio.curatorAcceptInvite": "Accept Invite",
"studio.curatorsEmptyCanAdd1": "You dont have curators right now.",
"studio.curatorsEmptyCanAdd2": "Add some curators to collaborate with!",
"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.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.favoritedFilter": "Favorited",
"studio.recentFilter": "Recent",
"studio.studentsFilter": "Students",
"studio.activityHeader": "Activity",
"studio.activityAddProjectToStudio": "{profileLink} added the project {projectLink}",
"studio.activityRemoveProjectStudio": "{profileLink} removed the project {projectLink}",
"studio.activityUpdateStudio": "{profileLink} made edits to the title, thumbnail, or description",

View file

@ -11,13 +11,15 @@ const Errors = keyMirror({
SERVER: null,
PERMISSION: null,
UNKNOWN_PROJECT: null,
RATE_LIMIT: null
RATE_LIMIT: null,
DUPLICATE: null
});
const normalizeError = (err, body, res) => {
if (err) return Errors.NETWORK;
if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION;
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 !== 200) return Errors.SERVER;
return null;
@ -60,10 +62,24 @@ const generateProjectListItem = (postBody, infoBody) => ({
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 studioId = selectStudioId(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({
uri: `/studios/${studioId}/project/${projectId}`,
method: 'POST',

View file

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

View file

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

View file

@ -22,11 +22,14 @@
.user-projects-modal-content {
padding: 0 30px 30px;
background: #E9F1FC;
max-height: 80vh;
max-height: calc(100vh - 200px);
overflow-y: auto;
overscroll-behavior: contain;
border-bottom-left-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 (
<div className="studio-activity">
<h2>Activity</h2>
<div className="studio-header-container">
<h2><FormattedMessage id="studio.activityHeader" /></h2>
</div>
{loading && <div>Loading...</div>}
{error && <Debug
label="Error"

View file

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

View file

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

View file

@ -3,12 +3,24 @@ import React, {useState} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
import {inviteCurator} from './lib/studio-member-actions';
import FlexRow from '../../components/flex-row/flex-row.jsx';
import {Errors, inviteCurator} from './lib/studio-member-actions';
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 [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
@ -23,11 +35,19 @@ const StudioCuratorInviter = ({onSubmit}) => {
return (
<div className="studio-adder-section">
<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
className={classNames({'mod-form-error': error})}
disabled={submitting}
type="text"
placeholder="<username>"
placeholder={intl.formatMessage({id: 'studio.inviteCuratorPlaceholder'})}
value={value}
onKeyDown={e => e.key === 'Enter' && submit()}
onChange={e => setValue(e.target.value)}
@ -36,17 +56,17 @@ const StudioCuratorInviter = ({onSubmit}) => {
className={classNames('button', {
'mod-mutating': submitting
})}
disabled={submitting}
disabled={submitting || value === ''}
onClick={submit}
><FormattedMessage id="studio.inviteCurator" /></button>
{error && <div>{error}</div>}
</FlexRow>
</div>
</div>
);
};
StudioCuratorInviter.propTypes = {
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
intl: intlShape
};
const mapStateToProps = () => ({});
@ -55,4 +75,4 @@ const mapDispatchToProps = ({
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">
<h2><FormattedMessage id="studio.curatorsHeader" /></h2>
<div className="studio-header-container">
<h2><FormattedMessage id="studio.curatorsHeader" /></h2>
</div>
{canInviteCurators && <CuratorInviter />}
{showCuratorInvite && <CuratorInvite />}
{error && <Debug

View file

@ -2,23 +2,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo} from '../../redux/studio-permissions';
import {
mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
Errors, mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
} 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 = ({
descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate
}) => {
const fieldClassName = classNames('studio-description', {
'mod-fetching': isFetching,
'mod-mutating': isMutating
'mod-mutating': isMutating,
'mod-form-error': !!descriptionError
});
return (
<React.Fragment>
<div className="studio-info-section">
<textarea
rows="20"
className={fieldClassName}
@ -27,8 +40,11 @@ const StudioDescription = ({
onBlur={e => e.target.value !== description &&
handleUpdate(e.target.value)}
/>
{descriptionError && <div>Error mutating description: {descriptionError}</div>}
</React.Fragment>
{descriptionError && <ValidationMessage
mode="error"
message={<FormattedMessage id={errorToMessageId(descriptionError)} />}
/>}
</div>
);
};

View file

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

View file

@ -2,19 +2,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {selectStudioImage, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo} from '../../redux/studio-permissions';
import {
mutateStudioImage, selectIsMutatingImage, selectImageMutationError
Errors, mutateStudioImage, selectIsMutatingImage, selectImageMutationError
} 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 StudioImage = ({
imageError, isFetching, isMutating, image, canEditInfo, handleUpdate
}) => {
const fieldClassName = classNames('studio-image', {
const fieldClassName = classNames('studio-info-section', {
'mod-fetching': isFetching,
'mod-mutating': isMutating
});
@ -36,7 +47,10 @@ const StudioImage = ({
e.target.value = '';
}}
/>
{imageError && <div>Error mutating image: {imageError}</div>}
{imageError && <ValidationMessage
mode="error"
message={<FormattedMessage id={errorToMessageId(imageError)} />}
/>}
</React.Fragment>
}
</div>

View file

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

View file

@ -14,6 +14,10 @@ import {
removeManager
} 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 = ({
canRemove, canPromote, onRemove, onPromote, isCreator, // mapState props
username, image // own props
@ -36,37 +40,47 @@ const StudioMemberTile = ({
>{username}</a>
{isCreator && <div className="studio-member-role"><FormattedMessage id="studio.creatorRole" /></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>
{(canRemove || canPromote) &&
<OverflowMenu>
{canPromote && <li>
<button
className={classNames({
'mod-mutating': submitting
})}
disabled={submitting}
onClick={() => {
setSubmitting(true);
setError(null);
onPromote(username).catch(e => {
setError(e);
setSubmitting(false);
});
}}
>
<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>}
</div>

View file

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

View file

@ -3,13 +3,13 @@ import React, {useState} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
import {addProject} from './lib/studio-project-actions';
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 [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
@ -25,11 +25,19 @@ const StudioProjectAdder = ({onSubmit}) => {
return (
<div className="studio-adder-section">
<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
className={classNames({'mod-form-error': error})}
disabled={submitting}
type="text"
placeholder="<project id>"
placeholder={intl.formatMessage({id: 'studio.addProjectPlaceholder'})}
value={value}
onKeyDown={e => e.key === 'Enter' && submit()}
onChange={e => setValue(e.target.value)}
@ -38,10 +46,9 @@ const StudioProjectAdder = ({onSubmit}) => {
className={classNames('button', {
'mod-mutating': submitting
})}
disabled={submitting}
disabled={submitting || value === ''}
onClick={submit}
><FormattedMessage id="studio.addProject" /></button>
{error && <div>{error}</div>}
<div className="studio-adder-vertical-divider" />
<button
className="button"
@ -50,13 +57,14 @@ const StudioProjectAdder = ({onSubmit}) => {
<FormattedMessage id="studio.browseProjects" />
</button>
{modalOpen && <UserProjectsModal onRequestClose={() => setModalOpen(false)} />}
</FlexRow>
</div>
</div>
);
};
StudioProjectAdder.propTypes = {
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
intl: intlShape
};
const mapStateToProps = () => ({});
@ -65,4 +73,4 @@ const mapDispatchToProps = ({
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 {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {selectCanRemoveProject} from '../../redux/studio-permissions';
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 = ({
canRemove, onRemove, // mapState props
id, title, image, avatar, username // own props
@ -41,23 +45,29 @@ const StudioProjectTile = ({
>{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>
<OverflowMenu>
<li>
<button
className={classNames({
'mod-mutating': submitting
})}
disabled={submitting}
onClick={() => {
setSubmitting(true);
setError(null);
onRemove(id)
.catch(e => {
setError(e);
setSubmitting(false);
});
}}
>
<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>
);

View file

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

View file

@ -2,21 +2,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo} from '../../redux/studio-permissions';
import {mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
import classNames from 'classnames';
import {Errors, mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
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 = ({
titleError, isFetching, isMutating, title, canEditInfo, handleUpdate
}) => {
const fieldClassName = classNames('studio-title', {
'mod-fetching': isFetching,
'mod-mutating': isMutating
'mod-mutating': isMutating,
'mod-form-error': !!titleError
});
return (
<React.Fragment>
<div className="studio-info-section">
<textarea
className={fieldClassName}
disabled={isMutating || !canEditInfo || isFetching}
@ -24,8 +41,11 @@ const StudioTitle = ({
onBlur={e => e.target.value !== title &&
handleUpdate(e.target.value)}
/>
{titleError && <div>Error mutating title: {titleError}</div>}
</React.Fragment>
{titleError && <ValidationMessage
mode="error"
message={<FormattedMessage id={errorToMessageId(titleError)} />}
/>}
</div>
);
};

View file

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