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 && } +
+ +
+ {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 @@ -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 +}) => ( + + +
+ + +

+ +
+ {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)} + > +