mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 09:35:56 -05:00
Merge pull request #5467 from seotts/muted-users-cant-edit-studios
Prototype: muted users can't edit studios
This commit is contained in:
commit
60b154126d
14 changed files with 361 additions and 61 deletions
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
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)
|
||||
}),
|
||||
{
|
||||
|
|
34
src/views/studio/studio-mute-edit-message.jsx
Normal file
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
|
||||
|
|
|
@ -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)
|
||||
}),
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -50,6 +50,18 @@
|
|||
"social": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"user1Muted": {
|
||||
"session": {
|
||||
"user": {
|
||||
"id": 1,
|
||||
"username": "user1-username"
|
||||
},
|
||||
"permissions": {
|
||||
"mute_status": {"muteExpiresAt": 32515480478, "offenses": [], "showWarning": false},
|
||||
"social": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue