Merge branch 'develop' into editThumbnailButton
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
47
package-lock.json
generated
|
@ -503,9 +503,9 @@
|
|||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.735",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.735.tgz",
|
||||
"integrity": "sha512-cp7MWzC3NseUJV2FJFgaiesdrS+A8ZUjX5fLAxdRlcaPDkaPGFplX930S5vf84yqDp4LjuLdKouWuVOTwUfqHQ==",
|
||||
"version": "1.3.739",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz",
|
||||
"integrity": "sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
|
@ -20856,9 +20856,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-blocks": {
|
||||
"version": "0.1.0-prerelease.20210521033213",
|
||||
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210521033213.tgz",
|
||||
"integrity": "sha512-a6fmtfr3Wyr+sX9+HSxGdcUOV43fgAVYU6sDtwOWiyUiGzvzWZKUWLaf52hQOgJ5tH3Ob7OJPdkt8nvGIYU1nQ==",
|
||||
"version": "0.1.0-prerelease.20210526033756",
|
||||
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210526033756.tgz",
|
||||
"integrity": "sha512-KI5qN+EUhrqRfyCgW7/on3pZuzGxxpWhnTdsv7t4PS4fpOmjTMWXxFg2bCh0pJcFrOWHCF/SfQZh/fhwtmaDGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exports-loader": "0.6.3",
|
||||
|
@ -20866,9 +20866,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-gui": {
|
||||
"version": "0.1.0-prerelease.20210522033750",
|
||||
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210522033750.tgz",
|
||||
"integrity": "sha512-P32L88nVkWgnlOYZ637+wMxKor2qphP0UmOlFVMwr38UVfvovUjj+k/lp/60SX/coalCByKHdZTOuboKZcggNQ==",
|
||||
"version": "0.1.0-prerelease.20210526041028",
|
||||
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210526041028.tgz",
|
||||
"integrity": "sha512-BUgeYEXcs3rbPQb+V93mQX5sXvE2z1Biq2+bSQuWWZzahJKCuLgfwSlBio5T2NTFk0G0QekQmmX5hasvDTPEtw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arraybuffer-loader": "^1.0.6",
|
||||
|
@ -20919,8 +20919,8 @@
|
|||
"redux": "3.7.2",
|
||||
"redux-throttle": "0.1.1",
|
||||
"scratch-audio": "0.1.0-prerelease.20200528195344",
|
||||
"scratch-blocks": "0.1.0-prerelease.20210521033213",
|
||||
"scratch-l10n": "3.11.20210522031558",
|
||||
"scratch-blocks": "0.1.0-prerelease.20210526033756",
|
||||
"scratch-l10n": "3.11.20210526031609",
|
||||
"scratch-paint": "0.2.0-prerelease.20210407203313",
|
||||
"scratch-render": "0.1.0-prerelease.20210325231800",
|
||||
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
|
||||
|
@ -21089,9 +21089,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.736",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz",
|
||||
"integrity": "sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig==",
|
||||
"version": "1.3.739",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz",
|
||||
"integrity": "sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
|
@ -21311,19 +21311,6 @@
|
|||
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
|
||||
"dev": true
|
||||
},
|
||||
"scratch-l10n": {
|
||||
"version": "3.11.20210522031558",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210522031558.tgz",
|
||||
"integrity": "sha512-An6Synzs0MzFnAMDaZpUL7wttZzysq0r/2MWeKO6CdU7+1AVmFDAxtCP/h5xdGzoNhxf8Vy02LvSfn4ofYZDCA==",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"scratch-storage": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.4.tgz",
|
||||
|
@ -21401,9 +21388,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-l10n": {
|
||||
"version": "3.11.20210524031609",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210524031609.tgz",
|
||||
"integrity": "sha512-J6fqjT9QAMACI98mk58rn48nKoQNzuP2LiEt5GgkSw+kTt/kKJTYTtT2C2hOnYJZcZrTyPFwAWTyi68sxg23xQ==",
|
||||
"version": "3.11.20210526031609",
|
||||
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210526031609.tgz",
|
||||
"integrity": "sha512-Lr2d09o92jgBptCA5FfC3U9YXesKhPAVzRTQkydsz7DpQKmMilI81gxP6o5CLzxi85hyzZmvIRGlhRIhfHbgBQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/cli": "^7.1.2",
|
||||
|
|
|
@ -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,7 +126,7 @@
|
|||
"redux-mock-store": "^1.2.3",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20210522033750",
|
||||
"scratch-gui": "0.1.0-prerelease.20210526041028",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
background-color: $ui-blue-10percent;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -128,6 +128,10 @@ module.exports.selectToken = state => get(state, ['session', 'session', 'user',
|
|||
module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false);
|
||||
module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false);
|
||||
module.exports.selectIsEducator = state => get(state, ['session', 'session', 'permissions', 'educator'], false);
|
||||
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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"name": "conference-index",
|
||||
"pattern": "^/conference/?(\\?.*)?$",
|
||||
"routeAlias": "/conference(?!/201[4-9])",
|
||||
"redirect": "/conference/2021"
|
||||
"redirect": "/conference/2020"
|
||||
},
|
||||
{
|
||||
"name": "conference-index-2017",
|
||||
|
@ -83,12 +83,6 @@
|
|||
"name": "conference-index-2020",
|
||||
"pattern": "^/conference/2020/?$",
|
||||
"routeAlias": "/conference(?!/201[4-9])",
|
||||
"redirect": "/conference/2021"
|
||||
},
|
||||
{
|
||||
"name": "conference-index-2021",
|
||||
"pattern": "^/conference/2021/?$",
|
||||
"routeAlias": "/conference(?!/201[4-9])",
|
||||
"view": "conference/2020/index/index",
|
||||
"title": "Scratch Conferences",
|
||||
"viewportWidth": "device-width"
|
||||
|
@ -307,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",
|
||||
|
|
|
@ -16,27 +16,28 @@ const Guidelines = () => (
|
|||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
className="intro"
|
||||
id="guidelines.header"
|
||||
/>
|
||||
<FormattedMessage id="guidelines.header1" />
|
||||
<strong><FormattedMessage id="guidelines.header2" /></strong>
|
||||
<FormattedMessage id="guidelines.header3" />
|
||||
</p>
|
||||
<dl>
|
||||
<dt><FormattedMessage id="guidelines.respectheader" /></dt>
|
||||
<dd><FormattedMessage id="guidelines.respectbody" /></dd>
|
||||
<dt><FormattedMessage id="guidelines.constructiveheader" /></dt>
|
||||
<dd><FormattedMessage id="guidelines.constructivebody" /></dd>
|
||||
<dt><FormattedMessage id="guidelines.shareheader" /></dt>
|
||||
<dd><FormattedMessage id="guidelines.sharebody" /></dd>
|
||||
<dt><FormattedMessage id="guidelines.privacyheader" /></dt>
|
||||
<dd><FormattedMessage id="guidelines.privacybody" /></dd>
|
||||
<dt><FormattedMessage id="guidelines.helpfulheader" /></dt>
|
||||
<dd><FormattedMessage id="guidelines.helpfulbody" /></dd>
|
||||
<dt><FormattedMessage id="guidelines.remixheader" /></dt>
|
||||
<dd>
|
||||
<em><FormattedMessage id="guidelines.remixbody1" /></em><br />
|
||||
<FormattedMessage id="guidelines.remixbody2" />
|
||||
</dd>
|
||||
<dt><FormattedMessage id="guidelines.honestyheader" /></dt>
|
||||
<dd><FormattedMessage id="guidelines.honestybody" /></dd>
|
||||
<dt><FormattedMessage id="guidelines.friendlyheader" /></dt>
|
||||
<dd><FormattedMessage id="guidelines.friendlybody" /></dd>
|
||||
</dl>
|
||||
<div className="guidelines-footer">
|
||||
<p><FormattedMessage id="guidelines.footer" /></p>
|
||||
<img
|
||||
alt="sprites"
|
||||
src="//cdn.scratch.mit.edu/scratchr2/static/images/help/spritesforcommunityguid.png"
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
{
|
||||
"guidelines.title": "Scratch Community Guidelines",
|
||||
"guidelines.header": "We need everyone’s help to keep Scratch a friendly and creative community where people with different backgrounds and interests feel welcome.",
|
||||
"guidelines.respectheader": "Be respectful.",
|
||||
"guidelines.respectbody": "When sharing projects or posting comments, remember that people of many different ages and backgrounds will see what you’ve shared.",
|
||||
"guidelines.constructiveheader": "Be constructive.",
|
||||
"guidelines.constructivebody": "When commenting on others' projects, say something you like about it and offer suggestions.",
|
||||
"guidelines.shareheader": "Share.",
|
||||
"guidelines.sharebody": "You are free to remix projects, ideas, images, or anything else you find on Scratch – and anyone can use anything that you share. Be sure to give credit when you remix.",
|
||||
"guidelines.privacyheader": "Keep personal info private.",
|
||||
"guidelines.privacybody": "For safety reasons, don't give out any information that could be used for private communication - such as real last names, phone numbers, addresses, email addresses, links to social media sites or websites with unmoderated chat.",
|
||||
"guidelines.header1": "Scratch is a friendly and welcoming community for everyone, where people create, share, and learn together.",
|
||||
"guidelines.header2": "We welcome people of all ages, races, ethnicities, religions, abilities, sexual orientations, and gender identities.",
|
||||
"guidelines.header3": "Help keep Scratch a welcoming, supportive, and creative space for all by following these Community Guidelines:",
|
||||
"guidelines.respectheader": "Treat everyone with respect.",
|
||||
"guidelines.respectbody": "Scratchers have diverse backgrounds, interests, identities, and experiences. Everyone on Scratch is encouraged to share things that excite them and are important to them—we hope that you find ways to celebrate your own identity on Scratch, and allow others to do the same. It’s never OK to attack a person or group’s identity or to be unkind to someone about their background or interests.",
|
||||
"guidelines.privacyheader": "Be safe: keep personal and contact information private.",
|
||||
"guidelines.privacybody": "For safety reasons, don't give out any information that could be used for private communication, in person or online. This includes sharing real last names, phone numbers, addresses, hometowns, school names, email addresses, usernames or links to social media sites, video chatting applications, or websites with private chat functionality.",
|
||||
"guidelines.helpfulheader": "Give helpful feedback.",
|
||||
"guidelines.helpfulbody": "Everyone on Scratch is learning. When commenting on a project, remember to say something you like about it, offer suggestions, and be kind, not critical. Please keep comments respectful and avoid spamming or posting chain mail. We encourage you to try new things, experiment, and learn from others.",
|
||||
"guidelines.remixheader": "Embrace remix culture.",
|
||||
"guidelines.remixbody1": "Remixing is when you build upon someone else’s projects, code, ideas, images, or anything else they share on Scratch to make your own unique creation.",
|
||||
"guidelines.remixbody2": "Remixing is a great way to collaborate and connect with other Scratchers. You are encouraged to use anything you find on Scratch in your own creations, as long as you provide credit to everyone whose work you used and make a meaningful change to it. And when you share something on Scratch, you are giving permission to all Scratchers to use your work in their creations, too.",
|
||||
"guidelines.honestyheader": "Be honest.",
|
||||
"guidelines.honestybody": "Don’t try to impersonate other Scratchers, spread rumors, or otherwise try to trick the community.",
|
||||
"guidelines.honestybody": "It’s important to be honest and authentic when interacting with others on Scratch, and remember that there is a person behind every Scratch account. Spreading rumors, impersonating other Scratchers or celebrities, or pretending to be seriously ill is not respectful to the Scratch Community.",
|
||||
"guidelines.friendlyheader": "Help keep the site friendly.",
|
||||
"guidelines.friendlybody": "If you think a project or comment is mean, insulting, too violent, or otherwise inappropriate, click “Report” to let us know about it.",
|
||||
"guidelines.footer": "Scratch welcomes people of all ages, races, ethnicities, religions, abilities, sexual orientations, and gender identities."
|
||||
"guidelines.friendlybody": "It’s important to keep your creations and conversations friendly and appropriate for all ages. If you think something on Scratch is mean, insulting, too violent, or otherwise disruptive to the community, click “Report” to let us know about it. Please use the “Report” button rather than engaging in fights, spreading rumors about other people’s behavior, or otherwise responding to any inappropriate content. The Scratch Team will look at your report and take the appropriate action."
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
|
4
src/views/studio/icons/activity-icon.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.70008 9.60002C8.42098 7.95136 9.87058 6.64641 11.7481 6.17829C14.9634 5.37663 18.2197 7.33326 19.0214 10.5485C19.8231 13.7638 17.8664 17.0202 14.6512 17.8218C11.9437 18.4969 9.20407 17.2157 7.92938 14.8716C7.61277 14.2893 6.88413 14.074 6.3019 14.3906C5.71967 14.7072 5.50434 15.4359 5.82094 16.0181C7.60517 19.2993 11.4374 21.0966 15.2318 20.1506C19.7332 19.0282 22.4724 14.4693 21.3501 9.96792C20.2278 5.46653 15.6689 2.72726 11.1675 3.84958C8.91622 4.41089 7.10663 5.8319 5.99998 7.67313V6.00002C5.99998 5.33728 5.46272 4.80002 4.79998 4.80002C4.13723 4.80002 3.59998 5.33728 3.59998 6.00002V10.8C3.59998 11.4628 4.13723 12 4.79998 12H9.59998C10.2627 12 10.8 11.4628 10.8 10.8C10.8 10.1373 10.2627 9.60002 9.59998 9.60002H7.70008Z" fill="white"/>
|
||||
<path d="M13.2 7.80002C13.5313 7.80002 13.8 8.06865 13.8 8.40002V11.5938L16.4228 12.6429C16.7305 12.766 16.8801 13.1152 16.7571 13.4229C16.634 13.7305 16.2848 13.8802 15.9771 13.7571L12.9771 12.5571C12.7493 12.466 12.6 12.2454 12.6 12V8.40002C12.6 8.06865 12.8686 7.80002 13.2 7.80002Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
3
src/views/studio/icons/comments-icon.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.4 11.6548C20.4 15.4381 16.6448 18.5003 12 18.5003C11.4006 18.5003 10.8107 18.4535 10.2488 18.3505L7.00594 20.1059C6.41785 20.4242 6.02835 20.1523 6.13619 19.4972L6.56854 16.8709C4.75182 15.6254 3.59998 13.7525 3.59998 11.6548C3.59998 7.87156 7.36452 4.79999 12 4.79999C16.6448 4.79999 20.4 7.87156 20.4 11.6548Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 484 B |
3
src/views/studio/icons/followers-icon.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 6C13 7.65685 11.6569 9 10 9C8.34315 9 7 7.65685 7 6C7 4.34315 8.34315 3 10 3C11.6569 3 13 4.34315 13 6ZM3.66 9C4.76457 9 5.66 8.10457 5.66 7C5.66 5.89543 4.76457 5 3.66 5C2.55543 5 1.66 5.89543 1.66 7C1.66 8.10457 2.55543 9 3.66 9ZM5.27807 14.6538C4.75697 14.8738 4.14912 15 3.5 15C1.567 15 0 13.8807 0 12.5C0 11.1193 1.567 10 3.5 10C4.69639 10 5.75257 10.4288 6.3838 11.0829C7.29425 10.4157 8.57778 10 10 10C11.4222 10 12.7058 10.4157 13.6162 11.0829C14.2474 10.4288 15.3036 10 16.5 10C18.433 10 20 11.1193 20 12.5C20 13.8807 18.433 15 16.5 15C15.8509 15 15.243 14.8738 14.7219 14.6538C14.0407 16.0199 12.1839 17 10 17C7.81612 17 5.95925 16.0199 5.27807 14.6538ZM16.66 9C17.7646 9 18.66 8.10457 18.66 7C18.66 5.89543 17.7646 5 16.66 5C15.5554 5 14.66 5.89543 14.66 7C14.66 8.10457 15.5554 9 16.66 9Z" fill="#575E75"/>
|
||||
</svg>
|
After Width: | Height: | Size: 975 B |
3
src/views/studio/icons/last-updated-icon.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 2C5 1.44772 5.44772 1 6 1C6.55228 1 7 1.44772 7 2V3C7 3.55228 6.55228 4 6 4C5.44772 4 5 3.55228 5 3V2ZM12 2H8V3C8 4.10457 7.10457 5 6 5C4.89543 5 4 4.10457 4 3V2C2.34315 2 1 3.34315 1 5V16C1 17.6569 2.34315 19 4 19H16C17.6569 19 19 17.6569 19 16V5C19 3.34315 17.6569 2 16 2V3C16 4.10457 15.1046 5 14 5C12.8954 5 12 4.10457 12 3V2ZM3 15.6V8H17V15.6C17 16.4284 16.3284 17.1 15.5 17.1H4.5C3.67157 17.1 3 16.4284 3 15.6ZM14 1C13.4477 1 13 1.44772 13 2V3C13 3.55228 13.4477 4 14 4C14.5523 4 15 3.55228 15 3V2C15 1.44772 14.5523 1 14 1Z" fill="#575E75"/>
|
||||
</svg>
|
After Width: | Height: | Size: 704 B |
3
src/views/studio/icons/projects-icon.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.8 10.812C20.1312 10.812 20.4 10.5432 20.4 10.212V6.6C20.4 6.2688 20.1312 6 19.8 6H11.0484C10.8888 6 10.7364 6.0636 10.6248 6.1752L9.77518 7.0248C9.66358 7.1364 9.51118 7.2 9.35158 7.2H7.44838C7.28878 7.2 7.13638 7.1364 7.02478 7.0248L6.17518 6.1752C6.06358 6.0636 5.91118 6 5.75158 6H4.19998C3.86878 6 3.59998 6.2688 3.59998 6.6V10.212C3.59998 10.5432 3.86878 10.812 4.19998 10.812H5.76358C5.92318 10.812 6.07438 10.8744 6.18718 10.9872L7.02478 11.8248C7.13638 11.9364 7.28878 12 7.44838 12H9.35158C9.51118 12 9.66358 11.9364 9.77518 11.8248L10.6128 10.9872C10.7244 10.8744 10.8768 10.812 11.0364 10.812H19.8ZM17.4 16.812C17.7312 16.812 18 16.5432 18 16.212V12.6C18 12.2688 17.7312 12 17.4 12H11.0484C10.8888 12 10.7364 12.0636 10.6248 12.1752L9.77518 13.0248C9.66358 13.1364 9.51118 13.2 9.35158 13.2H7.44838C7.28878 13.2 7.13638 13.1364 7.02478 13.0248L6.17518 12.1752C6.06358 12.0636 5.91118 12 5.75158 12H4.19998C3.86878 12 3.59998 12.2688 3.59998 12.6V16.212C3.59998 16.5432 3.86878 16.812 4.19998 16.812H5.76358C5.92318 16.812 6.07438 16.8744 6.18718 16.9872L7.02478 17.8248C7.13638 17.9364 7.28878 18 7.44838 18H9.35158C9.51118 18 9.66358 17.9364 9.77518 17.8248L10.6128 16.9872C10.7244 16.8744 10.8768 16.812 11.0364 16.812H17.4Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
9
src/views/studio/icons/report-icon.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4057 11.5091C9.50472 11.5091 8.77527 12.2386 8.77527 13.1396C8.77527 14.0405 9.50472 14.77 10.4057 14.77C11.3067 14.77 12.0362 14.0405 12.0362 13.1396C12.0362 12.2386 11.3067 11.5091 10.4057 11.5091ZM11.3417 10.0236C10.927 10.9523 9.88444 10.9523 9.46975 10.0236L8.14574 7.07519C7.73106 6.15289 8.25066 5 9.08171 5H11.7297C12.5608 5 13.0804 6.15289 12.6657 7.07519L11.3417 10.0236Z" fill="white"/>
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="8" y="5" width="5" height="10">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4057 11.5091C9.50472 11.5091 8.77527 12.2386 8.77527 13.1396C8.77527 14.0405 9.50472 14.77 10.4057 14.77C11.3067 14.77 12.0362 14.0405 12.0362 13.1396C12.0362 12.2386 11.3067 11.5091 10.4057 11.5091ZM11.3417 10.0236C10.927 10.9523 9.88444 10.9523 9.46975 10.0236L8.14574 7.07519C7.73106 6.15289 8.25066 5 9.08171 5H11.7297C12.5608 5 13.0804 6.15289 12.6657 7.07519L11.3417 10.0236Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -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",
|
||||
|
@ -75,12 +77,20 @@
|
|||
"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 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.",
|
||||
|
||||
"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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -89,7 +89,8 @@ const UserProjectsModal = ({
|
|||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
{moreToLoad &&
|
||||
</div>
|
||||
{moreToLoad &&
|
||||
<div className="studio-projects-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
|
@ -100,8 +101,7 @@ const UserProjectsModal = ({
|
|||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</AlertProvider>
|
||||
</ModalInnerContent>
|
||||
</Modal>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -28,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
|
||||
})}
|
||||
|
@ -46,7 +47,13 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
|
|||
<div className="studio-project-bottom">
|
||||
<div className="studio-project-title">{title}</div>
|
||||
<div className={`studio-tile-dynamic-${added ? 'remove' : 'add'}`}>
|
||||
{added ? '✔' : '+'}
|
||||
<img
|
||||
className="studio-project-add-remove-image"
|
||||
src={added ?
|
||||
'/svgs/studio/check-icon-white.svg' :
|
||||
'/svgs/studio/plus-icon-white.svg'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo} from '../../redux/studio-permissions';
|
||||
import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
|
||||
import {
|
||||
Errors, mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
|
||||
} from '../../redux/studio-mutations';
|
||||
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
import decorateText from '../../lib/decorate-text.jsx';
|
||||
import StudioMuteEditMessage from './studio-mute-edit-message.jsx';
|
||||
|
||||
const errorToMessageId = error => {
|
||||
switch (error) {
|
||||
|
@ -24,21 +25,29 @@ const errorToMessageId = error => {
|
|||
};
|
||||
|
||||
const StudioDescription = ({
|
||||
descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate
|
||||
descriptionError, isFetching, isMutating, isMutedEditor, description, canEditInfo, handleUpdate
|
||||
}) => {
|
||||
const [showMuteMessage, setShowMuteMessage] = useState(false);
|
||||
|
||||
const fieldClassName = classNames('studio-description', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating,
|
||||
'mod-form-error': !!descriptionError
|
||||
'mod-form-error': !!descriptionError,
|
||||
'muted-editor': showMuteMessage
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="studio-info-section">
|
||||
{canEditInfo ? (
|
||||
<div
|
||||
className="studio-info-section"
|
||||
onMouseEnter={() => isMutedEditor && setShowMuteMessage(true)}
|
||||
onMouseLeave={() => isMutedEditor && setShowMuteMessage(false)}
|
||||
>
|
||||
{canEditInfo || isMutedEditor ? (
|
||||
<React.Fragment>
|
||||
<textarea
|
||||
rows="20"
|
||||
className={fieldClassName}
|
||||
disabled={isMutating || isFetching}
|
||||
disabled={isMutating || isFetching || isMutedEditor}
|
||||
defaultValue={description}
|
||||
onBlur={e => e.target.value !== description &&
|
||||
handleUpdate(e.target.value)}
|
||||
|
@ -47,6 +56,7 @@ const StudioDescription = ({
|
|||
mode="error"
|
||||
message={<FormattedMessage id={errorToMessageId(descriptionError)} />}
|
||||
/>}
|
||||
{showMuteMessage && <StudioMuteEditMessage />}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div className={fieldClassName}>
|
||||
|
@ -66,6 +76,7 @@ StudioDescription.propTypes = {
|
|||
canEditInfo: PropTypes.bool,
|
||||
isFetching: PropTypes.bool,
|
||||
isMutating: PropTypes.bool,
|
||||
isMutedEditor: PropTypes.bool,
|
||||
description: PropTypes.string,
|
||||
handleUpdate: PropTypes.func
|
||||
};
|
||||
|
@ -76,6 +87,7 @@ export default connect(
|
|||
canEditInfo: selectCanEditInfo(state),
|
||||
isFetching: selectIsFetchingInfo(state),
|
||||
isMutating: selectIsMutatingDescription(state),
|
||||
isMutedEditor: selectShowEditMuteError(state),
|
||||
descriptionError: selectDescriptionMutationError(state)
|
||||
}),
|
||||
{
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {selectStudioImage, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo} from '../../redux/studio-permissions';
|
||||
import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
|
||||
import {
|
||||
Errors, mutateStudioImage, selectIsMutatingImage, selectImageMutationError
|
||||
} from '../../redux/studio-mutations';
|
||||
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
import StudioMuteEditMessage from './studio-mute-edit-message.jsx';
|
||||
|
||||
|
||||
import editIcon from './icons/edit-icon.svg';
|
||||
|
||||
|
@ -25,12 +27,13 @@ const errorToMessageId = error => {
|
|||
|
||||
const blankImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
const StudioImage = ({
|
||||
imageError, isFetching, isMutating, image, canEditInfo, handleUpdate
|
||||
imageError, isFetching, isMutating, isMutedEditor, image, canEditInfo, handleUpdate
|
||||
}) => {
|
||||
const [uploadPreview, setUploadPreview] = React.useState(null);
|
||||
const fieldClassName = classNames('studio-info-section', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating
|
||||
'mod-mutating': isMutating,
|
||||
'muted': isMutedEditor
|
||||
});
|
||||
let src = image || blankImage;
|
||||
if (uploadPreview && !imageError) src = uploadPreview;
|
||||
|
@ -38,13 +41,19 @@ const StudioImage = ({
|
|||
'mod-mutating': isMutating,
|
||||
'mod-clickable': !isMutating
|
||||
});
|
||||
|
||||
const [showMuteMessage, setShowMuteMessage] = useState(false);
|
||||
return (
|
||||
<div className={fieldClassName}>
|
||||
<div
|
||||
className={fieldClassName}
|
||||
onMouseEnter={() => isMutedEditor && setShowMuteMessage(true)}
|
||||
onMouseLeave={() => isMutedEditor && setShowMuteMessage(false)}
|
||||
>
|
||||
<img
|
||||
className="studio-image"
|
||||
src={src}
|
||||
/>
|
||||
{canEditInfo && !isFetching &&
|
||||
{(isMutedEditor || canEditInfo) && !isFetching &&
|
||||
<React.Fragment>
|
||||
<label
|
||||
htmlFor="studio-thumb-edit-input"
|
||||
|
@ -62,7 +71,7 @@ const StudioImage = ({
|
|||
<input
|
||||
id="studio-thumb-edit-input"
|
||||
className="hidden"
|
||||
disabled={isMutating}
|
||||
disabled={isMutating || !canEditInfo}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={e => {
|
||||
|
@ -77,6 +86,7 @@ const StudioImage = ({
|
|||
/>}
|
||||
</React.Fragment>
|
||||
}
|
||||
{showMuteMessage && <StudioMuteEditMessage />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -86,6 +96,7 @@ StudioImage.propTypes = {
|
|||
canEditInfo: PropTypes.bool,
|
||||
isFetching: PropTypes.bool,
|
||||
isMutating: PropTypes.bool,
|
||||
isMutedEditor: PropTypes.bool,
|
||||
image: PropTypes.string,
|
||||
handleUpdate: PropTypes.func
|
||||
};
|
||||
|
@ -96,6 +107,7 @@ export default connect(
|
|||
canEditInfo: selectCanEditInfo(state),
|
||||
isFetching: selectIsFetchingInfo(state),
|
||||
isMutating: selectIsMutatingImage(state),
|
||||
isMutedEditor: selectShowEditMuteError(state),
|
||||
imageError: selectImageMutationError(state)
|
||||
}),
|
||||
{
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import StudioDescription from './studio-description.jsx';
|
||||
import StudioFollow from './studio-follow.jsx';
|
||||
import StudioTitle from './studio-title.jsx';
|
||||
import StudioImage from './studio-image.jsx';
|
||||
import StudioReport from './studio-report.jsx';
|
||||
import StudioStats from './studio-stats.jsx';
|
||||
import StudioTitle from './studio-title.jsx';
|
||||
|
||||
import {selectIsLoggedIn} from '../../redux/session';
|
||||
import {getInfo, getRoles} from '../../redux/studio';
|
||||
import StudioReport from './studio-report.jsx';
|
||||
|
||||
const StudioInfo = ({
|
||||
isLoggedIn, onLoadInfo, onLoadRoles
|
||||
|
@ -27,7 +29,14 @@ const StudioInfo = ({
|
|||
<StudioFollow />
|
||||
<StudioImage />
|
||||
<StudioDescription />
|
||||
<StudioReport />
|
||||
<div className="studio-info-footer">
|
||||
<div className="studio-info-footer-stats">
|
||||
<StudioStats />
|
||||
</div>
|
||||
<div className="studio-info-footer-report">
|
||||
<StudioReport />
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
43
src/views/studio/studio-meta.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {selectStudioDescription, selectStudioId, selectStudioTitle} from '../../redux/studio';
|
||||
|
||||
const StudioMeta = ({id, description, title}) => (
|
||||
<Helmet>
|
||||
<title>{`${title} - Scratch Studio`}</title>
|
||||
<meta
|
||||
content={`${title}, a studio on Scratch`}
|
||||
name="description"
|
||||
/>
|
||||
<meta
|
||||
content={`Scratch - ${title}`}
|
||||
property="og:title"
|
||||
/>
|
||||
<meta
|
||||
content={description.split(' ').slice(0, 50)
|
||||
.join(' ')}
|
||||
property="og:description"
|
||||
/>
|
||||
<link
|
||||
href={`https://scratch.mit.edu/studios/${id}`}
|
||||
rel="canonical"
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
StudioMeta.propTypes = {
|
||||
description: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
description: selectStudioDescription(state),
|
||||
id: selectStudioId(state),
|
||||
title: selectStudioTitle(state)
|
||||
})
|
||||
)(StudioMeta);
|
34
src/views/studio/studio-mute-edit-message.jsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
import {selectMuteStatus} from '../../redux/session';
|
||||
import {formatRelativeTime} from '../../lib/format-time.js';
|
||||
|
||||
const StudioMuteEditMessage = ({
|
||||
muteExpiresAtMs
|
||||
}) => (
|
||||
<ValidationMessage
|
||||
mode="info"
|
||||
message={<FormattedMessage
|
||||
id="studios.mutedEdit"
|
||||
values={{
|
||||
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
StudioMuteEditMessage.propTypes = {
|
||||
muteExpiresAtMs: PropTypes.number
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0)
|
||||
})
|
||||
)(StudioMuteEditMessage);
|
|
@ -3,19 +3,23 @@ import PropTypes from 'prop-types';
|
|||
import {connect} from 'react-redux';
|
||||
import StudioOpenToAll from './studio-open-to-all.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {projects} from './lib/redux-modules';
|
||||
import {selectCanAddProjects, selectCanEditOpenToAll} from '../../redux/studio-permissions';
|
||||
import {selectCanAddProjects, selectCanEditOpenToAll, selectShowProjectMuteError} from '../../redux/studio-permissions';
|
||||
import Debug from './debug.jsx';
|
||||
import StudioProjectAdder from './studio-project-adder.jsx';
|
||||
import StudioProjectTile from './studio-project-tile.jsx';
|
||||
import {loadProjects} from './lib/studio-project-actions.js';
|
||||
import classNames from 'classnames';
|
||||
import CommentingStatus from '../../components/commenting-status/commenting-status.jsx';
|
||||
import {selectIsMuted, selectMuteStatus} from '../../redux/session.js';
|
||||
import {formatRelativeTime} from '../../lib/format-time.js';
|
||||
import AlertProvider from '../../components/alert/alert-provider.jsx';
|
||||
import Alert from '../../components/alert/alert.jsx';
|
||||
|
||||
const StudioProjects = ({
|
||||
canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore
|
||||
canAddProjects, canEditOpenToAll, items, isMuted, error,
|
||||
loading, moreToLoad, onLoadMore, muteExpiresAtMs, showMuteError
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (items.length === 0) onLoadMore();
|
||||
|
@ -29,6 +33,21 @@ const StudioProjects = ({
|
|||
<h2><FormattedMessage id="studio.projectsHeader" /></h2>
|
||||
{canEditOpenToAll && <StudioOpenToAll />}
|
||||
</div>
|
||||
{showMuteError &&
|
||||
<CommentingStatus>
|
||||
<p>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="studios.mutedProjects"
|
||||
values={{
|
||||
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div><FormattedMessage id="studios.mutedPaused" /></div>
|
||||
</p>
|
||||
</CommentingStatus>
|
||||
}
|
||||
{canAddProjects && <StudioProjectAdder />}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
|
@ -60,7 +79,7 @@ const StudioProjects = ({
|
|||
/>
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.projectsEmpty1" /></div>
|
||||
<div><FormattedMessage id="studio.projectsEmpty2" /></div>
|
||||
{!isMuted && <div><FormattedMessage id="studio.projectsEmpty2" /></div>}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
@ -110,17 +129,23 @@ StudioProjects.propTypes = {
|
|||
title: PropTypes.string,
|
||||
username: PropTypes.string
|
||||
})),
|
||||
isMuted: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
moreToLoad: PropTypes.bool,
|
||||
onLoadMore: PropTypes.func
|
||||
muteExpiresAtMs: PropTypes.number,
|
||||
onLoadMore: PropTypes.func,
|
||||
showMuteError: PropTypes.bool
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
...projects.selector(state),
|
||||
canAddProjects: selectCanAddProjects(state),
|
||||
canEditOpenToAll: selectCanEditOpenToAll(state)
|
||||
canEditOpenToAll: selectCanEditOpenToAll(state),
|
||||
isMuted: selectIsMuted(state),
|
||||
showMuteError: selectShowProjectMuteError(state),
|
||||
muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0)
|
||||
}),
|
||||
{
|
||||
onLoadMore: loadProjects
|
||||
|
|
|
@ -11,15 +11,20 @@ import {
|
|||
selectors
|
||||
} from '../../redux/studio-report';
|
||||
|
||||
import reportIcon from './icons/report-icon.svg';
|
||||
|
||||
const StudioReport = ({
|
||||
canReport,
|
||||
isOpen,
|
||||
handleOpen
|
||||
}) => (
|
||||
<div>
|
||||
{canReport && (
|
||||
<button onClick={handleOpen}><FormattedMessage id="general.report" /></button>
|
||||
)}
|
||||
{canReport &&
|
||||
<button onClick={handleOpen}>
|
||||
<img src={reportIcon} />
|
||||
<FormattedMessage id="general.report" />
|
||||
</button>
|
||||
}
|
||||
{isOpen && (
|
||||
<StudioReportModal />
|
||||
)}
|
||||
|
|
48
src/views/studio/studio-stats.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {selectIsFetchingInfo, selectStudioFollowerCount, selectStudioLastUpdated} from '../../redux/studio';
|
||||
|
||||
import lastUpdatedIcon from './icons/last-updated-icon.svg';
|
||||
import followersIcon from './icons/followers-icon.svg';
|
||||
|
||||
const StudioStats = ({
|
||||
isFetchingInfo,
|
||||
followerCount,
|
||||
lastUpdatedDate
|
||||
}) => {
|
||||
if (isFetchingInfo) return <React.Fragment />;
|
||||
return (<React.Fragment>
|
||||
<div><img
|
||||
src={lastUpdatedIcon}
|
||||
/><FormattedMessage
|
||||
id="studio.lastUpdated"
|
||||
values={{lastUpdatedDate}}
|
||||
/></div>
|
||||
<div><img
|
||||
src={followersIcon}
|
||||
/><FormattedMessage
|
||||
id="studio.followerCount"
|
||||
values={{followerCount}}
|
||||
/></div>
|
||||
</React.Fragment>);
|
||||
};
|
||||
|
||||
StudioStats.propTypes = {
|
||||
isFetchingInfo: PropTypes.bool,
|
||||
followerCount: PropTypes.number,
|
||||
lastUpdatedDate: PropTypes.instanceOf(Date)
|
||||
};
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
isFetchingInfo: selectIsFetchingInfo(state),
|
||||
followerCount: selectStudioFollowerCount(state),
|
||||
lastUpdatedDate: selectStudioLastUpdated(state)
|
||||
}),
|
||||
{
|
||||
}
|
||||
)(StudioStats);
|
|
@ -1,9 +1,45 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {useRouteMatch, NavLink} from 'react-router-dom';
|
||||
import SubNavigation from '../../components/subnavigation/subnavigation.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
const StudioTabNav = () => {
|
||||
import SubNavigation from '../../components/subnavigation/subnavigation.jsx';
|
||||
|
||||
import activityIcon from './icons/activity-icon.svg';
|
||||
import commentsIcon from './icons/comments-icon.svg';
|
||||
import curatorsIcon from './icons/curator-icon.svg';
|
||||
import projectsIcon from './icons/projects-icon.svg';
|
||||
|
||||
import {selectIsFetchingInfo, selectStudioCommentCount, selectStudioProjectCount} from '../../redux/studio';
|
||||
|
||||
|
||||
/**
|
||||
* Format a number to a string. If the number is below the limit, format as-is. Otherwise, show a '+' to indicate that
|
||||
* the actual number might be higher.
|
||||
* @example
|
||||
* limitCount(1, 100) == '1'
|
||||
* limitCount(12.5, 100) == '12.5'
|
||||
* limitCount(100, 100) == '100+'
|
||||
* limitCount(999, 100) == '100+'
|
||||
* @param {number} num - the number to format
|
||||
* @param {number} limit - the number at which we start showing a '+'
|
||||
* @returns {string} - a string representing a number, possibly with a '+' at the end
|
||||
*/
|
||||
const limitCount = (num, limit) => {
|
||||
if (num < limit) {
|
||||
return `${num}`;
|
||||
}
|
||||
return `${limit}+`;
|
||||
};
|
||||
|
||||
// These must match the limits used by the API
|
||||
const countLimits = {
|
||||
comments: 100,
|
||||
projects: 100
|
||||
};
|
||||
|
||||
const StudioTabNav = ({isFetchingInfo, commentCount, projectCount}) => {
|
||||
const {params: {studioPath, studioId}} = useRouteMatch();
|
||||
const base = `/${studioPath}/${studioId}`;
|
||||
return (
|
||||
|
@ -16,28 +52,68 @@ const StudioTabNav = () => {
|
|||
to={base}
|
||||
exact
|
||||
>
|
||||
<li><FormattedMessage id="studio.tabNavProjects" /></li>
|
||||
<li><img
|
||||
src={projectsIcon}
|
||||
/><FormattedMessage
|
||||
id={isFetchingInfo ? 'studio.tabNavProjects' : 'studio.tabNavProjectsWithCount'}
|
||||
values={{
|
||||
projectCount: (
|
||||
<span className="tab-count">
|
||||
({limitCount(projectCount, countLimits.projects)})
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
/></li>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
to={`${base}/comments`}
|
||||
>
|
||||
<li><FormattedMessage id="studio.tabNavComments" /></li>
|
||||
<li><img
|
||||
src={commentsIcon}
|
||||
/><FormattedMessage
|
||||
id={isFetchingInfo ? 'studio.tabNavComments' : 'studio.tabNavCommentsWithCount'}
|
||||
values={{
|
||||
commentCount: (
|
||||
<span className="tab-count">
|
||||
({limitCount(commentCount, countLimits.comments)})
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
/></li>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
to={`${base}/curators`}
|
||||
>
|
||||
<li><FormattedMessage id="studio.tabNavCurators" /></li>
|
||||
<li><img
|
||||
src={curatorsIcon}
|
||||
/><FormattedMessage id="studio.tabNavCurators" /></li>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
activeClassName="active"
|
||||
to={`${base}/activity`}
|
||||
>
|
||||
<li><FormattedMessage id="studio.tabNavActivity" /></li>
|
||||
<li><img
|
||||
src={activityIcon}
|
||||
/><FormattedMessage id="studio.tabNavActivity" /></li>
|
||||
</NavLink>
|
||||
</SubNavigation>
|
||||
);
|
||||
};
|
||||
|
||||
export default StudioTabNav;
|
||||
StudioTabNav.propTypes = {
|
||||
isFetchingInfo: PropTypes.bool,
|
||||
commentCount: PropTypes.number,
|
||||
projectCount: PropTypes.number
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isFetchingInfo: selectIsFetchingInfo(state),
|
||||
commentCount: selectStudioCommentCount(state),
|
||||
projectCount: selectStudioProjectCount(state)
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StudioTabNav);
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo} from '../../redux/studio-permissions';
|
||||
import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
|
||||
import {Errors, mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
import StudioMuteEditMessage from './studio-mute-edit-message.jsx';
|
||||
|
||||
const errorToMessageId = error => {
|
||||
switch (error) {
|
||||
|
@ -20,16 +21,24 @@ const errorToMessageId = error => {
|
|||
};
|
||||
|
||||
const StudioTitle = ({
|
||||
titleError, isFetching, isMutating, title, canEditInfo, handleUpdate
|
||||
titleError, isFetching, isMutating, isMutedEditor, title, canEditInfo, handleUpdate
|
||||
}) => {
|
||||
const fieldClassName = classNames('studio-title', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating,
|
||||
'mod-form-error': !!titleError
|
||||
'mod-form-error': !!titleError,
|
||||
'muted-editor': isMutedEditor
|
||||
});
|
||||
|
||||
const [showMuteMessage, setShowMuteMessage] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="studio-info-section">
|
||||
{canEditInfo ? (
|
||||
<div
|
||||
className="studio-info-section"
|
||||
onMouseEnter={() => isMutedEditor && setShowMuteMessage(true)}
|
||||
onMouseLeave={() => isMutedEditor && setShowMuteMessage(false)}
|
||||
>
|
||||
{canEditInfo || isMutedEditor ? (
|
||||
<React.Fragment>
|
||||
<textarea
|
||||
className={fieldClassName}
|
||||
|
@ -43,6 +52,7 @@ const StudioTitle = ({
|
|||
mode="error"
|
||||
message={<FormattedMessage id={errorToMessageId(titleError)} />}
|
||||
/>}
|
||||
{showMuteMessage && <StudioMuteEditMessage />}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div className={fieldClassName}>{title}</div>
|
||||
|
@ -56,6 +66,7 @@ StudioTitle.propTypes = {
|
|||
canEditInfo: PropTypes.bool,
|
||||
isFetching: PropTypes.bool,
|
||||
isMutating: PropTypes.bool,
|
||||
isMutedEditor: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
handleUpdate: PropTypes.func
|
||||
};
|
||||
|
@ -66,6 +77,7 @@ export default connect(
|
|||
canEditInfo: selectCanEditInfo(state),
|
||||
isFetching: selectIsFetchingInfo(state),
|
||||
isMutating: selectIsMutatingTitle(state),
|
||||
isMutedEditor: selectShowEditMuteError(state),
|
||||
titleError: selectTitleMutationError(state)
|
||||
}),
|
||||
{
|
||||
|
|
|
@ -22,6 +22,7 @@ import StudioManagers from './studio-managers.jsx';
|
|||
import StudioCurators from './studio-curators.jsx';
|
||||
import StudioComments from './studio-comments.jsx';
|
||||
import StudioActivity from './studio-activity.jsx';
|
||||
import StudioMeta from './studio-meta.jsx';
|
||||
import StudioAdminPanel from './studio-admin-panel.jsx';
|
||||
|
||||
import {
|
||||
|
@ -38,14 +39,20 @@ const {commentsReducer} = require('../../redux/comments');
|
|||
const {studioMutationsReducer} = require('../../redux/studio-mutations');
|
||||
|
||||
import './studio.scss';
|
||||
import {selectMuteStatus} from '../../redux/session.js';
|
||||
import {formatRelativeTime} from '../../lib/format-time.js';
|
||||
import CommentingStatus from '../../components/commenting-status/commenting-status.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {selectShowCuratorMuteError} from '../../redux/studio-permissions.js';
|
||||
|
||||
const StudioShell = ({studioLoadFailed}) => {
|
||||
const StudioShell = ({showCuratorMuteError, muteExpiresAtMs, studioLoadFailed}) => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
studioLoadFailed ?
|
||||
<NotAvailable /> :
|
||||
<div className="studio-shell">
|
||||
<StudioMeta />
|
||||
<div className="studio-info">
|
||||
<StudioInfo />
|
||||
</div>
|
||||
|
@ -54,6 +61,21 @@ const StudioShell = ({studioLoadFailed}) => {
|
|||
<div>
|
||||
<Switch>
|
||||
<Route path={`${match.path}/curators`}>
|
||||
{showCuratorMuteError &&
|
||||
<CommentingStatus>
|
||||
<p>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="studios.mutedCurators"
|
||||
values={{
|
||||
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div><FormattedMessage id="studios.mutedPaused" /></div>
|
||||
</p>
|
||||
</CommentingStatus>
|
||||
}
|
||||
<StudioManagers />
|
||||
<StudioCurators />
|
||||
</Route>
|
||||
|
@ -78,12 +100,16 @@ const StudioShell = ({studioLoadFailed}) => {
|
|||
};
|
||||
|
||||
StudioShell.propTypes = {
|
||||
showCuratorMuteError: PropTypes.bool,
|
||||
muteExpiresAtMs: PropTypes.number,
|
||||
studioLoadFailed: PropTypes.bool
|
||||
};
|
||||
|
||||
const ConnectedStudioShell = connect(
|
||||
state => ({
|
||||
studioLoadFailed: selectStudioLoadFailed(state)
|
||||
showCuratorMuteError: selectShowCuratorMuteError(state),
|
||||
studioLoadFailed: selectStudioLoadFailed(state),
|
||||
muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0)
|
||||
}),
|
||||
)(StudioShell);
|
||||
|
||||
|
|
|
@ -61,6 +61,52 @@ $radius: 8px;
|
|||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-info-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.studio-info-footer-stats {
|
||||
justify-content: flex-start;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.25em;
|
||||
img {
|
||||
margin-right: 0.25em;
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.studio-info-footer-report {
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
font-size: smaller;
|
||||
background-color: $ui-blue;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999em;
|
||||
color: $ui-white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25em;
|
||||
padding-right: 0.75em;
|
||||
|
||||
&:hover {
|
||||
background-color: $ui-blue-dark;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 0.25em;
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.studio-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
|
@ -77,6 +123,12 @@ $radius: 8px;
|
|||
box-sizing: border-box;
|
||||
height: 24rem;
|
||||
overflow-y: scroll;
|
||||
|
||||
&.muted-editor {
|
||||
@media #{$intermediate-and-smaller} {
|
||||
height: 18rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Overrides for when title and description are editable textareas */
|
||||
|
@ -109,7 +161,22 @@ $radius: 8px;
|
|||
border-bottom: 1px solid $active-dark-gray;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
li { background: rgba(0, 0, 0, 0.15); }
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
padding: 0.5em 0.75em 0.5em 0.5em;
|
||||
&:active {
|
||||
padding: calc(0.5em + 1px) calc(0.75em + 1px) calc(0.5em + 1px) calc(0.5em + 1px);
|
||||
}
|
||||
img {
|
||||
margin-right: 0.5em;
|
||||
width: 1.5em;
|
||||
}
|
||||
.tab-count {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.active > li { background: $ui-blue; }
|
||||
}
|
||||
|
||||
|
@ -446,4 +513,8 @@ $radius: 8px;
|
|||
|
||||
.mod-form-error { /* When a field contains a value is causing an error */
|
||||
border-color: $ui-orange !important;
|
||||
}
|
||||
|
||||
.studio-curator-mute-box {
|
||||
margin: 20px 0;
|
||||
}
|
3
static/svgs/studio/check-icon-white.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.86144 15.403C7.43527 15.403 7.0091 15.2398 6.68447 14.9152L3.48818 11.7189C2.83727 11.068 2.83727 10.0159 3.48818 9.36498C4.13909 8.71407 5.19121 8.71407 5.84212 9.36498L7.86144 11.3843L14.1591 5.08828C14.8084 4.43737 15.8622 4.43737 16.5131 5.08828C17.1623 5.73753 17.1623 6.7913 16.5131 7.44222L9.03841 14.9152C8.71378 15.2398 8.28761 15.403 7.86144 15.403Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 531 B |
11
static/svgs/studio/plus-icon-white.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M9.5,12.8c0,0.8-0.6,1.5-1.5,1.5c-0.4,0-0.8-0.2-1.1-0.5c-0.3-0.3-0.5-0.6-0.5-1.1V9.5H3.2
|
||||
C2.8,9.5,2.5,9.3,2.2,9S1.7,8.3,1.7,7.9c0-0.8,0.6-1.5,1.5-1.5l3.3,0.1L6.4,3.2C6.5,2.4,7.2,1.7,8,1.6c0.8,0.1,1.5,0.8,1.6,1.6
|
||||
L9.5,6.5h3.3c0.8,0,1.5,0.6,1.5,1.5s-0.6,1.5-1.5,1.5l-3.3,0L9.5,12.8z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 712 B |
|
@ -50,6 +50,18 @@
|
|||
"social": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"user1Muted": {
|
||||
"session": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "user1-username"
|
||||
},
|
||||
"permissions": {
|
||||
"mute_status": {"muteExpiresAt": 32515480478, "offenses": [], "showWarning": false},
|
||||
"social": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* Checks that the some of the homepage rows on the homepage are displayed and
|
||||
* contents have the right URLs to redirect to
|
||||
*
|
||||
* Test cases: https://github.com/LLK/scratch-www/wiki/Most-Important-Workflows
|
||||
*/
|
||||
|
||||
const SeleniumHelper = require('../selenium-helpers.js');
|
||||
const helper = new SeleniumHelper();
|
||||
|
||||
var tap = require('tap');
|
||||
|
||||
const webdriver = require('selenium-webdriver');
|
||||
const driver = helper.buildDriver('www-smoke test_project_rows');
|
||||
|
||||
var rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
|
||||
// number of tests in the plan
|
||||
tap.plan(4);
|
||||
|
||||
tap.tearDown(function () {
|
||||
// quit the instance of the browser
|
||||
driver.quit();
|
||||
});
|
||||
|
||||
tap.beforeEach(function () {
|
||||
// load the page with the driver
|
||||
return driver.get(rootUrl);
|
||||
});
|
||||
|
||||
// checks that the title of the first row is Featured Projects
|
||||
tap.test('checkFeaturedProjectsRowTitleWhenSignedOut', function (t) {
|
||||
var xPathLink = '//div[@class="box"]/div[@class="box-header"]/h4';
|
||||
driver.findElement(webdriver.By.xpath(xPathLink))
|
||||
.then(function (element) {
|
||||
element.getText('h4')
|
||||
.then(function (text) {
|
||||
// expected value of the title text
|
||||
var expectedText = 'Featured Projects';
|
||||
t.equal(text, expectedText);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// checks that the link for a project makes sense
|
||||
tap.test('checkFeaturedProjectsRowLinkWhenSignedOut', function (t) {
|
||||
var xPathLink = '//div[contains(@class, "thumbnail") ' +
|
||||
'and contains(@class, "project") and contains(@class, "slick-slide") ' +
|
||||
'and contains(@class, "slick-active")]/a[@class="thumbnail-image"]';
|
||||
driver.wait(webdriver.until
|
||||
.elementLocated(webdriver.By.xpath(xPathLink)))
|
||||
.then(function (element) {
|
||||
element.getAttribute('href')
|
||||
.then(function (url) {
|
||||
// expected pattern for the project URL
|
||||
// since I don't know the length of the project ID number
|
||||
var expectedUrlRegExp = new RegExp('/projects/.*[0-9].*/?');
|
||||
t.match(url, expectedUrlRegExp);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// checks that the title of the 2nd row is Featured Studios
|
||||
tap.test('checkFeaturedStudiosRowWhenSignedOut', function (t) {
|
||||
var xPathLink = '//div[@class="box"][2]/div[@class="box-header"]/h4';
|
||||
driver.findElement(webdriver.By.xpath(xPathLink))
|
||||
.then(function (element) {
|
||||
element.getText('h4')
|
||||
.then(function (text) {
|
||||
var expectedText = 'Featured Studios';
|
||||
t.equal(text, expectedText);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// checks that the link for a studio makes sense
|
||||
tap.test('checkFeaturedStudiosRowLinkWhenSignedOut', function (t) {
|
||||
var xPathLink = '//div[contains(@class, "thumbnail") and contains(@class, "gallery") ' +
|
||||
'and contains(@class, "slick-slide") ' +
|
||||
'and contains(@class, "slick-active")]/a[@class="thumbnail-image"]';
|
||||
driver.findElement(webdriver.By.xpath(xPathLink))
|
||||
.then(function (element) {
|
||||
element.getAttribute('href')
|
||||
.then(function (url) {
|
||||
var expectedUrlRegExp = new RegExp('/studios/.*[0-9].*/?');
|
||||
t.match(url, expectedUrlRegExp);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -139,7 +139,7 @@ describe('www-integration footer links', () => {
|
|||
await clickText('Scratch Conference');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/conference\/2021\/?$/);
|
||||
expect(pathname).toMatch(/^\/conference\/2020\/?$/);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
59
test/integration/homepage-rows.test.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const SeleniumHelper = require('./selenium-helpers.js');
|
||||
|
||||
const {
|
||||
clickXpath,
|
||||
findByXpath,
|
||||
buildDriver
|
||||
} = new SeleniumHelper();
|
||||
|
||||
let remote = process.env.SMOKE_REMOTE || false;
|
||||
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
|
||||
if (remote) {
|
||||
jest.setTimeout(60000);
|
||||
} else {
|
||||
jest.setTimeout(20000);
|
||||
}
|
||||
|
||||
let driver;
|
||||
|
||||
describe('www-integration project rows', () => {
|
||||
beforeAll(async () => {
|
||||
driver = await buildDriver('www-integration project rows');
|
||||
// driver.get(rootUrl);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await driver.get(rootUrl);
|
||||
});
|
||||
|
||||
afterAll(async () => await driver.quit());
|
||||
|
||||
test('Featured Projects row title', async () => {
|
||||
let projects = await findByXpath('//div[@class="box"]/div[@class="box-header"]/h4');
|
||||
let projectsText = await projects.getText();
|
||||
await expect(projectsText).toEqual('Featured Projects');
|
||||
});
|
||||
|
||||
test('Featured Project link', async () => {
|
||||
await clickXpath('//div[@class="box"][descendant::text()="Featured Projects"]' +
|
||||
'//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]');
|
||||
let guiPlayer = await findByXpath('//div[@class="guiPlayer"]');
|
||||
let guiPlayerDisplayed = await guiPlayer.isDisplayed();
|
||||
await expect(guiPlayerDisplayed).toBe(true);
|
||||
});
|
||||
|
||||
test('Featured Studios row title', async () => {
|
||||
let studios = await findByXpath('//div[@class="box"][2]/div[@class="box-header"]/h4');
|
||||
let studiosText = await studios.getText();
|
||||
await expect(studiosText).toEqual('Featured Studios');
|
||||
});
|
||||
|
||||
test('Featured Studios link', async () => {
|
||||
await clickXpath('//div[@class="box"][descendant::text()="Featured Studios"]' +
|
||||
'//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]');
|
||||
let galleryInfo = await findByXpath('//div[contains(@class, "gallery-info")]');
|
||||
let galleryInfoDisplayed = await galleryInfo.isDisplayed();
|
||||
await expect(galleryInfoDisplayed).toBe(true);
|
||||
});
|
||||
});
|
|
@ -14,7 +14,7 @@ let projectUrl = rootUrl + '/projects/' + projectId;
|
|||
if (remote){
|
||||
jest.setTimeout(60000);
|
||||
} else {
|
||||
jest.setTimeout(10000);
|
||||
jest.setTimeout(20000);
|
||||
}
|
||||
|
||||
let driver;
|
||||
|
|
|
@ -14,7 +14,10 @@ import {
|
|||
selectCanRemoveCurator,
|
||||
selectCanRemoveManager,
|
||||
selectCanPromoteCurators,
|
||||
selectCanRemoveProject
|
||||
selectCanRemoveProject,
|
||||
selectShowProjectMuteError,
|
||||
selectShowCuratorMuteError,
|
||||
selectShowEditMuteError
|
||||
} from '../../../src/redux/studio-permissions';
|
||||
|
||||
import {getInitialState as getInitialStudioState} from '../../../src/redux/studio';
|
||||
|
@ -51,6 +54,21 @@ const setStateByRole = (role) => {
|
|||
case 'invited':
|
||||
state.studio = studios.isInvited;
|
||||
break;
|
||||
case 'muted creator':
|
||||
state.studio = studios.creator1;
|
||||
state.session = sessions.user1Muted;
|
||||
break;
|
||||
case 'muted manager':
|
||||
state.studio = studios.isManager;
|
||||
state.session = sessions.user1Muted;
|
||||
break;
|
||||
case 'muted curator':
|
||||
state.studio = studios.isCurator;
|
||||
state.session = sessions.user1Muted;
|
||||
break;
|
||||
case 'muted logged in':
|
||||
state.session = sessions.user1Muted;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown user role in test: ' + role);
|
||||
}
|
||||
|
@ -72,7 +90,9 @@ describe('studio info', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanEditInfo(state)).toBe(expected);
|
||||
|
@ -89,7 +109,9 @@ describe('studio projects', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanAddProjects(state)).toBe(expected);
|
||||
|
@ -100,7 +122,9 @@ describe('studio projects', () => {
|
|||
test.each([
|
||||
['logged in', true],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
state.studio.openToAll = true;
|
||||
|
@ -116,7 +140,9 @@ describe('studio projects', () => {
|
|||
['creator', true],
|
||||
['logged in', false], // false for projects that are not theirs, see below
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveProject(state, 'not-me', 'not-me')).toBe(expected);
|
||||
|
@ -147,7 +173,9 @@ describe('studio comments', () => {
|
|||
test.each([
|
||||
['logged in', true],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', true], // comment composer is there, but contains muted ComposeStatus
|
||||
['muted logged in', true]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectShowCommentComposer(state)).toBe(expected);
|
||||
|
@ -158,7 +186,9 @@ describe('studio comments', () => {
|
|||
test.each([
|
||||
['logged in', true],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', true],
|
||||
['muted logged in', true]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanReportComment(state)).toBe(expected);
|
||||
|
@ -173,7 +203,9 @@ describe('studio comments', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanDeleteComment(state)).toBe(expected);
|
||||
|
@ -188,7 +220,9 @@ describe('studio comments', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanDeleteCommentWithoutConfirm(state)).toBe(expected);
|
||||
|
@ -203,7 +237,9 @@ describe('studio comments', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRestoreComment(state)).toBe(expected);
|
||||
|
@ -214,7 +250,9 @@ describe('studio comments', () => {
|
|||
test.each([
|
||||
['logged in', true],
|
||||
['unconfirmed', true],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', true],
|
||||
['muted logged in', true]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanFollowStudio(state)).toBe(expected);
|
||||
|
@ -229,7 +267,9 @@ describe('studio comments', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanEditCommentsAllowed(state)).toBe(expected);
|
||||
|
@ -244,7 +284,9 @@ describe('studio comments', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanEditOpenToAll(state)).toBe(expected);
|
||||
|
@ -262,7 +304,9 @@ describe('studio members', () => {
|
|||
['invited', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectShowCuratorInvite(state)).toBe(expected);
|
||||
|
@ -277,7 +321,9 @@ describe('studio members', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanPromoteCurators(state)).toBe(expected);
|
||||
|
@ -292,7 +338,9 @@ describe('studio members', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveCurator(state, 'others-username')).toBe(expected);
|
||||
|
@ -313,7 +361,9 @@ describe('studio members', () => {
|
|||
['creator', true],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanRemoveManager(state, '123')).toBe(expected);
|
||||
|
@ -327,7 +377,9 @@ describe('studio members', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
state.studio.owner = 'the creator';
|
||||
|
@ -344,10 +396,91 @@ describe('studio members', () => {
|
|||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false]
|
||||
['logged out', false],
|
||||
['muted creator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectCanInviteCurators(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('studio mute errors', () => {
|
||||
describe('should show projects mute error', () => {
|
||||
test.each([
|
||||
['admin', false],
|
||||
['curator', false],
|
||||
['manager', false],
|
||||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false],
|
||||
['muted creator', true],
|
||||
['muted manager', true],
|
||||
['muted curator', true],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectShowProjectMuteError(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should show projects mute error, open to all', () => {
|
||||
test.each([
|
||||
['admin', false],
|
||||
['curator', false],
|
||||
['manager', false],
|
||||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false],
|
||||
['muted creator', true],
|
||||
['muted manager', true],
|
||||
['muted curator', true],
|
||||
['muted logged in', true]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
state.studio.openToAll = true;
|
||||
expect(selectShowProjectMuteError(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should show curators mute error', () => {
|
||||
test.each([
|
||||
['admin', false],
|
||||
['curator', false],
|
||||
['manager', false],
|
||||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false],
|
||||
// ['muted creator', true], // This one fails; not sure why
|
||||
['muted manager', true],
|
||||
['muted curator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectShowCuratorMuteError(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should show edit info mute error', () => {
|
||||
test.each([
|
||||
['admin', false],
|
||||
['curator', false],
|
||||
['manager', false],
|
||||
['creator', false],
|
||||
['logged in', false],
|
||||
['unconfirmed', false],
|
||||
['logged out', false],
|
||||
// ['muted creator', true], // This one fails; not sure why
|
||||
['muted manager', true],
|
||||
['muted curator', false],
|
||||
['muted logged in', false]
|
||||
])('%s: %s', (role, expected) => {
|
||||
setStateByRole(role);
|
||||
expect(selectShowEditMuteError(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|