diff --git a/bin/configure-fastly.js b/bin/configure-fastly.js
index c3eb4cca2..d98c01b68 100644
--- a/bin/configure-fastly.js
+++ b/bin/configure-fastly.js
@@ -270,12 +270,11 @@ async.auto({
fastly.activateVersion(results.version, function (e, resp) {
if (e) throw new Error(e);
process.stdout.write('Successfully configured and activated version ' + resp.number + '\n');
- if (process.env.FASTLY_PURGE_ALL) {
- fastly.purgeAll(FASTLY_SERVICE_ID, function (error) {
- if (error) throw new Error(error);
- process.stdout.write('Purged all.\n');
- });
- }
+ // purge static-assets using surrogate key
+ fastly.purgeKey(FASTLY_SERVICE_ID, 'static-assets', function (error) {
+ if (error) throw new Error(error);
+ process.stdout.write('Purged static assets.\n');
+ });
});
}
});
diff --git a/package-lock.json b/package-lock.json
index a59122658..4c57007c8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,9 +16,9 @@
}
},
"@babel/cli": {
- "version": "7.13.16",
- "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.13.16.tgz",
- "integrity": "sha512-cL9tllhqvsQ6r1+d9Invf7nNXg/3BlfL1vvvL/AdH9fZ2l5j0CeBcoq6UjsqHpvyN1v5nXSZgqJZoGeK+ZOAbw==",
+ "version": "7.14.3",
+ "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.14.3.tgz",
+ "integrity": "sha512-zU4JLvwk32ay1lhhyGfqiRUSPoltVDjhYkA3aQq8+Yby9z30s/EsFw1EPOHxWG9YZo2pAGfgdRNeHZQAYU5m9A==",
"dev": true,
"requires": {
"@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents",
@@ -219,23 +219,23 @@
}
},
"@babel/compat-data": {
- "version": "7.14.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz",
- "integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.4.tgz",
+ "integrity": "sha512-i2wXrWQNkH6JplJQGn3Rd2I4Pij8GdHkXwHMxm+zV5YG/Jci+bCNrWZEWC4o+umiDkRrRs4dVzH3X4GP7vyjQQ==",
"dev": true
},
"@babel/core": {
- "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==",
+ "version": "7.14.3",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz",
+ "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
- "@babel/generator": "^7.14.2",
+ "@babel/generator": "^7.14.3",
"@babel/helper-compilation-targets": "^7.13.16",
"@babel/helper-module-transforms": "^7.14.2",
"@babel/helpers": "^7.14.0",
- "@babel/parser": "^7.14.2",
+ "@babel/parser": "^7.14.3",
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.14.2",
"@babel/types": "^7.14.2",
@@ -257,9 +257,9 @@
}
},
"@babel/generator": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
- "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
+ "version": "7.14.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz",
+ "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==",
"dev": true,
"requires": {
"@babel/types": "^7.14.2",
@@ -308,9 +308,9 @@
}
},
"@babel/parser": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
- "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz",
+ "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==",
"dev": true
},
"@babel/template": {
@@ -341,9 +341,9 @@
}
},
"@babel/types": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
- "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+ "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
@@ -478,14 +478,14 @@
}
},
"@babel/helper-compilation-targets": {
- "version": "7.13.16",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz",
- "integrity": "sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.4.tgz",
+ "integrity": "sha512-JgdzOYZ/qGaKTVkn5qEDV/SXAh8KcyUVkCoSWGN8T3bwrgd6m+/dJa2kVGi6RJYJgEYPBdZ84BZp9dUjNWkBaA==",
"dev": true,
"requires": {
- "@babel/compat-data": "^7.13.15",
+ "@babel/compat-data": "^7.14.4",
"@babel/helper-validator-option": "^7.12.17",
- "browserslist": "^4.14.5",
+ "browserslist": "^4.16.6",
"semver": "^6.3.0"
},
"dependencies": {
@@ -503,9 +503,9 @@
}
},
"electron-to-chromium": {
- "version": "1.3.727",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz",
- "integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==",
+ "version": "1.3.749",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.749.tgz",
+ "integrity": "sha512-F+v2zxZgw/fMwPz/VUGIggG4ZndDsYy0vlpthi3tjmDZlcfbhN5mYW0evXUsBr2sUtuDANFtle410A9u/sd/4A==",
"dev": true
},
"semver": {
@@ -546,9 +546,9 @@
},
"dependencies": {
"@babel/types": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
- "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+ "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
@@ -573,9 +573,9 @@
},
"dependencies": {
"@babel/types": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
- "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+ "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
@@ -616,9 +616,9 @@
}
},
"@babel/generator": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
- "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
+ "version": "7.14.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz",
+ "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==",
"dev": true,
"requires": {
"@babel/types": "^7.14.2",
@@ -667,9 +667,9 @@
}
},
"@babel/parser": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
- "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz",
+ "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==",
"dev": true
},
"@babel/template": {
@@ -700,9 +700,9 @@
}
},
"@babel/types": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
- "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+ "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
@@ -789,9 +789,9 @@
},
"dependencies": {
"@babel/types": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
- "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+ "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
@@ -813,15 +813,15 @@
"dev": true
},
"@babel/helper-replace-supers": {
- "version": "7.13.12",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz",
- "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.4.tgz",
+ "integrity": "sha512-zZ7uHCWlxfEAAOVDYQpEf/uyi1dmeC7fX4nCf2iz9drnCwi1zvwXL3HwWWNXUQEJ1k23yVn3VbddiI9iJEXaTQ==",
"dev": true,
"requires": {
"@babel/helper-member-expression-to-functions": "^7.13.12",
"@babel/helper-optimise-call-expression": "^7.12.13",
- "@babel/traverse": "^7.13.0",
- "@babel/types": "^7.13.12"
+ "@babel/traverse": "^7.14.2",
+ "@babel/types": "^7.14.4"
},
"dependencies": {
"@babel/code-frame": {
@@ -834,9 +834,9 @@
}
},
"@babel/generator": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
- "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
+ "version": "7.14.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz",
+ "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==",
"dev": true,
"requires": {
"@babel/types": "^7.14.2",
@@ -885,9 +885,9 @@
}
},
"@babel/parser": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
- "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz",
+ "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==",
"dev": true
},
"@babel/template": {
@@ -918,9 +918,9 @@
}
},
"@babel/types": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
- "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+ "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
@@ -1007,9 +1007,9 @@
},
"dependencies": {
"@babel/types": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
- "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+ "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
@@ -1066,9 +1066,9 @@
}
},
"@babel/generator": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.2.tgz",
- "integrity": "sha512-OnADYbKrffDVai5qcpkMxQ7caomHOoEwjkouqnN2QhydAjowFAZcsdecFIRUBdb+ZcruwYE4ythYmF1UBZU5xQ==",
+ "version": "7.14.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz",
+ "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==",
"dev": true,
"requires": {
"@babel/types": "^7.14.2",
@@ -1117,9 +1117,9 @@
}
},
"@babel/parser": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.2.tgz",
- "integrity": "sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz",
+ "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==",
"dev": true
},
"@babel/template": {
@@ -1150,9 +1150,9 @@
}
},
"@babel/types": {
- "version": "7.14.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz",
- "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==",
+ "version": "7.14.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+ "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
@@ -1368,10 +1368,27 @@
}
}
},
+ "@formatjs/ecma402-abstract": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.9.3.tgz",
+ "integrity": "sha512-DBrRUL65m4SVtfq+T4Qltd8+upAzfb9K1MX0UZ0hqQ0wpBY0PSIti9XJe0ZQ/j2v/KxpwQ0Jw5NLumKVezJFQg==",
+ "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.6.0",
- "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.6.0.tgz",
- "integrity": "sha512-1967VujZeow0K0NpzerdNOaskcE0KwnnchxT8TzlkLs4RjXx7Uz0bjQPtUYGV7kvbgMJ9qb6tWmCqIwe3sBKUw==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.7.0.tgz",
+ "integrity": "sha512-3pQjp+WCHrFfkUeSuw//LIx/orcykopZX4U/2kY1jTYhATVJbKdU8Rl5V2/d+fw1naKjKYoIjIXGn85Ti396+A==",
"dev": true,
"requires": {
"cldr-core": "38",
@@ -1387,26 +1404,17 @@
}
},
"@formatjs/intl-locale": {
- "version": "2.4.26",
- "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.26.tgz",
- "integrity": "sha512-f5NyHb5qdfA0oO2IGLhNU0k9BWq8xI26VqxzZqjTfEKnn8xJ+SBE9drwRfTqlRz6pmvztPmkDg3FSflmGdDKGw==",
+ "version": "2.4.32",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.32.tgz",
+ "integrity": "sha512-jA6f4ASAO3P4mBl+1nxc+lhx6x74wP+ahUOKPK9tPYZmCn7JPTAlit0iWae79vH3wE9TblOTUZ9RwYlOPWzchw==",
"dev": true,
"requires": {
- "@formatjs/ecma402-abstract": "1.8.0",
- "@formatjs/intl-getcanonicallocales": "1.6.0",
+ "@formatjs/ecma402-abstract": "1.9.3",
+ "@formatjs/intl-getcanonicallocales": "1.7.0",
"cldr-core": "38",
"tslib": "^2.1.0"
},
"dependencies": {
- "@formatjs/ecma402-abstract": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.8.0.tgz",
- "integrity": "sha512-X+nxZcIQr0YfYNtw1ZkHjN3YSyi0fEmdAJqRzk24KwNvqLv7GmVfw70mf7ADnwOvkcrSaAdx24GfAqckGTv9ww==",
- "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.20",
- "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.20.tgz",
- "integrity": "sha512-ayyjvIh9ShXQxo0Y9GhUTyW9zyB3jiBFRIEhMmGowpIlTVRhOwl57XZ+YuUgo/yCvtJqnvGPu/4/fQTouxHuGw==",
+ "version": "4.0.27",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.27.tgz",
+ "integrity": "sha512-Q4RAXZXuhWwMWK3Vsbf9AIRBa9B+BTiWjNkzlCq77pCRQYo555owWrGxnZUVk2203wygOIkoRYSvP7Tu2RXNFA==",
"dev": true,
"requires": {
- "@formatjs/ecma402-abstract": "1.8.0",
+ "@formatjs/ecma402-abstract": "1.9.3",
"tslib": "^2.1.0"
},
"dependencies": {
- "@formatjs/ecma402-abstract": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.8.0.tgz",
- "integrity": "sha512-X+nxZcIQr0YfYNtw1ZkHjN3YSyi0fEmdAJqRzk24KwNvqLv7GmVfw70mf7ADnwOvkcrSaAdx24GfAqckGTv9ww==",
- "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.20210518033204",
- "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210518033204.tgz",
- "integrity": "sha512-o3RUtZuepebeWvk0qGnemE1Efl3LTA/uULuyXDdFDCtigY8UJd4G981KW7NrBUzn6hcIC3tmtOw3JyR4MStBHA==",
+ "version": "0.1.0-prerelease.20210606091012",
+ "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210606091012.tgz",
+ "integrity": "sha512-ZAK5zI4rTJSOsyP6ZImHJkH5dtSwHZCRpH5UL3dx0PGyO2lHTljhKZ6WUzONyGi4lAE2KOmxWKFNkzkXketbBg==",
"dev": true,
"requires": {
"exports-loader": "0.6.3",
@@ -20866,9 +20865,9 @@
}
},
"scratch-gui": {
- "version": "0.1.0-prerelease.20210519034531",
- "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210519034531.tgz",
- "integrity": "sha512-1QaK9K4jm2aztmvUcC1Wjwegy/eb8d6Tc6Zv/E8ppcmCvei/KchYuKIXbP+R5ezciRZvS7scqjLEuDoqRay1Xw==",
+ "version": "0.1.0-prerelease.20210606094924",
+ "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210606094924.tgz",
+ "integrity": "sha512-2Kll76gdxQT0QJoGhXabKf2byfA/rlrTFmyXzHvADLApIi/jo21/xclTD5MIu+EPbTwv5PL95N32osUvuedkcw==",
"dev": true,
"requires": {
"arraybuffer-loader": "^1.0.6",
@@ -20919,14 +20918,14 @@
"redux": "3.7.2",
"redux-throttle": "0.1.1",
"scratch-audio": "0.1.0-prerelease.20200528195344",
- "scratch-blocks": "0.1.0-prerelease.20210518033204",
- "scratch-l10n": "3.11.20210519031633",
+ "scratch-blocks": "0.1.0-prerelease.20210606091012",
+ "scratch-l10n": "3.11.20210606031618",
"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-storage": "1.3.5",
"scratch-svg-renderer": "0.2.0-prerelease.20210511195415",
- "scratch-vm": "0.2.0-prerelease.20210510162256",
+ "scratch-vm": "0.2.0-prerelease.20210601191643",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
"text-encoding": "0.7.0",
@@ -21089,9 +21088,9 @@
"dev": true
},
"electron-to-chromium": {
- "version": "1.3.732",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.732.tgz",
- "integrity": "sha512-qKD5Pbq+QMk4nea4lMuncUMhpEiQwaJyCW7MrvissnRcBDENhVfDmAqQYRQ3X525oTzhar9Zh1cK0L2d1UKYcw==",
+ "version": "1.3.749",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.749.tgz",
+ "integrity": "sha512-F+v2zxZgw/fMwPz/VUGIggG4ZndDsYy0vlpthi3tjmDZlcfbhN5mYW0evXUsBr2sUtuDANFtle410A9u/sd/4A==",
"dev": true
},
"has-flag": {
@@ -21312,28 +21311,17 @@
"dev": true
},
"scratch-storage": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.4.tgz",
- "integrity": "sha512-BuMwX0337JKdHI90k9ULu5AapxMiVhwUTD9GEtAIUjyLUdiWO9MHCC0pzfrEXwosMV3BxtFzTe21UiSkWK9Pcw==",
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.5.tgz",
+ "integrity": "sha512-MrIRidvUJtncx0xlMJDng9KypHR3/kyCE2stXJ1CYNLTWrl2pGCpNNcYWygRQw7aFQ0hIzP/Z118T8X53sUDAA==",
"dev": true,
"requires": {
- "arraybuffer-loader": "^1.0.8",
+ "arraybuffer-loader": "^1.0.3",
"base64-js": "1.3.0",
"fastestsmallesttextencoderdecoder": "^1.0.7",
"js-md5": "0.7.3",
"minilog": "3.1.0",
"worker-loader": "^2.0.0"
- },
- "dependencies": {
- "arraybuffer-loader": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/arraybuffer-loader/-/arraybuffer-loader-1.0.8.tgz",
- "integrity": "sha512-CwUVCcxCgcgZUu2w741OV6Xj1tvRVQebq22RCyGXiLgJOJ4e4M/59EPYdtK2MLfIN28t1TDvuh2ojstNq3Kh5g==",
- "dev": true,
- "requires": {
- "loader-utils": "^1.1.0"
- }
- }
}
},
"source-list-map": {
@@ -21388,9 +21376,9 @@
}
},
"scratch-l10n": {
- "version": "3.11.20210519031633",
- "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210519031633.tgz",
- "integrity": "sha512-MpwoltjrM0Yzi1j2I+s2QEneDLwnM8tt/FtD67dKNnRgkAS+gYuY+xcHf6w/DBhuN1GziDNHHB/LNvfMag4c8A==",
+ "version": "3.11.20210606031618",
+ "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210606031618.tgz",
+ "integrity": "sha512-AKKaQERm74zlgfeY8YJ49nlBe0S+sa2ulB5ZAOfi+reD0J52tvNOrubQTtOPIRbp7PPfHYlnD84RCnVb/P73IA==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
@@ -21478,15 +21466,6 @@
"twgl.js": "4.4.0"
},
"dependencies": {
- "arraybuffer-loader": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/arraybuffer-loader/-/arraybuffer-loader-1.0.8.tgz",
- "integrity": "sha512-CwUVCcxCgcgZUu2w741OV6Xj1tvRVQebq22RCyGXiLgJOJ4e4M/59EPYdtK2MLfIN28t1TDvuh2ojstNq3Kh5g==",
- "dev": true,
- "requires": {
- "loader-utils": "^1.1.0"
- }
- },
"base64-js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
@@ -21524,12 +21503,12 @@
}
},
"scratch-storage": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.4.tgz",
- "integrity": "sha512-BuMwX0337JKdHI90k9ULu5AapxMiVhwUTD9GEtAIUjyLUdiWO9MHCC0pzfrEXwosMV3BxtFzTe21UiSkWK9Pcw==",
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.5.tgz",
+ "integrity": "sha512-MrIRidvUJtncx0xlMJDng9KypHR3/kyCE2stXJ1CYNLTWrl2pGCpNNcYWygRQw7aFQ0hIzP/Z118T8X53sUDAA==",
"dev": true,
"requires": {
- "arraybuffer-loader": "^1.0.8",
+ "arraybuffer-loader": "^1.0.3",
"base64-js": "1.3.0",
"fastestsmallesttextencoderdecoder": "^1.0.7",
"js-md5": "0.7.3",
@@ -21666,9 +21645,9 @@
"dev": true
},
"scratch-vm": {
- "version": "0.2.0-prerelease.20210510162256",
- "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20210510162256.tgz",
- "integrity": "sha512-14KHdLRoEcpHRsLPkiBNnMDNAtoGYr0ZkAUdHatwvqTfzS1eN22qytqdkJ8Mx3LIxP52K/IMOVq9rFVUZ4df3w==",
+ "version": "0.2.0-prerelease.20210601191643",
+ "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20210601191643.tgz",
+ "integrity": "sha512-SWXa176Ymo2EER+dEF5yJXGOaq7xekHcmggEJ2p+8vt3LZUlBpmUlL/U1FTY65wjaYLxQWMi7q+d+IpnO/vkEg==",
"dev": true,
"requires": {
"@vernier/godirect": "1.5.0",
diff --git a/package.json b/package.json
index 510a2cd5d..09274e8e3 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"deploy": "npm run deploy:s3 && npm run deploy:fastly",
"deploy:fastly": "node ./bin/configure-fastly.js",
"deploy:s3": "npm run deploy:s3:all && npm run deploy:s3:svg && npm run deploy:s3:js",
- "deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600",
+ "deploy:s3cmd": "s3cmd sync -P --delete-removed --add-header=Cache-Control:no-cache,public,max-age=3600 --add-header=x-amz-meta-surrogate-key:static-assets",
"deploy:s3:all": "npm run deploy:s3cmd -- --exclude '.DS_Store' --exclude '*.svg' --exclude '*.js' ./build/ s3://$S3_BUCKET_NAME/",
"deploy:s3:svg": "npm run deploy:s3cmd -- --exclude '*' --include '*.svg' --mime-type 'image/svg+xml' ./build/ s3://$S3_BUCKET_NAME/",
"deploy:s3:js": "npm run deploy:s3cmd -- --exclude '*' --include '*.js' --mime-type 'application/javascript' ./build/ s3://$S3_BUCKET_NAME/",
@@ -126,8 +126,8 @@
"redux-mock-store": "^1.2.3",
"redux-thunk": "2.0.1",
"sass-loader": "6.0.6",
- "scratch-gui": "0.1.0-prerelease.20210519034531",
- "scratch-l10n": "latest",
+ "scratch-gui": "0.1.0-prerelease.20210606094924",
+ "scratch-l10n": "3.11.20210606031618",
"selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0",
"style-loader": "0.12.3",
diff --git a/src/components/alert/alert-component.jsx b/src/components/alert/alert-component.jsx
new file mode 100644
index 000000000..9dec54fd7
--- /dev/null
+++ b/src/components/alert/alert-component.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import {FormattedMessage} from 'react-intl';
+import Button from '../../components/forms/button.jsx';
+
+import './alert.scss';
+
+const AlertComponent = ({className, icon, id, values, onClear}) => (
+
+
+ {icon &&
![]({icon})
}
+
+
+
+ {onClear &&
}
+
+
+);
+
+AlertComponent.propTypes = {
+ className: PropTypes.string,
+ icon: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ values: PropTypes.shape({}),
+ onClear: PropTypes.func
+};
+
+export default AlertComponent;
diff --git a/src/components/alert/alert-context.js b/src/components/alert/alert-context.js
new file mode 100644
index 000000000..171f640e3
--- /dev/null
+++ b/src/components/alert/alert-context.js
@@ -0,0 +1,18 @@
+import {createContext, useContext} from 'react';
+import AlertStatus from './alert-status.js';
+
+const AlertContext = createContext({
+ // Note: defaults here are only used if there is no Provider in the tree
+ status: AlertStatus.NONE,
+ data: {},
+ clearAlert: () => {},
+ successAlert: () => {},
+ errorAlert: () => {}
+});
+
+const useAlertContext = () => useContext(AlertContext);
+
+export {
+ AlertContext as default,
+ useAlertContext
+};
diff --git a/src/components/alert/alert-provider.jsx b/src/components/alert/alert-provider.jsx
new file mode 100644
index 000000000..2a75acd80
--- /dev/null
+++ b/src/components/alert/alert-provider.jsx
@@ -0,0 +1,54 @@
+import React, {useRef, useState} from 'react';
+import PropTypes from 'prop-types';
+
+import AlertStatus from './alert-status.js';
+import AlertContext from './alert-context.js';
+
+const AlertProvider = ({children}) => {
+ const defaultState = {
+ status: AlertStatus.NONE,
+ data: {},
+ showClear: false
+ };
+ const [state, setState] = useState(defaultState);
+ const timeoutRef = useRef(null);
+
+ const clearAlert = () => {
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ setState(defaultState);
+ };
+
+ const handleAlert = (status, data, timeoutSeconds = 3) => {
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
+ setState({status, data, showClear: !timeoutSeconds});
+ if (timeoutSeconds) {
+ timeoutRef.current = setTimeout(() => {
+ timeoutRef.current = null;
+ setState(defaultState);
+ }, timeoutSeconds * 1000);
+ }
+ };
+
+ return (
+
+ handleAlert(AlertStatus.SUCCESS, newData, timeoutSeconds),
+ errorAlert: (newData, timeoutSeconds = 3) =>
+ handleAlert(AlertStatus.ERROR, newData, timeoutSeconds)
+ }}
+ >
+ {children}
+
+ );
+};
+AlertProvider.propTypes = {
+ children: PropTypes.node
+};
+
+export default AlertProvider;
diff --git a/src/components/alert/alert-status.js b/src/components/alert/alert-status.js
new file mode 100644
index 000000000..a964d9fa1
--- /dev/null
+++ b/src/components/alert/alert-status.js
@@ -0,0 +1,5 @@
+export default {
+ NONE: 'NONE',
+ SUCCESS: 'SUCCESS',
+ ERROR: 'ERROR'
+};
diff --git a/src/components/alert/alert.jsx b/src/components/alert/alert.jsx
new file mode 100644
index 000000000..6f686e46b
--- /dev/null
+++ b/src/components/alert/alert.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import AlertComponent from './alert-component.jsx';
+import AlertStatus from './alert-status.js';
+import {useAlertContext} from './alert-context.js';
+
+import successIcon from './icon-alert-success.svg';
+import errorIcon from './icon-alert-error.svg';
+
+const Alert = ({className}) => {
+ const {status, data, showClear, clearAlert} = useAlertContext();
+ if (status === AlertStatus.NONE) return null;
+ return (
+
+ );
+};
+
+Alert.propTypes = {
+ className: PropTypes.string
+};
+
+export default Alert;
diff --git a/src/components/alert/alert.scss b/src/components/alert/alert.scss
new file mode 100644
index 000000000..043d17795
--- /dev/null
+++ b/src/components/alert/alert.scss
@@ -0,0 +1,40 @@
+
+.alert-wrapper {
+ position: absolute;
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ z-index: 100;
+ pointer-events: none;
+
+ .alert {
+ display: flex;
+ box-sizing: border-box;
+ padding: 10px 20px;
+ border-radius: 8px;
+ align-items: center;
+ margin-top: 1rem;
+ min-height: 60px;
+ pointer-events: auto;
+ &.alert-error {
+ background: #FFF0DF;
+ border: 1px solid #FF8C1A;
+ box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25)
+ }
+
+ &.alert-success {
+ background: #CEF2E8;
+ border: 1px solid #0EBD8C;
+ box-shadow: 0px 0px 0px rgba(14, 189, 140, 0.25);
+ }
+ .alert-msg {
+ font-size: 14px;
+ font-weight: bold;
+ }
+ .alert-close-button {
+ position: unset;
+ margin-left: 1rem;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/components/alert/icon-alert-error.svg b/src/components/alert/icon-alert-error.svg
new file mode 100644
index 000000000..6dc116bdf
--- /dev/null
+++ b/src/components/alert/icon-alert-error.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/alert/icon-alert-success.svg b/src/components/alert/icon-alert-success.svg
new file mode 100644
index 000000000..fbe57da4b
--- /dev/null
+++ b/src/components/alert/icon-alert-success.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/components/commenting-status/commenting-status.scss b/src/components/commenting-status/commenting-status.scss
index 91d51ad46..1c1d14a81 100644
--- a/src/components/commenting-status/commenting-status.scss
+++ b/src/components/commenting-status/commenting-status.scss
@@ -7,6 +7,7 @@
margin: .5rem 0 2.25rem;
background-color: $ui-blue-10percent;
text-align: center;
+ box-sizing: border-box;
p {
margin-bottom: 0;
diff --git a/src/components/navigation/www/navigation.jsx b/src/components/navigation/www/navigation.jsx
index 39e69c242..14a6d412b 100644
--- a/src/components/navigation/www/navigation.jsx
+++ b/src/components/navigation/www/navigation.jsx
@@ -135,6 +135,7 @@ class Navigation extends React.Component {
/>
get(state, ['session', 'session', 'perm
module.exports.selectIsEducator = state => get(state, ['session', 'session', 'permissions', 'educator'], false);
module.exports.selectProjectCommentsGloballyEnabled = state =>
get(state, ['session', 'session', 'flags', 'project_comments_enabled'], false);
+module.exports.selectMuteStatus = state => get(state, ['session', 'session', 'permissions', 'mute_status'],
+ {muteExpiresAt: 0, offenses: [], showWarning: false});
+module.exports.selectIsMuted = state => (module.exports.selectMuteStatus(state).muteExpiresAt || 0) * 1000 > Date.now();
+
+module.exports.selectHasFetchedSession = state => state.session.status === module.exports.Status.FETCHED;
// 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);
diff --git a/src/redux/studio-permissions.js b/src/redux/studio-permissions.js
index 25c445fe0..0afdb2735 100644
--- a/src/redux/studio-permissions.js
+++ b/src/redux/studio-permissions.js
@@ -1,4 +1,5 @@
-const {selectUserId, selectIsAdmin, selectIsSocial, selectIsLoggedIn, selectUsername} = require('./session');
+const {selectUserId, selectIsAdmin, selectIsSocial,
+ selectIsLoggedIn, selectUsername, selectIsMuted} = require('./session');
// Fine-grain selector helpers - not exported, use the higher level selectors below
const isCreator = state => selectUserId(state) === state.studio.owner;
@@ -6,11 +7,12 @@ const isCurator = state => state.studio.curator;
const isManager = state => state.studio.manager || isCreator(state);
// Action-based permissions selectors
-const selectCanEditInfo = state => selectIsAdmin(state) || isManager(state);
+const selectCanEditInfo = state => !selectIsMuted(state) && (selectIsAdmin(state) || isManager(state));
const selectCanAddProjects = state =>
- isManager(state) ||
+ !selectIsMuted(state) &&
+ (isManager(state) ||
isCurator(state) ||
- (selectIsSocial(state) && state.studio.openToAll);
+ (selectIsSocial(state) && state.studio.openToAll));
// This isn't "canComment" since they could be muted, but comment composer handles that
const selectShowCommentComposer = state => selectIsSocial(state);
@@ -26,12 +28,13 @@ const selectCanDeleteCommentWithoutConfirm = state => selectIsAdmin(state);
const selectCanFollowStudio = state => selectIsLoggedIn(state);
// Matching existing behavior, only admin/creator is allowed to toggle comments.
-const selectCanEditCommentsAllowed = state => selectIsAdmin(state) || isCreator(state);
-const selectCanEditOpenToAll = state => isManager(state);
+const selectCanEditCommentsAllowed = state => !selectIsMuted(state) && (selectIsAdmin(state) || isCreator(state));
+const selectCanEditOpenToAll = state => !selectIsMuted(state) && isManager(state);
-const selectShowCuratorInvite = state => !!state.studio.invited;
-const selectCanInviteCurators = state => isManager(state);
+const selectShowCuratorInvite = state => !selectIsMuted(state) && !!state.studio.invited;
+const selectCanInviteCurators = state => !selectIsMuted(state) && isManager(state);
const selectCanRemoveCurator = (state, username) => {
+ if (selectIsMuted(state)) return false;
// Admins/managers can remove any curators
if (isManager(state) || selectIsAdmin(state)) return true;
// Curators can remove themselves
@@ -41,10 +44,12 @@ const selectCanRemoveCurator = (state, username) => {
return false;
};
const selectCanRemoveManager = (state, managerId) =>
- (selectIsAdmin(state) || isManager(state)) && managerId !== state.studio.owner;
-const selectCanPromoteCurators = state => isManager(state);
+ !selectIsMuted(state) && (selectIsAdmin(state) || isManager(state)) && managerId !== state.studio.owner;
+const selectCanPromoteCurators = state => !selectIsMuted(state) && isManager(state);
const selectCanRemoveProject = (state, creatorUsername, actorId) => {
+ if (selectIsMuted(state)) return false;
+
// Admins/managers can remove any projects
if (isManager(state) || selectIsAdmin(state)) return true;
// Project owners can always remove their projects
@@ -58,6 +63,15 @@ const selectCanRemoveProject = (state, creatorUsername, actorId) => {
return false;
};
+// We should only show the mute errors to muted users who have any permissions related to the content
+const selectShowEditMuteError = state => selectIsMuted(state) && (isManager(state) || selectIsAdmin(state));
+const selectShowProjectMuteError = state => selectIsMuted(state) &&
+ (selectIsAdmin(state) ||
+ isManager(state) ||
+ isCurator(state) ||
+ (selectIsSocial(state) && state.studio.openToAll));
+const selectShowCuratorMuteError = state => selectIsMuted(state) && (isManager(state) || selectIsAdmin(state));
+
export {
selectCanEditInfo,
selectCanAddProjects,
@@ -74,5 +88,8 @@ export {
selectCanRemoveCurator,
selectCanRemoveManager,
selectCanPromoteCurators,
- selectCanRemoveProject
+ selectCanRemoveProject,
+ selectShowEditMuteError,
+ selectShowProjectMuteError,
+ selectShowCuratorMuteError
};
diff --git a/src/redux/studio.js b/src/redux/studio.js
index 2cbb80394..02f885205 100644
--- a/src/redux/studio.js
+++ b/src/redux/studio.js
@@ -94,7 +94,11 @@ const selectStudioDescription = state => state.studio.description;
const selectStudioImage = state => state.studio.image;
const selectStudioOpenToAll = state => state.studio.openToAll;
const selectStudioCommentsAllowed = state => state.studio.commentsAllowed;
+const selectStudioLastUpdated = state => state.studio.updated;
const selectStudioLoadFailed = state => state.studio.infoStatus === Status.ERROR;
+const selectStudioCommentCount = state => state.studio.commentCount;
+const selectStudioFollowerCount = state => state.studio.followers;
+const selectStudioProjectCount = state => state.studio.projectCount;
const selectIsFetchingInfo = state => state.studio.infoStatus === Status.FETCHING;
const selectIsFollowing = state => state.studio.following;
const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCHING;
@@ -115,7 +119,9 @@ const getInfo = () => ((dispatch, getState) => {
openToAll: body.open_to_all,
commentsAllowed: body.comments_allowed,
updated: new Date(body.history.modified),
+ commentCount: body.stats.comments,
followers: body.stats.followers,
+ projectCount: body.stats.projects,
owner: body.owner
}));
});
@@ -170,7 +176,11 @@ module.exports = {
selectStudioImage,
selectStudioOpenToAll,
selectStudioCommentsAllowed,
+ selectStudioLastUpdated,
selectStudioLoadFailed,
+ selectStudioCommentCount,
+ selectStudioFollowerCount,
+ selectStudioProjectCount,
selectIsFetchingInfo,
selectIsFetchingRoles,
selectIsFollowing,
diff --git a/src/routes.json b/src/routes.json
index 97976d6bb..48832a30b 100644
--- a/src/routes.json
+++ b/src/routes.json
@@ -301,7 +301,8 @@
"pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
"routeAlias": "/studios-playground/?$",
"view": "studio/studio",
- "title": "Studio Playground"
+ "title": "Studio Playground",
+ "dynamicMetaTags": true
},
{
"name": "teacher-faq",
diff --git a/src/views/components/components.jsx b/src/views/components/components.jsx
index 9fd180fc0..18f8dbb20 100644
--- a/src/views/components/components.jsx
+++ b/src/views/components/components.jsx
@@ -14,12 +14,43 @@ 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');
+const AlertProvider = require('../../components/alert/alert-provider.jsx').default;
+const {useAlertContext} = require('../../components/alert/alert-context.js');
+const Alert = require('../../components/alert/alert.jsx').default;
require('./components.scss');
+/* eslint-disable react/prop-types, react/jsx-no-bind */
+/* Demo of how to use the useAlertContext hook */
+const AlertButton = ({type, timeoutSeconds}) => {
+ const {errorAlert, successAlert} = useAlertContext();
+ const onClick = type === 'success' ?
+ () => successAlert({id: 'success-alert.string.id'}, timeoutSeconds) :
+ () => errorAlert({id: 'error-alert.string.id'}, timeoutSeconds);
+ return (
+
+ );
+};
+
const Components = () => (
+
Alert Provider, Display and Hooks
+
+
+
Overflow Menu
diff --git a/src/views/guidelines/guidelines.jsx b/src/views/guidelines/guidelines.jsx
index 0ff09e6b2..e18d9aada 100644
--- a/src/views/guidelines/guidelines.jsx
+++ b/src/views/guidelines/guidelines.jsx
@@ -16,27 +16,28 @@ const Guidelines = () => (
}
>
-
+
+
+
-
-
-
-
+
+
+
+ -
+
+
+
-
![sprites](//cdn.scratch.mit.edu/scratchr2/static/images/help/spritesforcommunityguid.png)
@@ -278,10 +287,12 @@ Comment.propTypes = {
canRestore: PropTypes.bool,
content: PropTypes.string,
datetimeCreated: PropTypes.string,
+ hasReachedThreadLimit: PropTypes.bool,
highlighted: PropTypes.bool,
id: PropTypes.number,
onAddComment: PropTypes.func,
onDelete: PropTypes.func,
+ onReply: PropTypes.func,
onReport: PropTypes.func,
onRestore: PropTypes.func,
parentId: PropTypes.number,
diff --git a/src/views/preview/comment/comment.scss b/src/views/preview/comment/comment.scss
index 324df51c1..abd9ee6e2 100644
--- a/src/views/preview/comment/comment.scss
+++ b/src/views/preview/comment/comment.scss
@@ -1,5 +1,10 @@
@import "../../../colors";
+.compose-row {
+ margin-top: 30px;
+ margin-bottom: 40px;
+}
+
.compose-comment {
margin-left: .5rem;
width: 100%;
@@ -279,6 +284,15 @@
}
}
+.thread-limit-status {
+ width: calc(100% - 4rem);
+ margin-left: auto;
+
+ .comment-status-icon {
+ display: none;
+ }
+}
+
.compose-disabled {
opacity: .5;
}
diff --git a/src/views/preview/comment/compose-comment.jsx b/src/views/preview/comment/compose-comment.jsx
index 4f2fc532e..374ec55ad 100644
--- a/src/views/preview/comment/compose-comment.jsx
+++ b/src/views/preview/comment/compose-comment.jsx
@@ -17,6 +17,7 @@ const formatTime = require('../../../lib/format-time');
const connect = require('react-redux').connect;
const api = require('../../../lib/api');
+const {selectMuteStatus} = require('../../../redux/session.js');
require('./comment.scss');
@@ -329,7 +330,7 @@ class ComposeComment extends React.Component {
className={classNames('flex-row',
'comment',
this.state.status === ComposeStatus.REJECTED_MUTE ?
- 'compose-disabled' : '')}
+ 'compose-disabled' : 'compose-row')}
>
@@ -444,9 +445,7 @@ ComposeComment.propTypes = {
};
const mapStateToProps = state => ({
- muteStatus: state.session.session.permissions.mute_status ?
- state.session.session.permissions.mute_status :
- {muteExpiresAt: 0, offenses: [], showWarning: false},
+ muteStatus: selectMuteStatus(state),
user: state.session.session.user
});
diff --git a/src/views/preview/comment/top-level-comment.jsx b/src/views/preview/comment/top-level-comment.jsx
index 1d2a1cdb4..eb6ec167d 100644
--- a/src/views/preview/comment/top-level-comment.jsx
+++ b/src/views/preview/comment/top-level-comment.jsx
@@ -6,9 +6,13 @@ const FormattedMessage = require('react-intl').FormattedMessage;
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
const Comment = require('./comment.jsx');
+const CommentingStatus = require('../../../components/commenting-status/commenting-status.jsx');
require('./comment.scss');
+// Thread limit only applies if hasThreadLimit prop is true
+const THREAD_LIMIT = 25;
+
class TopLevelComment extends React.Component {
constructor (props) {
super(props);
@@ -16,11 +20,14 @@ class TopLevelComment extends React.Component {
'handleExpandThread',
'handleAddComment',
'handleDeleteReply',
+ 'handleReplyStatus',
'handleReportReply',
'handleRestoreReply'
]);
this.state = {
- expanded: this.props.defaultExpanded
+ expanded: this.props.defaultExpanded,
+ threadLimitCommentId: '',
+ threadLimitParentId: ''
};
// A cache of {userId: username, ...} in order to show reply usernames
@@ -55,6 +62,12 @@ class TopLevelComment extends React.Component {
this.props.onAddComment(comment, this.props.id);
}
+ handleReplyStatus (id, parentId) {
+ // Send the parentId up to track which thread got "reply" clicked on
+ if (this.props.onReply) this.props.onReply(parentId);
+ this.setState({threadLimitCommentId: id, threadLimitParentId: parentId});
+ }
+
authorUsername (authorId) {
if (this.authorUsernameCache[authorId]) return this.authorUsernameCache[authorId];
@@ -80,6 +93,7 @@ class TopLevelComment extends React.Component {
canRestore,
content,
datetimeCreated,
+ hasThreadLimit,
highlightedCommentId,
id,
moreRepliesToLoad,
@@ -88,17 +102,40 @@ class TopLevelComment extends React.Component {
onRestore,
replies,
postURI,
+ threadHasReplyStatus,
+ totalReplyCount,
visibility
} = this.props;
const parentVisible = visibility === 'visible';
+ // Check whether this comment thread has reached the thread limit
+ const hasReachedThreadLimit = hasThreadLimit && totalReplyCount >= THREAD_LIMIT;
+
+ /*
+ Check all the following conditions:
+ - hasReachedThreadLimit: the thread has reached the limit
+ - threadHasReplyStatus: this thread should be showing the status
+ (false, if the user just clicked reply elsewhere and another thread/comment stole the status message)
+ - Use the comment id and parent id of this particular comment in this thread
+ to see if it has the reply status,
+ only one comment in a thread can have the status
+
+ All of these conditions together ensure that the user only sees one status message on the comments page.
+ */
+ const commentHasReplyStatus = (commentId, commentParentId) =>
+ hasReachedThreadLimit &&
+ threadHasReplyStatus &&
+ (this.state.threadLimitCommentId === commentId) &&
+ (this.state.threadLimitParentId === commentParentId);
+
return (
+ {commentHasReplyStatus(id, id) &&
+
+
+
+
+
+ }
{replies.length > 0 &&
{(this.state.expanded ? replies : replies.slice(0, 3)).map(reply => (
-
+
+
+ {commentHasReplyStatus(reply.id, id) &&
+
+
+
+
+
+ }
+
))}
{((!this.state.expanded && replies.length > 3) ||
(this.state.expanded && moreRepliesToLoad)) &&
@@ -179,24 +237,30 @@ TopLevelComment.propTypes = {
datetimeCreated: PropTypes.string,
defaultExpanded: PropTypes.bool,
deletable: PropTypes.bool,
+ hasThreadLimit: PropTypes.bool,
highlightedCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
id: PropTypes.number,
moreRepliesToLoad: PropTypes.bool,
onAddComment: PropTypes.func,
onDelete: PropTypes.func,
onLoadMoreReplies: PropTypes.func,
+ onReply: PropTypes.func,
onReport: PropTypes.func,
onRestore: PropTypes.func,
parentId: PropTypes.number,
postURI: PropTypes.string,
replies: PropTypes.arrayOf(PropTypes.object),
+ threadHasReplyStatus: PropTypes.bool,
+ totalReplyCount: PropTypes.number,
visibility: PropTypes.string
};
TopLevelComment.defaultProps = {
canDeleteWithoutConfirm: false,
defaultExpanded: false,
- moreRepliesToLoad: false
+ hasThreadLimit: false,
+ moreRepliesToLoad: false,
+ threadHasReplyStatus: false
};
module.exports = TopLevelComment;
diff --git a/src/views/studio/icons/activity-icon.svg b/src/views/studio/icons/activity-icon.svg
new file mode 100644
index 000000000..f7ec6145f
--- /dev/null
+++ b/src/views/studio/icons/activity-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/views/studio/icons/comments-icon.svg b/src/views/studio/icons/comments-icon.svg
new file mode 100644
index 000000000..8a19100c1
--- /dev/null
+++ b/src/views/studio/icons/comments-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/views/studio/icons/edit-icon.svg b/src/views/studio/icons/edit-icon.svg
new file mode 100644
index 000000000..376c4c01d
--- /dev/null
+++ b/src/views/studio/icons/edit-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/views/studio/icons/followers-icon.svg b/src/views/studio/icons/followers-icon.svg
new file mode 100644
index 000000000..84e31d837
--- /dev/null
+++ b/src/views/studio/icons/followers-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/views/studio/icons/last-updated-icon.svg b/src/views/studio/icons/last-updated-icon.svg
new file mode 100644
index 000000000..320d0de62
--- /dev/null
+++ b/src/views/studio/icons/last-updated-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/views/studio/icons/projects-icon.svg b/src/views/studio/icons/projects-icon.svg
new file mode 100644
index 000000000..6b1ddccfd
--- /dev/null
+++ b/src/views/studio/icons/projects-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/views/studio/icons/report-icon.svg b/src/views/studio/icons/report-icon.svg
new file mode 100644
index 000000000..cf68c1ae2
--- /dev/null
+++ b/src/views/studio/icons/report-icon.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/views/studio/l10n.json b/src/views/studio/l10n.json
index b2cc54c35..da065babd 100644
--- a/src/views/studio/l10n.json
+++ b/src/views/studio/l10n.json
@@ -1,7 +1,9 @@
{
"studio.tabNavProjects": "Projects",
+ "studio.tabNavProjectsWithCount": "Projects {projectCount}",
"studio.tabNavCurators": "Curators",
"studio.tabNavComments": "Comments",
+ "studio.tabNavCommentsWithCount": "Comments {commentCount}",
"studio.tabNavActivity": "Activity",
"studio.title": "Title",
@@ -26,7 +28,11 @@
"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.projectErrors.checkUrl": "Could not find that project. Check the URL and try again.",
+ "studio.projectErrors.generic": "Could not add project.",
+ "studio.projectErrors.tooFast": "You are adding projects too quickly.",
+ "studio.projectErrors.permission": "You do not have permission to add that project.",
+ "studio.projectErrors.duplicate": "That project is already in this studio.",
"studio.creatorRole": "Studio Creator",
@@ -35,11 +41,16 @@
"studio.unfollowStudio": "Unfollow Studio",
"studio.followStudio": "Follow Studio",
+ "studio.editThumbnail": "Edit Thumbnail",
+
"studio.curatorsHeader": "Curators",
"studio.inviteCuratorsHeader": "Invite Curators",
"studio.inviteCurator": "Invite",
"studio.inviteCuratorPlaceholder": "Scratch Username",
+ "studio.curatorInvitationAccepted": "Congratulations! You are now a curator of this studio.",
+ "studio.curatorInvitation": "You’ve been invited to become a curator of this studio.",
"studio.curatorAcceptInvite": "Accept Invite",
+ "studio.curatorInvitationError": "Something went wrong, try again later.",
"studio.curatorsEmptyCanAdd1": "You don’t have curators right now.",
"studio.curatorsEmptyCanAdd2": "Add some curators to collaborate with!",
"studio.curatorsEmpty1": "This studio has no curators right now.",
@@ -47,11 +58,19 @@
"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.curatorDoYouWantToPromote": "Do you want to promote this person to a manager?",
+ "studio.curatorManagersCan": "Managers can...",
+ "studio.curatorAddAndDeleteCurators": "add and delete curators",
+ "studio.curatorDeleteManagers": "delete other managers",
+ "studio.curatorAddAndDeleteProjects": "add and delete projects",
+ "studio.curatorIfYouTrust": "If you trust this person and you’re sure you want to give them extra permissions, click Promote.",
"studio.remove": "Remove",
"studio.promote": "Promote",
+ "studio.cancel": "Cancel",
"studio.commentsHeader": "Comments",
+ "studio.commentsNotAllowed": "Commenting for this studio has been turned off.",
"studio.comments.toggleOff": "Commenting off",
"studio.comments.toggleOn": "Commenting on",
"studio.comments.turnedOff": "Sorry, comment posting has been turned off for this studio.",
@@ -69,9 +88,27 @@
"studio.activityRemoveCurator": "{removerProfileLink} removed the curator {removedProfileLink}",
"studio.activityBecomeOwner": "{promotedProfileLink} was promoted to manager by {promotorProfileLink}",
+ "studio.lastUpdated": "Updated {lastUpdatedDate, date, medium}",
+ "studio.followerCount": "{followerCount} followers",
+
"studio.reportThisStudio": "Report this studio",
- "studio.reportPleaseExplain": "Please explain why you feel this studio is disrespectful or inappropriate, or otherwise breaks the Scratch Community Guidelines.",
+ "studio.reportPleaseExplain": "Please select which part of the studio you find to be disrespectful or inappropriate, or otherwise breaks the Scratch Community Guidelines.",
"studio.reportAreThereComments": "Are there inappropriate comments in the studio? Please report them by clicking the \"report\" button on the individual comments.",
"studio.reportThanksForLettingUsKnow": "Thanks for letting us know!",
- "studio.reportYourFeedback": "Your feedback will help us make Scratch better."
+ "studio.reportYourFeedback": "Your feedback will help us make Scratch better.",
+
+ "studios.mutedCurators": "You will be able to invite curators and add managers again {inDuration}.",
+ "studios.mutedProjects": "You will be able to add and remove projects again {inDuration}.",
+ "studios.mutedEdit": "You will be able to edit studios again {inDuration}.",
+ "studios.mutedPaused": "Your account has been paused from using studios until then.",
+
+ "studio.alertProjectAdded": "\"{title}\" added to studio",
+ "studio.alertProjectAlreadyAdded": "That project is already in this studio",
+ "studio.alertProjectRemoveError": "Something went wrong removing the project",
+ "studio.alertProjectAddError": "Something went wrong adding the project",
+ "studio.alertCuratorAlreadyInvited": "\"{name}\" has already been invited",
+ "studio.alertCuratorInvited": "Curator invite sent to \"{name}\"",
+ "studio.alertManagerPromote": "\"{name}\" is now a manager",
+ "studio.alertManagerPromoteError": "Something went wrong promoting \"{name}\"",
+ "studio.alertMemberRemoveError": "Something went wrong removing \"{name}\""
}
diff --git a/src/views/studio/lib/studio-member-actions.js b/src/views/studio/lib/studio-member-actions.js
index 887ff683a..1fb13c373 100644
--- a/src/views/studio/lib/studio-member-actions.js
+++ b/src/views/studio/lib/studio-member-actions.js
@@ -121,8 +121,6 @@ const inviteCurator = username => ((dispatch, getState) => new Promise((resolve,
}, (err, body, res) => {
const error = normalizeError(err, body, res);
if (error) return reject(error);
- // eslint-disable-next-line no-alert
- alert(`successfully invited ${username}`);
return resolve(username);
});
}));
@@ -169,7 +167,9 @@ const acceptInvitation = () => ((dispatch, getState) => new Promise((resolve, re
// Note: this assumes that the user items from the curator endpoint
// are the same structure as the single user data returned from /users/:username
dispatch(curators.actions.create(userBody, true));
- dispatch(setRoles({invited: false, curator: true}));
+ setTimeout(() => {
+ dispatch(setRoles({invited: false, curator: true}));
+ }, 5 * 1000);
return resolve();
});
});
diff --git a/src/views/studio/lib/user-projects-actions.js b/src/views/studio/lib/user-projects-actions.js
index 6c568a421..a50e025b2 100644
--- a/src/views/studio/lib/user-projects-actions.js
+++ b/src/views/studio/lib/user-projects-actions.js
@@ -44,7 +44,7 @@ const normalizeError = (err, body, res) => {
const loadUserProjects = type => ((dispatch, getState) => {
const state = getState();
const projectCount = userProjects.selector(state).items.length;
- const projectsPerPage = 20;
+ const projectsPerPage = 24;
const opts = {
...Endpoints[type](state),
params: {
diff --git a/src/views/studio/modals/promote-modal.jsx b/src/views/studio/modals/promote-modal.jsx
new file mode 100644
index 000000000..aa1a7a371
--- /dev/null
+++ b/src/views/studio/modals/promote-modal.jsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {FormattedMessage} from 'react-intl';
+
+import Modal from '../../../components/modal/base/modal.jsx';
+import ModalTitle from '../../../components/modal/base/modal-title.jsx';
+import ModalInnerContent from '../../../components/modal/base/modal-inner-content.jsx';
+
+import './promote-modal.scss';
+
+const PromoteModal = ({
+ handleClose,
+ handlePromote,
+ username
+}) => (
+
+
+
+
![](/svgs/studio/promote-illustration.svg)
+
+
+
+
+ {username}
+
+
+
+
+
+
+
+
+
+
+
+);
+
+PromoteModal.propTypes = {
+ handleClose: PropTypes.func,
+ handlePromote: PropTypes.func,
+ username: PropTypes.string
+};
+
+export default PromoteModal;
diff --git a/src/views/studio/modals/promote-modal.scss b/src/views/studio/modals/promote-modal.scss
new file mode 100644
index 000000000..8f429d2f9
--- /dev/null
+++ b/src/views/studio/modals/promote-modal.scss
@@ -0,0 +1,50 @@
+@import "../../../colors";
+
+.promote-modal {
+ .promote-title {
+ background: $ui-blue;
+ border-top-left-radius: 12px;
+ border-top-right-radius: 12px;
+ padding-top: .75rem;
+ width: 100%;
+ height: 3rem;
+ cursor: pointer;
+ }
+
+ h2 {
+ line-height: 2.5rem;
+ margin-bottom: 1rem;
+ }
+
+ ul {
+ line-height: 1rem;
+ }
+
+ .promote-content {
+ display: flex;
+ align-items: flex-start;
+ }
+
+ .promote-image {
+ margin-top: 2rem;
+ }
+
+ .promote-inner {
+ padding: 2rem;
+ }
+
+ .promote-button-row {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ .button {
+ margin-left: 0.5rem;
+ }
+
+ .cancel-button {
+ background-color: $ui-white;
+ color: $ui-blue;
+ box-shadow: 0px 0px 0 1px $ui-blue;
+ }
+}
diff --git a/src/views/studio/modals/studio-report-modal.jsx b/src/views/studio/modals/studio-report-modal.jsx
index bec0fcf4d..e29bd5daf 100644
--- a/src/views/studio/modals/studio-report-modal.jsx
+++ b/src/views/studio/modals/studio-report-modal.jsx
@@ -46,16 +46,25 @@ const StudioReportModal = ({
isOpen
className="studio-report-modal"
onRequestClose={handleClose}
+ useStandardSizes
>
-
-
-
-
+
+
+
+
+
+
) : (
- setFilter(Filters.SHARED)}
>
-
-
+
-
+
+
{showStudentsFilter &&
- setFilter(Filters.STUDENTS)}
>
-
+
}
- {error && Error loading {filter}: {error}
}
-
- {items.map(project => (
-
- ))}
- {moreToLoad &&
-
-
+
+ {error && Error loading {filter}: {error}
}
+
+
+ {items.map(project => (
+
+ ))}
+ {moreToLoad &&
+
+
+
}
-
+
);
diff --git a/src/views/studio/modals/user-projects-modal.scss b/src/views/studio/modals/user-projects-modal.scss
index 3530aa73f..87c70a7a5 100644
--- a/src/views/studio/modals/user-projects-modal.scss
+++ b/src/views/studio/modals/user-projects-modal.scss
@@ -13,7 +13,7 @@
}
.user-projects-modal-nav {
padding: 6px 12px;
- li {
+ button {
cursor: pointer;
background: rgba(0, 0, 0, 0.15);
&.active { background: $ui-blue; }
@@ -31,8 +31,20 @@
& { max-height: calc(100vh - 105px); }
}
}
+
+ .studio-projects-load-more {
+ display: contents;
+ }
}
+.studio-tile-added {
+ border: 1px solid $ui-blue !important; // Override the tile border set in studio.scss .studio-project-tile
+ box-shadow: 0 0 0 4px $ui-blue-25percent;
+}
+
+.studio-project-add-remove-image {
+ margin: 7px;
+}
.studio-tile-dynamic-remove,
.studio-tile-dynamic-add {
@@ -50,7 +62,9 @@
margin: 0;
padding: 0;
line-height: 32px;
- text-align: center;
+ align-content: center;
+ display: flex;
+ justify-content: center;
}
.studio-tile-dynamic-remove {
background: #0FBD8C;
@@ -60,6 +74,7 @@
.user-projects-modal-grid {
margin-top: 12px;
+ margin-bottom: 8px;
display: grid;
grid-template-columns: repeat(3, minmax(0,1fr));
@@ -72,10 +87,6 @@
column-gap: 14px;
row-gap: 14px;
- .studio-projects-load-more {
- grid-column: 1 / -1;
- }
-
.studio-project-bottom {
padding: 8px 10px 8px 10px;
}
diff --git a/src/views/studio/modals/user-projects-tile.jsx b/src/views/studio/modals/user-projects-tile.jsx
index cf49a1999..409ac0bca 100644
--- a/src/views/studio/modals/user-projects-tile.jsx
+++ b/src/views/studio/modals/user-projects-tile.jsx
@@ -1,23 +1,26 @@
/* eslint-disable react/jsx-no-bind */
-import React, {useState} from 'react';
+import React, {useContext, useState} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
+import AlertContext from '../../../components/alert/alert-context.js';
const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
const [submitting, setSubmitting] = useState(false);
const [added, setAdded] = useState(inStudio);
- const [error, setError] = useState(null);
+ const {errorAlert} = useContext(AlertContext);
const toggle = () => {
setSubmitting(true);
- setError(null);
(added ? onRemove(id) : onAdd(id))
.then(() => {
setAdded(!added);
setSubmitting(false);
})
- .catch(e => {
- setError(e);
+ .catch(() => {
setSubmitting(false);
+ errorAlert({
+ id: added ? 'studio.alertProjectRemoveError' :
+ 'studio.alertProjectAddError'
+ }, null);
});
};
return (
@@ -25,6 +28,7 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
role="button"
tabIndex="0"
className={classNames('studio-project-tile', {
+ 'studio-tile-added': added,
'mod-clickable': true,
'mod-mutating': submitting
})}
@@ -43,9 +47,14 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
{title}
- {added ? '✔' : '+'}
+
- {error &&
{error}
}
);
diff --git a/src/views/studio/studio-admin-panel.jsx b/src/views/studio/studio-admin-panel.jsx
new file mode 100644
index 000000000..817d9befd
--- /dev/null
+++ b/src/views/studio/studio-admin-panel.jsx
@@ -0,0 +1,95 @@
+/* eslint-disable react/jsx-no-bind */
+import React, {useState, useEffect} from 'react';
+import {connect} from 'react-redux';
+import classNames from 'classnames';
+
+
+import {selectIsAdmin} from '../../redux/session.js';
+import {selectStudioId} from '../../redux/studio.js';
+import log from '../../lib/log';
+
+import AdminPanel from '../../components/adminpanel/adminpanel.jsx';
+
+const adminPanelOpenKey = 'adminPanelToggled_studios';
+const adminPanelOpenClass = 'mod-view-admin-panel-open';
+
+/**
+ * Propagate the admin panel openness to localStorage and set a class name
+ * on the #view element.
+ * @param {boolean} value - whether the admin panel is now open.
+ */
+const storeAdminPanelOpen = value => {
+ try {
+ localStorage.setItem(adminPanelOpenKey, value ? 'open' : 'closed');
+ } catch (e) {
+ log.error('Could not set adminPanelToggled_studios in local storage', e);
+ }
+ try {
+ document.querySelector('#view').classList
+ .toggle(adminPanelOpenClass, value);
+ } catch (e) {
+ log.error('Could not set admin-panel-open class on #view');
+ }
+};
+
+/**
+ * Get the previous stored value of admin panel openness from localStorage.
+ * @returns {boolean} - whether the admin panel should be open.
+ */
+const getAdminPanelOpen = () => {
+ try {
+ return localStorage.getItem(adminPanelOpenKey) === 'open';
+ } catch (_) {
+ return false;
+ }
+};
+
+const StudioAdminPanel = ({studioId, showAdminPanel}) => {
+ const [adminPanelOpen, setAdminPanelOpen] = useState(getAdminPanelOpen());
+
+ useEffect(() => {
+ storeAdminPanelOpen(adminPanelOpen);
+ }, [adminPanelOpen]);
+
+ useEffect(() => {
+ if (!showAdminPanel) return;
+ const handleMessage = e => {
+ if (e.data === 'closePanel') setAdminPanelOpen(false);
+ };
+ window.addEventListener('message', handleMessage);
+ return () => {
+ window.removeEventListener('message', handleMessage);
+ };
+ }, [showAdminPanel]);
+
+ return showAdminPanel && (
+ setAdminPanelOpen(true)}
+ >
+
+
+ );
+};
+
+const ConnnectedStudioAdminPanel = connect(
+ state => ({
+ studioId: selectStudioId(state),
+ showAdminPanel: selectIsAdmin(state)
+ })
+)(StudioAdminPanel);
+
+export {
+ ConnnectedStudioAdminPanel as default,
+ // Export the unconnected component by name for testing
+ StudioAdminPanel,
+ // Export some constants for easy testing as well
+ adminPanelOpenClass,
+ adminPanelOpenKey
+};
diff --git a/src/views/studio/studio-comments-not-allowed.jsx b/src/views/studio/studio-comments-not-allowed.jsx
new file mode 100644
index 000000000..15f5d5cf5
--- /dev/null
+++ b/src/views/studio/studio-comments-not-allowed.jsx
@@ -0,0 +1,10 @@
+const React = require('react');
+const FormattedMessage = require('react-intl').FormattedMessage;
+
+const StudioCommentsNotAllowed = () => (
+
+
+
+);
+
+module.exports = StudioCommentsNotAllowed;
diff --git a/src/views/studio/studio-comments.jsx b/src/views/studio/studio-comments.jsx
index b6ac7c15d..22cda114c 100644
--- a/src/views/studio/studio-comments.jsx
+++ b/src/views/studio/studio-comments.jsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useRef} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {FormattedMessage} from 'react-intl';
@@ -8,8 +8,9 @@ import ComposeComment from '../preview/comment/compose-comment.jsx';
import TopLevelComment from '../preview/comment/top-level-comment.jsx';
import studioCommentActions from '../../redux/studio-comment-actions.js';
import StudioCommentsAllowed from './studio-comments-allowed.jsx';
+import StudioCommentsNotAllowed from './studio-comments-not-allowed.jsx';
-import {selectIsAdmin} from '../../redux/session';
+import {selectIsAdmin, selectHasFetchedSession} from '../../redux/session';
import {
selectShowCommentComposer,
selectCanDeleteComment,
@@ -24,6 +25,7 @@ const StudioComments = ({
comments,
commentsAllowed,
isAdmin,
+ hasFetchedSession,
handleLoadMoreComments,
handleNewComment,
moreCommentsToLoad,
@@ -42,8 +44,8 @@ const StudioComments = ({
handleLoadMoreReplies
}) => {
useEffect(() => {
- if (comments.length === 0) handleLoadMoreComments();
- }, [comments.length === 0]);
+ if (comments.length === 0 && hasFetchedSession) handleLoadMoreComments();
+ }, [comments.length === 0, hasFetchedSession]);
// The comments you see depend on your admin status
// so reset them if isAdmin changes.
@@ -54,21 +56,37 @@ const StudioComments = ({
if (isAdmin !== wasAdmin) handleResetComments();
}, [isAdmin]);
+ const [replyStatusCommentId, setReplyStatusCommentId] = useState('');
+
+ const hasReplyStatus = function (comment) {
+ return (
+ comment.parent_id && comment.parent_id === replyStatusCommentId
+ ) || (comment.id === replyStatusCommentId);
+ };
+
+ const handleReplyStatusChange = function (id) {
+ setReplyStatusCommentId(id);
+ };
+
return (
-
+
{canEditCommentsAllowed && }
-
- {shouldShowCommentComposer && commentsAllowed &&
-
+
+ {shouldShowCommentComposer ?
+ (commentsAllowed ?
+
:
+
+ ) : null
}
{comments.map(comment => (
@@ -108,6 +130,7 @@ StudioComments.propTypes = {
comments: PropTypes.arrayOf(PropTypes.shape({})),
commentsAllowed: PropTypes.bool,
isAdmin: PropTypes.bool,
+ hasFetchedSession: PropTypes.bool,
handleLoadMoreComments: PropTypes.func,
handleNewComment: PropTypes.func,
moreCommentsToLoad: PropTypes.bool,
@@ -133,6 +156,7 @@ export {
export default connect(
state => ({
comments: state.comments.comments,
+ hasFetchedSession: selectHasFetchedSession(state),
isAdmin: selectIsAdmin(state),
moreCommentsToLoad: state.comments.moreCommentsToLoad,
replies: state.comments.replies,
diff --git a/src/views/studio/studio-curator-invite.jsx b/src/views/studio/studio-curator-invite.jsx
index 7fe1a129a..9995367c7 100644
--- a/src/views/studio/studio-curator-invite.jsx
+++ b/src/views/studio/studio-curator-invite.jsx
@@ -6,15 +6,42 @@ import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {acceptInvitation} from './lib/studio-member-actions';
+import {selectShowCuratorInvite} from '../../redux/studio-permissions';
-const StudioCuratorInvite = ({onSubmit}) => {
+const StudioCuratorInvite = ({showCuratorInvite, onSubmit}) => {
const [submitting, setSubmitting] = useState(false);
- const [error, setError] = useState(null);
+ const [accepted, setAccepted] = useState(false);
+ const [error, setError] = useState(false);
+
+ if (!showCuratorInvite) return null;
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (accepted) {
+ return (
+
+ );
+ }
return (
-
+
+
+
+
- {error &&
{error}
}
);
};
StudioCuratorInvite.propTypes = {
+ showCuratorInvite: PropTypes.func,
onSubmit: PropTypes.func
};
-const mapStateToProps = () => ({});
+const mapStateToProps = state => ({
+ showCuratorInvite: selectShowCuratorInvite(state)
+});
const mapDispatchToProps = ({
onSubmit: acceptInvitation
diff --git a/src/views/studio/studio-curator-inviter.jsx b/src/views/studio/studio-curator-inviter.jsx
index 25a8b83d0..6d92b5e96 100644
--- a/src/views/studio/studio-curator-inviter.jsx
+++ b/src/views/studio/studio-curator-inviter.jsx
@@ -5,6 +5,7 @@ import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
+import {useAlertContext} from '../../components/alert/alert-context';
import {Errors, inviteCurator} from './lib/studio-member-actions';
import ValidationMessage from '../../components/forms/validation-message.jsx';
@@ -24,12 +25,30 @@ const StudioCuratorInviter = ({intl, onSubmit}) => {
const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
+ const {successAlert} = useAlertContext();
+
const submit = () => {
setSubmitting(true);
setError(null);
onSubmit(value)
- .then(() => setValue(''))
- .catch(e => setError(e))
+ .then(() => {
+ successAlert({
+ id: 'studio.alertCuratorInvited',
+ values: {name: value}
+ });
+ setValue('');
+ })
+ .catch(e => {
+ if (e === Errors.DUPLICATE) {
+ successAlert({
+ id: 'studio.alertCuratorAlreadyInvited',
+ values: {name: value}
+ });
+ setValue('');
+ } else {
+ setError(e);
+ }
+ })
.then(() => setSubmitting(false));
};
return (
diff --git a/src/views/studio/studio-curators.jsx b/src/views/studio/studio-curators.jsx
index 089eb5549..687a8e2c3 100644
--- a/src/views/studio/studio-curators.jsx
+++ b/src/views/studio/studio-curators.jsx
@@ -8,72 +8,76 @@ import {curators} from './lib/redux-modules';
import Debug from './debug.jsx';
import {CuratorTile} from './studio-member-tile.jsx';
import CuratorInviter from './studio-curator-inviter.jsx';
-import CuratorInvite from './studio-curator-invite.jsx';
import {loadCurators} from './lib/studio-member-actions';
-import {selectCanInviteCurators, selectShowCuratorInvite} from '../../redux/studio-permissions';
+import {selectCanInviteCurators} from '../../redux/studio-permissions';
+import AlertProvider from '../../components/alert/alert-provider.jsx';
+import Alert from '../../components/alert/alert.jsx';
const StudioCurators = ({
- canInviteCurators, showCuratorInvite, items, error, loading, moreToLoad, onLoadMore
+ canInviteCurators, items, error, loading, moreToLoad, onLoadMore
}) => {
useEffect(() => {
if (items.length === 0) onLoadMore();
}, []);
- return (
-
-
-
- {canInviteCurators &&
}
- {showCuratorInvite &&
}
- {error &&
}
-
- {items.length === 0 && !loading ? (
-
-
![](/images/studios/curators-empty.png)
- {canInviteCurators ? (
-
-
-
+ return (
+
+
+
+
+
+
+ {canInviteCurators &&
}
+ {error &&
}
+
+ {items.length === 0 && !loading ? (
+
+
![](/images/studios/curators-empty.png)
+ {canInviteCurators ? (
+
+ ) : (
+
+ )}
) : (
-
+
+ {items.map(item =>
+ ()
+ )}
+ {moreToLoad &&
+
+
+
+ }
+
)}
- ) : (
-
- {items.map(item =>
- ()
- )}
- {moreToLoad &&
-
-
-
- }
-
- )}
-
- );
+
+ );
};
StudioCurators.propTypes = {
@@ -87,7 +91,6 @@ StudioCurators.propTypes = {
})
})),
canInviteCurators: PropTypes.bool,
- showCuratorInvite: PropTypes.bool,
loading: PropTypes.bool,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
moreToLoad: PropTypes.bool,
@@ -97,8 +100,7 @@ StudioCurators.propTypes = {
export default connect(
state => ({
...curators.selector(state),
- canInviteCurators: selectCanInviteCurators(state),
- showCuratorInvite: selectShowCuratorInvite(state)
+ canInviteCurators: selectCanInviteCurators(state)
}),
{
onLoadMore: loadCurators
diff --git a/src/views/studio/studio-description.jsx b/src/views/studio/studio-description.jsx
index 3f3765302..11a16f7ff 100644
--- a/src/views/studio/studio-description.jsx
+++ b/src/views/studio/studio-description.jsx
@@ -1,17 +1,19 @@
/* eslint-disable react/jsx-no-bind */
-import React from 'react';
+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 {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio';
-import {selectCanEditInfo} from '../../redux/studio-permissions';
+import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
import {
Errors, mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
} from '../../redux/studio-mutations';
import ValidationMessage from '../../components/forms/validation-message.jsx';
+import decorateText from '../../lib/decorate-text.jsx';
+import StudioMuteEditMessage from './studio-mute-edit-message.jsx';
const errorToMessageId = error => {
switch (error) {
@@ -23,27 +25,48 @@ const errorToMessageId = error => {
};
const StudioDescription = ({
- descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate
+ descriptionError, isFetching, isMutating, isMutedEditor, description, canEditInfo, handleUpdate
}) => {
+ const [showMuteMessage, setShowMuteMessage] = useState(false);
+
const fieldClassName = classNames('studio-description', {
'mod-fetching': isFetching,
'mod-mutating': isMutating,
- 'mod-form-error': !!descriptionError
+ 'mod-form-error': !!descriptionError,
+ 'muted-editor': showMuteMessage
});
+
return (
-