Merge pull request #5467 from seotts/muted-users-cant-edit-studios

Prototype: muted users can't edit studios
This commit is contained in:
Paul Kaplan 2021-05-26 11:01:47 -04:00 committed by GitHub
commit 60b154126d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 361 additions and 61 deletions

View file

@ -8,6 +8,7 @@
background-color: $ui-blue-10percent;
width: 100%;
text-align: center;
box-sizing: border-box;
p {
margin-bottom: 0;

View file

@ -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

View file

@ -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
};

View file

@ -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
});

View file

@ -84,6 +84,11 @@
"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",

View file

@ -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)
}),
{

View file

@ -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';
const errorToMessageId = error => {
switch (error) {
@ -23,25 +25,32 @@ 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;
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>
<input
disabled={isMutating}
disabled={isMutating || !canEditInfo}
type="file"
accept="image/*"
onChange={e => {
@ -56,6 +65,7 @@ const StudioImage = ({
/>}
</React.Fragment>
}
{showMuteMessage && <StudioMuteEditMessage />}
</div>
);
};
@ -65,6 +75,7 @@ StudioImage.propTypes = {
canEditInfo: PropTypes.bool,
isFetching: PropTypes.bool,
isMutating: PropTypes.bool,
isMutedEditor: PropTypes.bool,
image: PropTypes.string,
handleUpdate: PropTypes.func
};
@ -75,6 +86,7 @@ export default connect(
canEditInfo: selectCanEditInfo(state),
isFetching: selectIsFetchingInfo(state),
isMutating: selectIsMutatingImage(state),
isMutedEditor: selectShowEditMuteError(state),
imageError: selectImageMutationError(state)
}),
{

View 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);

View file

@ -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

View file

@ -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)
}),
{

View file

@ -38,8 +38,13 @@ 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 (
@ -54,6 +59,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 +98,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);

View file

@ -123,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 */
@ -491,3 +497,7 @@ $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;
}

View file

@ -50,6 +50,18 @@
"social": false
}
}
},
"user1Muted": {
"session": {
"user": {
"id": 1,
"username": "user1-username"
},
"permissions": {
"mute_status": {"muteExpiresAt": 32515480478, "offenses": [], "showWarning": false},
"social": true
}
}
}
}
}

View file

@ -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);
});
});
});