diff --git a/package-lock.json b/package-lock.json index 9e62cbaf9..029a35877 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,20 +61,20 @@ } }, "chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "dev": true, "optional": true, "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" + "readdirp": "~3.6.0" } }, "commander": { @@ -176,9 +176,9 @@ "optional": true }, "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "optional": true, "requires": { @@ -219,9 +219,9 @@ } }, "@babel/compat-data": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.5.tgz", - "integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz", + "integrity": "sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==", "dev": true }, "@babel/core": { @@ -308,9 +308,9 @@ } }, "@babel/parser": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz", - "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -325,9 +325,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -335,7 +335,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -372,9 +372,9 @@ } }, "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "dev": true, "requires": { "safe-buffer": "~5.1.1" @@ -504,9 +504,9 @@ } }, "electron-to-chromium": { - "version": "1.3.752", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz", - "integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==", + "version": "1.3.755", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.755.tgz", + "integrity": "sha512-BJ1s/kuUuOeo1bF/EM2E4yqW9te0Hpof3wgwBx40AWJE18zsD1Tqo0kr7ijnOc+lRsrlrqKPauJAHqaxOItoUA==", "dev": true }, "semver": { @@ -565,9 +565,9 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", "dev": true, "requires": { "@babel/types": "^7.14.5" @@ -695,9 +695,9 @@ } }, "@babel/parser": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz", - "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -712,9 +712,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -722,7 +722,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -914,9 +914,9 @@ } }, "@babel/parser": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz", - "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -931,9 +931,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -941,7 +941,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -1147,9 +1147,9 @@ } }, "@babel/parser": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz", - "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -1164,9 +1164,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -1174,7 +1174,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -7613,6 +7613,12 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", + "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", + "dev": true + }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -20873,9 +20879,9 @@ } }, "scratch-blocks": { - "version": "0.1.0-prerelease.20210615035054", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210615035054.tgz", - "integrity": "sha512-/YGZN3QiMa41gtcyHUUUVTfhztoT7zEb0+N+FeBmtbkZfeESOtEugJ0y0ftttUY7WJRga35TTDp1UdEyo7sxAg==", + "version": "0.1.0-prerelease.20210620032544", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210620032544.tgz", + "integrity": "sha512-lGid69E7uvW9XZyx1u7skwzBSD54AmFlG9Rvf1JxGxyuggt5LsIVLlyrnDUSLLDFfrpcnWgYUgICdx7OPbGIaw==", "dev": true, "requires": { "exports-loader": "0.6.3", @@ -20883,9 +20889,9 @@ } }, "scratch-gui": { - "version": "0.1.0-prerelease.20210615041617", - "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210615041617.tgz", - "integrity": "sha512-HVmqbo9MLfV5tvc5owt7BT+fJKj5QRrz+c4S2vc8WyAsxMEerAYKfaNCHI89LmA5CRb42sgb0Z6YnwlsNVsaiw==", + "version": "0.1.0-prerelease.20210621040041", + "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210621040041.tgz", + "integrity": "sha512-UtAPMLhTQWYLtIgHEVegOzQuHr4A3KqTWj31IGt5taaZxxpT8TsSC0A1stas/rqQB9/gXalKyX3LylogesfEnw==", "dev": true, "requires": { "arraybuffer-loader": "^1.0.6", @@ -20936,8 +20942,8 @@ "redux": "3.7.2", "redux-throttle": "0.1.1", "scratch-audio": "0.1.0-prerelease.20200528195344", - "scratch-blocks": "0.1.0-prerelease.20210615035054", - "scratch-l10n": "3.12.20210615031544", + "scratch-blocks": "0.1.0-prerelease.20210620032544", + "scratch-l10n": "3.13.20210621031558", "scratch-paint": "0.2.0-prerelease.20210615011117", "scratch-render": "0.1.0-prerelease.20210325231800", "scratch-render-fonts": "1.0.0-prerelease.20210401210003", @@ -21328,6 +21334,18 @@ "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", "dev": true }, + "scratch-l10n": { + "version": "3.13.20210621031558", + "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.13.20210621031558.tgz", + "integrity": "sha512-tZfvJkxiIfwMd996qfmZ8/m05gIEuVUx5UAs0QXAPfqNwyBYIHUaJBihTxN1K4SWffL4Z80iUnLJZiQGwMqtyw==", + "dev": true, + "requires": { + "@babel/cli": "^7.1.2", + "@babel/core": "^7.1.2", + "babel-plugin-react-intl": "^3.0.1", + "transifex": "1.6.6" + } + }, "scratch-storage": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.5.tgz", @@ -21394,15 +21412,14 @@ } }, "scratch-l10n": { - "version": "3.12.20210615031544", - "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.12.20210615031544.tgz", - "integrity": "sha512-8U2y0wu+xy29ayND5bY4odklo9D/5mVW1XQ+YrBx7rykUPHiJOzPUYHvTUQVXC0CI8khAtW98Lt3SI9m4MxBuw==", + "version": "3.13.20210623031509", + "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.13.20210623031509.tgz", + "integrity": "sha512-TT5+0Gz20tZ3PLEspb5OmZwY23+OxbrSlJuMMQGM24HUxK7nyjMbncwuAzVnd7v9ef8BqH+nQ9nEItKQNcPTBQ==", "dev": true, "requires": { "@babel/cli": "^7.1.2", "@babel/core": "^7.1.2", "babel-plugin-react-intl": "^3.0.1", - "react-intl": "^2.8.0", "transifex": "1.6.6" } }, diff --git a/package.json b/package.json index 01ae88a91..988afa5d9 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "eslint-config-scratch": "7.0.0", "eslint-plugin-json": "2.0.1", "eslint-plugin-react": "7.14.2", + "eslint-plugin-react-hooks": "^4.2.0", "fastly": "1.2.1", "formik": "1.5.4", "formsy-react": "1.1.4", @@ -126,8 +127,8 @@ "redux-mock-store": "^1.2.3", "redux-thunk": "2.0.1", "sass-loader": "6.0.6", - "scratch-gui": "0.1.0-prerelease.20210615041617", - "scratch-l10n": "3.12.20210615031544", + "scratch-gui": "0.1.0-prerelease.20210621040041", + "scratch-l10n": "3.13.20210623031509", "selenium-webdriver": "3.6.0", "slick-carousel": "1.6.0", "style-loader": "0.12.3", diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 81246be59..f8107fee1 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -7,7 +7,7 @@ module.exports = { globals: { process: true }, - plugins: ['json'], + plugins: ['json', 'react-hooks'], settings: { react: { version: 'detect' @@ -17,6 +17,7 @@ module.exports = { 'camelcase': [2, { properties: 'never', // This is from the base `scratch` config allow: ['^UNSAFE_'] // Allow until migrated to new lifecycle methods - }] + }], + 'react-hooks/rules-of-hooks': 'error' } }; diff --git a/src/lib/admin-requests.js b/src/lib/admin-requests.js new file mode 100644 index 000000000..6fdfed705 --- /dev/null +++ b/src/lib/admin-requests.js @@ -0,0 +1,23 @@ +import {selectIsAdmin, selectToken} from '../redux/session'; + +/** + * Augment an `options` object that will be used by api.js + * to automatically include admin authentication token and + * /admin url prefix if the user is an admin. + * + * @param {object} opts Object argument for api.js request + * @param {string} opts.uri A uri that may be prefixed with /admin + * @param {object} state The full redux state tree to use with session selectors + * @returns {object} The augmented options object + */ +const withAdmin = (opts, state) => { + if (selectIsAdmin(state)) { + return Object.assign({}, opts, { + uri: `/admin${opts.uri}`, + authentication: selectToken(state) + }); + } + return opts; +}; + +export {withAdmin}; diff --git a/src/redux/comments.js b/src/redux/comments.js index 129242877..e51291858 100644 --- a/src/redux/comments.js +++ b/src/redux/comments.js @@ -70,7 +70,17 @@ module.exports.commentsReducer = (state, action) => { return Object.assign({}, state, { replies: Object.assign({}, state.replies, { // Replies to comments go at the end of the thread - [action.topLevelCommentId]: state.replies[action.topLevelCommentId].concat(action.comment) + [action.topLevelCommentId]: + (state.replies[action.topLevelCommentId] || []) + .concat(action.comment) + }), + comments: state.comments.map(comment => { + if (comment.id === action.topLevelCommentId) { + return Object.assign({}, comment, { + reply_count: comment.reply_count + 1 + }); + } + return comment; }) }); } diff --git a/src/redux/session.js b/src/redux/session.js index fc2822721..f7d762017 100644 --- a/src/redux/session.js +++ b/src/redux/session.js @@ -142,9 +142,13 @@ module.exports.selectIsSocial = state => 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.selectStudioCommentsGloballyEnabled = state => + get(state, ['session', 'session', 'flags', 'gallery_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.selectNewStudiosLaunched = state => get(state, ['session', 'session', 'flags', 'new_studios_launched'], + false); module.exports.selectHasFetchedSession = state => state.session.status === module.exports.Status.FETCHED; diff --git a/src/redux/studio-permissions.js b/src/redux/studio-permissions.js index 0afdb2735..93d6a8b75 100644 --- a/src/redux/studio-permissions.js +++ b/src/redux/studio-permissions.js @@ -1,5 +1,6 @@ const {selectUserId, selectIsAdmin, selectIsSocial, - selectIsLoggedIn, selectUsername, selectIsMuted} = require('./session'); + selectIsLoggedIn, selectUsername, selectIsMuted, + selectHasFetchedSession, selectStudioCommentsGloballyEnabled} = require('./session'); // Fine-grain selector helpers - not exported, use the higher level selectors below const isCreator = state => selectUserId(state) === state.studio.owner; @@ -17,12 +18,18 @@ const selectCanAddProjects = state => // This isn't "canComment" since they could be muted, but comment composer handles that const selectShowCommentComposer = state => selectIsSocial(state); -const selectCanReportComment = state => selectIsSocial(state); +const selectCanReportComment = (state, commentUsername) => + selectIsLoggedIn(state) && selectUsername(state) !== commentUsername; const selectCanRestoreComment = state => selectIsAdmin(state); // On the project page, project owners can delete comments with a confirmation, -// and admins can delete comments without a confirmation. For now, only admins -// can delete studio comments, so the following two are the same. -const selectCanDeleteComment = state => selectIsAdmin(state); +// and admins can delete comments without a confirmation. +// On the studio page, studio creators and managers have the ability to delete *their own* comments with confirmation. +// Admins can delete comments without a confirmation. +const selectCanDeleteComment = (state, commentUsername) => { + if (selectIsAdmin(state)) return true; + if (isManager(state) && selectUsername(state) === commentUsername) return true; + return false; +}; const selectCanDeleteCommentWithoutConfirm = state => selectIsAdmin(state); const selectCanFollowStudio = state => selectIsLoggedIn(state); @@ -71,7 +78,9 @@ const selectShowProjectMuteError = state => selectIsMuted(state) && isCurator(state) || (selectIsSocial(state) && state.studio.openToAll)); const selectShowCuratorMuteError = state => selectIsMuted(state) && (isManager(state) || selectIsAdmin(state)); - +const selectShowCommentsGloballyOffError = state => + selectHasFetchedSession(state) && !selectStudioCommentsGloballyEnabled(state); +const selectShowCommentsList = state => selectHasFetchedSession(state) && selectStudioCommentsGloballyEnabled(state); export { selectCanEditInfo, selectCanAddProjects, @@ -89,6 +98,8 @@ export { selectCanRemoveManager, selectCanPromoteCurators, selectCanRemoveProject, + selectShowCommentsList, + selectShowCommentsGloballyOffError, selectShowEditMuteError, selectShowProjectMuteError, selectShowCuratorMuteError diff --git a/src/redux/studio.js b/src/redux/studio.js index 09d1aef9c..ecbd58ad1 100644 --- a/src/redux/studio.js +++ b/src/redux/studio.js @@ -1,4 +1,5 @@ const keyMirror = require('keymirror'); +const {withAdmin} = require('../lib/admin-requests'); const api = require('../lib/api'); const log = require('../lib/log'); @@ -25,6 +26,7 @@ const getInitialState = () => ({ followers: 0, managers: 0, owner: null, + public: null, // BEWARE: classroomId is only loaded if the user is an educator classroomId: null, @@ -110,11 +112,14 @@ const selectIsFetchingInfo = state => state.studio.infoStatus === Status.FETCHIN const selectIsFollowing = state => state.studio.following; const selectIsFetchingRoles = state => state.studio.rolesStatus === Status.FETCHING; const selectClassroomId = state => state.studio.classroomId; +const selectStudioPublic = state => state.studio.public; // Thunks const getInfo = () => ((dispatch, getState) => { - const studioId = selectStudioId(getState()); - api({uri: `/studios/${studioId}`}, (err, body, res) => { + const state = getState(); + const studioId = selectStudioId(state); + const opts = {uri: `/studios/${studioId}`}; + api(withAdmin(opts, state), (err, body, res) => { if (err || typeof body === 'undefined' || res.statusCode !== 200) { dispatch(setFetchStatus('infoStatus', Status.ERROR, err)); return; @@ -130,7 +135,8 @@ const getInfo = () => ((dispatch, getState) => { followers: body.stats.followers, managers: body.stats.managers, projectCount: body.stats.projects, - owner: body.owner + owner: body.owner, + public: body.public })); }); }); @@ -199,5 +205,6 @@ module.exports = { selectIsFetchingInfo, selectIsFetchingRoles, selectIsFollowing, - selectClassroomId + selectClassroomId, + selectStudioPublic }; diff --git a/src/views/preview/comment/top-level-comment.jsx b/src/views/preview/comment/top-level-comment.jsx index eb6ec167d..d33dcaeb5 100644 --- a/src/views/preview/comment/top-level-comment.jsx +++ b/src/views/preview/comment/top-level-comment.jsx @@ -131,7 +131,7 @@ class TopLevelComment extends React.Component { return ( - - ( - -
-
©
- {' '} - {/* eslint-disable react/jsx-sort-props */} - {props.shareDate ? ( - - ) : 'Unshared'} - {/* eslint-enable react/jsx-sort-props */} -
- - {(props.canReport) && - - - {props.reportOpen && ( - - )} - - } - {props.canAddToStudio && - - - {props.addToStudioOpen && ( - - )} - - } - {/* only show copy link button, modal if project is shared */} - {props.isShared && props.projectInfo && props.projectInfo.id && ( - - - {props.socialOpen && ( - - )} - - )} +const Subactions = ({ + addToStudioOpen, + canAddToStudio, + canReport, + isAdmin, + isShared, + onAddToStudioClicked, + onAddToStudioClosed, + onReportClicked, + onReportClose, + onReportSubmit, + onSocialClicked, + onSocialClosed, + onToggleStudio, + projectInfo, + reportOpen, + shareDate, + showAddToStudioMuteError, + socialOpen, + userOwnsProject +}) => { + const [showMuteMessage, setShowMuteMessage] = useState(false); + + return ( + +
+
©
+ {' '} + {/* eslint-disable react/jsx-sort-props */} + {shareDate ? ( + + ) : 'Unshared'} + {/* eslint-enable react/jsx-sort-props */} +
+ + {(canReport) && + + + {reportOpen && ( + + )} + + } + {canAddToStudio && + +
showAddToStudioMuteError && setShowMuteMessage(true)} + onMouseLeave={() => showAddToStudioMuteError && setShowMuteMessage(false)} + /* eslint-enable react/jsx-no-bind */ + > + + {showMuteMessage && } +
+ {addToStudioOpen && ( + + )} +
+ } + {/* only show copy link button, modal if project is shared */} + {isShared && projectInfo && projectInfo.id && ( + + + {socialOpen && ( + + )} + + )} +
-
-); + ); +}; Subactions.propTypes = { addToStudioOpen: PropTypes.bool, @@ -110,8 +152,13 @@ Subactions.propTypes = { projectInfo: projectShape, reportOpen: PropTypes.bool, shareDate: PropTypes.string, + showAddToStudioMuteError: PropTypes.bool, socialOpen: PropTypes.bool, userOwnsProject: PropTypes.bool }; -module.exports = Subactions; +module.exports = connect( + state => ({ + showAddToStudioMuteError: selectNewStudiosLaunched(state) && selectIsMuted(state) + }) +)(Subactions); diff --git a/src/views/preview/subactions.scss b/src/views/preview/subactions.scss index bba4bd6c8..d70791d94 100644 --- a/src/views/preview/subactions.scss +++ b/src/views/preview/subactions.scss @@ -109,3 +109,10 @@ } } } + +.studio-button-error { + top: auto; + transform: none; + width: 100%; + margin-left: 0; +} diff --git a/src/views/studio/debug.jsx b/src/views/studio/debug.jsx deleted file mode 100644 index 3b037079e..000000000 --- a/src/views/studio/debug.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Debug = ({label, data}) => (
- {label} - -
-            {JSON.stringify(data, null, '  ')}
-        
-
-
); - -Debug.propTypes = { - label: PropTypes.string, - data: PropTypes.any // eslint-disable-line react/forbid-prop-types -}; - -export default Debug; diff --git a/src/views/studio/l10n.json b/src/views/studio/l10n.json index 36bb7eeed..0f27cafbf 100644 --- a/src/views/studio/l10n.json +++ b/src/views/studio/l10n.json @@ -6,6 +6,8 @@ "studio.tabNavCommentsWithCount": "Comments {commentCount}", "studio.tabNavActivity": "Activity", + "studio.showingDeleted": "Showing Deleted Studio", + "studio.title": "Title", "studio.description": "Description", "studio.thumbnail": "Thumbnail", @@ -16,6 +18,15 @@ "studio.updateErrors.thumbnailTooLarge": "Maximum file size is 512 KB and less than 500x500 pixels.", "studio.updateErrors.thumbnailInvalid": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.", + "studio.followErrors.confirmEmail": "Please confirm your email address first", + "studio.followErrors.generic": "Something went wrong following the studio", + + "studio.sectionLoadError.projectsHeadline": "Something went wrong loading projects", + "studio.sectionLoadError.curatorsHeadline": "Something went wrong loading curators", + "studio.sectionLoadError.managersHeadline": "Something went wrong loading managers", + "studio.sectionLoadError.activityHeadline": "Something went wrong loading activity", + "studio.sectionLoadError.tryAgain": "Try again", + "studio.projectsHeader": "Projects", "studio.addProjectsHeader": "Add Projects", "studio.addProject": "Add", @@ -88,6 +99,7 @@ "studio.comments.toggleOff": "Commenting off", "studio.comments.toggleOn": "Commenting on", "studio.comments.turnedOff": "Sorry, comment posting has been turned off for this studio.", + "studio.comments.turnedOffGlobally" : "Studio comments across Scratch are turned off, but don't worry, your comments are saved and will be back soon.", "studio.sharedFilter": "Shared", "studio.favoritedFilter": "Favorited", diff --git a/src/views/studio/lib/studio-activity-actions.js b/src/views/studio/lib/studio-activity-actions.js index 4a694cfa3..7800bd187 100644 --- a/src/views/studio/lib/studio-activity-actions.js +++ b/src/views/studio/lib/studio-activity-actions.js @@ -3,6 +3,7 @@ import keyMirror from 'keymirror'; import api from '../../../lib/api'; import {activity} from './redux-modules'; import {selectStudioId} from '../../../redux/studio'; +import {withAdmin} from '../../../lib/admin-requests'; const Errors = keyMirror({ NETWORK: null, @@ -27,10 +28,11 @@ const loadActivity = () => ((dispatch, getState) => { // the date of the oldest one we've already loaded params.dateLimit = items[items.length - 1].datetime_created; } - api({ + const opts = { uri: `/studios/${studioId}/activity/`, params - }, (err, body, res) => { + }; + api(withAdmin(opts, state), (err, body, res) => { const error = normalizeError(err, body, res); if (error) return dispatch(activity.actions.error(error)); const ids = items.map(item => item.id); diff --git a/src/views/studio/lib/studio-member-actions.js b/src/views/studio/lib/studio-member-actions.js index 88af84948..c8eb56db0 100644 --- a/src/views/studio/lib/studio-member-actions.js +++ b/src/views/studio/lib/studio-member-actions.js @@ -4,6 +4,7 @@ import api from '../../../lib/api'; import {curators, managers} from './redux-modules'; import {selectUsername} from '../../../redux/session'; import {selectStudioId, setRoles, setInfo} from '../../../redux/studio'; +import {withAdmin} from '../../../lib/admin-requests'; const Errors = keyMirror({ NETWORK: null, @@ -40,10 +41,11 @@ const loadManagers = () => ((dispatch, getState) => { const studioId = selectStudioId(state); const managerCount = managers.selector(state).items.length; const managersPerPage = 20; - api({ + const opts = { uri: `/studios/${studioId}/managers/`, params: {limit: managersPerPage, offset: managerCount} - }, (err, body, res) => { + }; + api(withAdmin(opts, state), (err, body, res) => { const error = normalizeError(err, body, res); if (error) return dispatch(managers.actions.error(error)); dispatch(managers.actions.append(body, body.length === managersPerPage)); @@ -55,10 +57,11 @@ const loadCurators = () => ((dispatch, getState) => { const studioId = selectStudioId(state); const curatorCount = curators.selector(state).items.length; const curatorsPerPage = 20; - api({ + const opts = { uri: `/studios/${studioId}/curators/`, params: {limit: curatorsPerPage, offset: curatorCount} - }, (err, body, res) => { + }; + api(withAdmin(opts, state), (err, body, res) => { const error = normalizeError(err, body, res); if (error) return dispatch(curators.actions.error(error)); dispatch(curators.actions.append(body, body.length === curatorsPerPage)); diff --git a/src/views/studio/lib/studio-project-actions.js b/src/views/studio/lib/studio-project-actions.js index a822fc803..0cf048cd6 100644 --- a/src/views/studio/lib/studio-project-actions.js +++ b/src/views/studio/lib/studio-project-actions.js @@ -1,4 +1,5 @@ import keyMirror from 'keymirror'; +import {withAdmin} from '../../../lib/admin-requests'; import api from '../../../lib/api'; import {selectToken} from '../../../redux/session'; @@ -32,10 +33,11 @@ const loadProjects = () => ((dispatch, getState) => { const studioId = selectStudioId(state); const projectCount = projects.selector(state).items.length; const projectsPerPage = 20; - api({ + const opts = { uri: `/studios/${studioId}/projects/`, params: {limit: projectsPerPage, offset: projectCount} - }, (err, body, res) => { + }; + api(withAdmin(opts, state), (err, body, res) => { const error = normalizeError(err, body, res); if (error) return dispatch(projects.actions.error(error)); dispatch(projects.actions.append(body, body.length === projectsPerPage)); diff --git a/src/views/studio/modals/user-projects-modal.jsx b/src/views/studio/modals/user-projects-modal.jsx index 2bf40ee2a..2014c5141 100644 --- a/src/views/studio/modals/user-projects-modal.jsx +++ b/src/views/studio/modals/user-projects-modal.jsx @@ -20,6 +20,7 @@ import './user-projects-modal.scss'; import {selectIsEducator} from '../../../redux/session'; import AlertProvider from '../../../components/alert/alert-provider.jsx'; import Alert from '../../../components/alert/alert.jsx'; +import Spinner from '../../../components/spinner/spinner.jsx'; const UserProjectsModal = ({ items, error, loading, moreToLoad, showStudentsFilter, @@ -77,7 +78,7 @@ const UserProjectsModal = ({ {error &&
Error loading {filter}: {error}
} - {items.length > 0 ? ( + {items.length > 0 &&
{items.map(project => ( @@ -105,7 +106,8 @@ const UserProjectsModal = ({
}
- ) : + } + {!loading && items.length === 0 &&
} + {loading && + + }
diff --git a/src/views/studio/modals/user-projects-modal.scss b/src/views/studio/modals/user-projects-modal.scss index adef9d1fa..315d53e78 100644 --- a/src/views/studio/modals/user-projects-modal.scss +++ b/src/views/studio/modals/user-projects-modal.scss @@ -13,21 +13,34 @@ } .user-projects-modal-nav { padding: 6px 12px; + border-bottom: 1px solid $active-gray; + width: unset; button { cursor: pointer; - background: rgba(0, 0, 0, 0.15); - &.active { background: $ui-blue; } + background: white; + border: 1px solid rgba(0, 0, 0, 0.15); + color: #575e75; + &.active { + background: $ui-blue; + color: white; + } + &:active { + padding: .75em 1.5em; + } + } + button:hover { + background: $ui-blue-25percent; + border: 1px solid $ui-blue-10percent; } } .user-projects-modal-content { - padding: 0 30px 30px; + padding: 0 30px 8px; background: #E9F1FC; - max-height: calc(100vh - 270px); - min-height: 300px; + height: calc(100vh - 300px); overflow-y: auto; overscroll-behavior: contain; @media #{$intermediate-and-smaller} { - & { max-height: calc(100vh - 175px); } + & { height: calc(100vh - 170px); } } } @@ -39,6 +52,7 @@ display: flex; justify-content: flex-end; padding: 6px 12px; + border-top: 1px solid $active-gray; } .studio-projects-empty { @@ -46,15 +60,20 @@ flex-direction: column; justify-content: center; align-items: center; - margin: 4rem; + margin: auto; } .studio-projects-empty-text { color: hsla(215, 100, 65, .75); - width: 325px; + max-width: 325px; text-align: center; + line-height: 1.5rem; margin-top: 1rem; } + + .studio-projects-spinner { + margin: auto; + } } .studio-tile-added { diff --git a/src/views/studio/studio-activity.jsx b/src/views/studio/studio-activity.jsx index 519dbb2d5..ef65c406c 100644 --- a/src/views/studio/studio-activity.jsx +++ b/src/views/studio/studio-activity.jsx @@ -6,7 +6,6 @@ import {connect} from 'react-redux'; import {activity} from './lib/redux-modules'; import {loadActivity} from './lib/studio-activity-actions'; -import Debug from './debug.jsx'; import classNames from 'classnames'; import SocialMessage from '../../components/social-message/social-message.jsx'; @@ -181,10 +180,15 @@ const StudioActivity = ({items, loading, error, moreToLoad, onLoadMore}) => {

{loading &&
Loading...
} - {error && } + {error &&
+

+ +
}
    diff --git a/src/views/studio/studio-admin-panel.jsx b/src/views/studio/studio-admin-panel.jsx index 817d9befd..f97ef18a3 100644 --- a/src/views/studio/studio-admin-panel.jsx +++ b/src/views/studio/studio-admin-panel.jsx @@ -48,8 +48,11 @@ const StudioAdminPanel = ({studioId, showAdminPanel}) => { const [adminPanelOpen, setAdminPanelOpen] = useState(getAdminPanelOpen()); useEffect(() => { - storeAdminPanelOpen(adminPanelOpen); - }, [adminPanelOpen]); + // This effect will both keep localStorage up-to-date AND cause + // the spacing to change to allow for the open admin panel, so make + // sure the admin panel should be visible at all before making any changes. + if (showAdminPanel) storeAdminPanelOpen(adminPanelOpen); + }, [showAdminPanel, adminPanelOpen]); useEffect(() => { if (!showAdminPanel) return; diff --git a/src/views/studio/studio-comment.js b/src/views/studio/studio-comment.js new file mode 100644 index 000000000..8cb18978e --- /dev/null +++ b/src/views/studio/studio-comment.js @@ -0,0 +1,17 @@ +import {connect} from 'react-redux'; +import Comment from '../preview/comment/comment.jsx'; + +import { + selectCanDeleteComment, + selectCanReportComment, + selectShowCommentComposer +} from '../../redux/studio-permissions'; +import {selectStudioCommentsAllowed} from '../../redux/studio.js'; + +export default connect( + (state, ownProps) => ({ + canReport: selectCanReportComment(state, ownProps.author.username), + canDelete: selectCanDeleteComment(state, ownProps.author.username), + canReply: selectShowCommentComposer(state) && selectStudioCommentsAllowed(state) + }) +)(Comment); diff --git a/src/views/studio/studio-comments.jsx b/src/views/studio/studio-comments.jsx index 22cda114c..b0654e0d3 100644 --- a/src/views/studio/studio-comments.jsx +++ b/src/views/studio/studio-comments.jsx @@ -4,22 +4,23 @@ import {connect} from 'react-redux'; import {FormattedMessage} from 'react-intl'; import Button from '../../components/forms/button.jsx'; +import CommentingStatus from '../../components/commenting-status/commenting-status.jsx'; 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, selectHasFetchedSession} from '../../redux/session'; import { selectShowCommentComposer, - selectCanDeleteComment, selectCanDeleteCommentWithoutConfirm, - selectCanReportComment, selectCanRestoreComment, - selectCanEditCommentsAllowed + selectCanEditCommentsAllowed, + selectShowCommentsList, + selectShowCommentsGloballyOffError } from '../../redux/studio-permissions'; import {selectStudioCommentsAllowed} from '../../redux/studio.js'; +import StudioComment from './studio-comment.js'; const StudioComments = ({ comments, @@ -32,20 +33,38 @@ const StudioComments = ({ replies, postURI, shouldShowCommentComposer, - canDeleteComment, + shouldShowCommentsList, + shouldShowCommentsGloballyOffError, canDeleteCommentWithoutConfirm, canEditCommentsAllowed, - canReportComment, canRestoreComment, handleDeleteComment, handleRestoreComment, handleResetComments, handleReportComment, - handleLoadMoreReplies + handleLoadMoreReplies, + handleLoadSingleComment }) => { + const commentHashPrefix = '#comments-'; + const [singleCommentId, setSingleCommentId] = useState( + window.location.hash.indexOf(commentHashPrefix) !== -1 && + parseInt(window.location.hash.replace(commentHashPrefix, ''), 10)); + useEffect(() => { - if (comments.length === 0 && hasFetchedSession) handleLoadMoreComments(); - }, [comments.length === 0, hasFetchedSession]); + if (comments.length === 0 && hasFetchedSession) { + if (singleCommentId) { + handleLoadSingleComment(singleCommentId); + } else { + handleLoadMoreComments(); + } + } + }, [comments.length === 0, hasFetchedSession, singleCommentId]); + + const handleSeeAllComments = () => { + setSingleCommentId(false); + history.pushState('', document.title, window.location.pathname + window.location.search); + handleResetComments(); + }; // The comments you see depend on your admin status // so reset them if isAdmin changes. @@ -57,13 +76,13 @@ const StudioComments = ({ }, [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); }; @@ -74,54 +93,78 @@ const StudioComments = ({

    {canEditCommentsAllowed && } + {shouldShowCommentsGloballyOffError &&
    - {shouldShowCommentComposer ? - (commentsAllowed ? - +

    + +

    + + +
    + } + {shouldShowCommentsList && +
    + {shouldShowCommentComposer ? + (commentsAllowed ? + : + + ) : null + } + {comments.map(comment => ( + : - - ) : null - } - {comments.map(comment => ( - - ))} - {moreCommentsToLoad && + onDelete={handleDeleteComment} + onRestore={handleRestoreComment} + // eslint-disable-next-line react/jsx-no-bind + onReply={handleReplyStatusChange} + onReport={handleReportComment} + onLoadMoreReplies={handleLoadMoreReplies} + /> + ))} + {!!singleCommentId && - } -
    + } + {moreCommentsToLoad && + + } + + } ); }; @@ -136,16 +179,17 @@ StudioComments.propTypes = { moreCommentsToLoad: PropTypes.bool, replies: PropTypes.shape({}), shouldShowCommentComposer: PropTypes.bool, - canDeleteComment: PropTypes.bool, + shouldShowCommentsGloballyOffError: PropTypes.bool, + shouldShowCommentsList: PropTypes.bool, canDeleteCommentWithoutConfirm: PropTypes.bool, canEditCommentsAllowed: PropTypes.bool, - canReportComment: PropTypes.bool, canRestoreComment: PropTypes.bool, handleDeleteComment: PropTypes.func, handleRestoreComment: PropTypes.func, handleReportComment: PropTypes.func, handleResetComments: PropTypes.func, handleLoadMoreReplies: PropTypes.func, + handleLoadSingleComment: PropTypes.func, postURI: PropTypes.string }; @@ -162,15 +206,16 @@ export default connect( replies: state.comments.replies, commentsAllowed: selectStudioCommentsAllowed(state), shouldShowCommentComposer: selectShowCommentComposer(state), - canDeleteComment: selectCanDeleteComment(state), + shouldShowCommentsGloballyOffError: selectShowCommentsGloballyOffError(state), + shouldShowCommentsList: selectShowCommentsList(state), canDeleteCommentWithoutConfirm: selectCanDeleteCommentWithoutConfirm(state), canEditCommentsAllowed: selectCanEditCommentsAllowed(state), - canReportComment: selectCanReportComment(state), canRestoreComment: selectCanRestoreComment(state), postURI: `/proxy/comments/studio/${state.studio.id}` }), { handleLoadMoreComments: studioCommentActions.getTopLevelComments, + handleLoadSingleComment: studioCommentActions.getCommentById, handleNewComment: studioCommentActions.addNewComment, handleDeleteComment: studioCommentActions.deleteComment, handleRestoreComment: studioCommentActions.restoreComment, diff --git a/src/views/studio/studio-curators.jsx b/src/views/studio/studio-curators.jsx index 687a8e2c3..22df830fd 100644 --- a/src/views/studio/studio-curators.jsx +++ b/src/views/studio/studio-curators.jsx @@ -5,7 +5,6 @@ import {FormattedMessage} from 'react-intl'; import classNames from 'classnames'; 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 {loadCurators} from './lib/studio-member-actions'; @@ -28,10 +27,15 @@ const StudioCurators = ({

    {canInviteCurators && } - {error && } + {error &&
    +

    + +
    }
    {items.length === 0 && !loading ? (
    @@ -39,7 +43,7 @@ const StudioCurators = ({ width="179" height="111" className="studio-empty-img" - src="/images/studios/curators-empty.png" + src="/images/studios/curators-empty-image.svg" /> {canInviteCurators ? (
    diff --git a/src/views/studio/studio-deleted.jsx b/src/views/studio/studio-deleted.jsx new file mode 100644 index 000000000..96cada6ed --- /dev/null +++ b/src/views/studio/studio-deleted.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; +import {FormattedMessage} from 'react-intl'; +import {selectStudioPublic} from '../../redux/studio'; + +const StudioDeleted = ({deleted}) => { + if (!deleted) return null; + return (
    + +
    ); +}; + +StudioDeleted.propTypes = { + deleted: PropTypes.bool +}; + +export default connect( + state => ({ + deleted: selectStudioPublic(state) === false + }) +)(StudioDeleted); diff --git a/src/views/studio/studio-description.jsx b/src/views/studio/studio-description.jsx index 9538085ac..5aa00334f 100644 --- a/src/views/studio/studio-description.jsx +++ b/src/views/studio/studio-description.jsx @@ -12,6 +12,7 @@ import { Errors, mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError } from '../../redux/studio-mutations'; +import '../../components/forms/inplace-input.scss'; import ValidationMessage from '../../components/forms/validation-message.jsx'; import decorateText from '../../lib/decorate-text.jsx'; import StudioMuteEditMessage from './studio-mute-edit-message.jsx'; @@ -53,7 +54,7 @@ const StudioDescription = ({