diff --git a/src/redux/studio-permissions.js b/src/redux/studio-permissions.js index 99b75f9d9..3895d044c 100644 --- a/src/redux/studio-permissions.js +++ b/src/redux/studio-permissions.js @@ -59,6 +59,7 @@ const selectCanTransfer = (state, managerId) => { // classroomId is loaded only for educator and admin users. Only educators can create class studios, // so educators and admins are the only users who otherwise would be able to transfer a class studio. if (state.studio.classroomId !== null) return false; + if (selectIsMuted(state)) return false; // Muted users cannot transfer studios. if (state.studio.managers > 1) { // If there is more than one manager, if (managerId === state.studio.owner) { // and the selected manager is the current owner/host, if (isHost(state)) return true; // Owner/host can transfer diff --git a/src/views/studio/lib/studio-member-actions.js b/src/views/studio/lib/studio-member-actions.js index f4fd6a54a..d35ac5368 100644 --- a/src/views/studio/lib/studio-member-actions.js +++ b/src/views/studio/lib/studio-member-actions.js @@ -10,6 +10,7 @@ const Errors = keyMirror({ NETWORK: null, SERVER: null, PERMISSION: null, + PASSWORD: null, DUPLICATE: null, USER_MUTED: null, UNKNOWN_USERNAME: null, @@ -27,6 +28,9 @@ const normalizeError = (err, body, res) => { return Errors.MANAGER_LIMIT; } if (res.statusCode === 403 && body.mute_status) return Errors.USER_MUTED; + if (res.statusCode === 401 && body.message === 'password incorrect') { + return Errors.PASSWORD; + } if (res.statusCode === 401 || res.statusCode === 403) return Errors.PERMISSION; if (res.statusCode === 404) return Errors.UNKNOWN_USERNAME; if (res.statusCode === 409) return Errors.CANNOT_BE_HOST; @@ -41,10 +45,10 @@ const normalizeError = (err, body, res) => { return null; }; -const loadManagers = () => ((dispatch, getState) => { +const loadManagers = (reloadAll = false) => ((dispatch, getState) => { const state = getState(); const studioId = selectStudioId(state); - const managerCount = managers.selector(state).items.length; + const managerCount = reloadAll ? 0 : managers.selector(state).items.length; const opts = { uri: `/studios/${studioId}/managers/`, params: {limit: PER_PAGE_LIMIT, offset: managerCount} @@ -52,6 +56,7 @@ const loadManagers = () => ((dispatch, getState) => { api(withAdmin(opts, state), (err, body, res) => { const error = normalizeError(err, body, res); if (error) return dispatch(managers.actions.error(error)); + if (reloadAll) dispatch(managers.actions.clear()); dispatch(managers.actions.append(body, body.length === PER_PAGE_LIMIT)); }); }); diff --git a/src/views/studio/modals/transfer-host-confirmation.jsx b/src/views/studio/modals/transfer-host-confirmation.jsx index 6c4a951f4..2fc55d9da 100644 --- a/src/views/studio/modals/transfer-host-confirmation.jsx +++ b/src/views/studio/modals/transfer-host-confirmation.jsx @@ -13,7 +13,7 @@ import ValidationMessage from '../../../components/forms/validation-message.jsx' import {managers} from '../lib/redux-modules'; import {useAlertContext} from '../../../components/alert/alert-context'; -import {Errors, transferHost} from '../lib/studio-member-actions'; +import {Errors, transferHost, loadManagers} from '../lib/studio-member-actions'; import './transfer-host-modal.scss'; @@ -21,6 +21,7 @@ const TransferHostConfirmation = ({ handleBack, handleClose, handleTransferHost, + handleLoadManagers, intl, items, hostId, @@ -31,6 +32,7 @@ const TransferHostConfirmation = ({ const newHostUsername = items.find(item => item.id === selectedId).username; const newHostImage = items.find(item => item.id === selectedId).profile.images['90x90']; const [passwordInputValue, setPasswordInputValue] = useState(''); + const [submitting, setSubmitting] = useState(false); const [validationError, setValidationError] = useState(null); const {errorAlert, successAlert} = useAlertContext(); @@ -43,9 +45,11 @@ const TransferHostConfirmation = ({ }; const handleSubmit = () => { + setSubmitting(true); handleTransferHost(passwordInputValue, newHostUsername, selectedId) .then(() => { handleClose(); + handleLoadManagers(true); // reload the list of managers, to get them in the correct order successAlert({ id: 'studio.alertTransfer', values: {name: newHostUsername} @@ -53,7 +57,8 @@ const TransferHostConfirmation = ({ }) .catch(e => { // For password errors, show validation alert without closing the modal - if (e === Errors.PERMISSION) { + if (e === Errors.PASSWORD) { + setSubmitting(false); setValidationError(e); return; } @@ -149,7 +154,7 @@ const TransferHostConfirmation = ({ @@ -173,6 +178,7 @@ TransferHostConfirmation.propTypes = { }) })), handleTransferHost: PropTypes.func, + handleLoadManagers: PropTypes.func, selectedId: PropTypes.number, hostId: PropTypes.number }; @@ -182,7 +188,8 @@ const connectedConfirmationStep = connect( hostId: state.studio.owner, ...managers.selector(state) }), { - handleTransferHost: transferHost + handleTransferHost: transferHost, + handleLoadManagers: loadManagers } )(TransferHostConfirmation); diff --git a/src/views/studio/modals/transfer-host-modal.scss b/src/views/studio/modals/transfer-host-modal.scss index 71c2bfbbd..f4d36a7c0 100644 --- a/src/views/studio/modals/transfer-host-modal.scss +++ b/src/views/studio/modals/transfer-host-modal.scss @@ -122,6 +122,10 @@ margin: auto 8px; } + .transfer-host-tile { + cursor: pointer; + } + .transfer-host-tile-selected { background: $ui-aqua; } @@ -146,6 +150,7 @@ display: flex; align-items: center; margin-bottom: 1rem; + height: 4rem; } .transfer-password-validation { diff --git a/src/views/studio/modals/transfer-host-selection.jsx b/src/views/studio/modals/transfer-host-selection.jsx index 3e20ab9b3..e0cdad659 100644 --- a/src/views/studio/modals/transfer-host-selection.jsx +++ b/src/views/studio/modals/transfer-host-selection.jsx @@ -47,6 +47,7 @@ const TransferHostSelection = ({ image={item.profile.images['90x90']} isCreator={false} selected={item.id === selectedId} + className="transfer-host-tile" />) )} {moreToLoad && diff --git a/src/views/studio/studio-member-tile.jsx b/src/views/studio/studio-member-tile.jsx index 2171b9042..6ab8fc75a 100644 --- a/src/views/studio/studio-member-tile.jsx +++ b/src/views/studio/studio-member-tile.jsx @@ -29,7 +29,7 @@ import removeIcon from './icons/remove-icon.svg'; import promoteIcon from './icons/curator-icon.svg'; const StudioMemberTile = ({ - canRemove, canPromote, onRemove, canTransferHost, onPromote, + canRemove, canPromote, onRemove, canTransferHost, onPromote, studioTransferLaunched, isCreator, hasReachedManagerLimit, // mapState props username, image // own props }) => { @@ -52,7 +52,12 @@ const StudioMemberTile = ({ href={userUrl} className="studio-member-name" >{username} - {isCreator &&
} + {isCreator && +
+ {studioTransferLaunched ? + : + } +
} {(canRemove || canPromote || canTransferHost) && @@ -149,7 +154,8 @@ StudioMemberTile.propTypes = { username: PropTypes.string, image: PropTypes.string, isCreator: PropTypes.bool, - hasReachedManagerLimit: PropTypes.bool + hasReachedManagerLimit: PropTypes.bool, + studioTransferLaunched: PropTypes.bool }; const ManagerTile = connect( @@ -158,7 +164,8 @@ const ManagerTile = connect( canPromote: false, canTransferHost: selectCanTransfer(state, ownProps.id) && selectStudioTransferLaunched(state), - isCreator: state.studio.owner === ownProps.id + isCreator: state.studio.owner === ownProps.id, + studioTransferLaunched: selectStudioTransferLaunched(state) }), { onRemove: removeManager diff --git a/test/unit/redux/studio-member-actions.test.js b/test/unit/redux/studio-member-actions.test.js index 84f8f1616..c5a3dea64 100644 --- a/test/unit/redux/studio-member-actions.test.js +++ b/test/unit/redux/studio-member-actions.test.js @@ -45,6 +45,12 @@ describe('loadManagers', () => { expect(api.mock.calls[1][0].params.offset).toBe(3); items = managers.selector(store.getState()).items; expect(items.length).toBe(6); + + // Reload the list + store.dispatch(loadManagers(true)); + expect(api.mock.calls[2][0].params.offset).toBe(0); + items = managers.selector(store.getState()).items; + expect(items.length).toBe(3); }); test('it correctly uses the admin route when possible', () => { diff --git a/test/unit/redux/studio-permissions.test.js b/test/unit/redux/studio-permissions.test.js index 994cdafb1..8753b953a 100644 --- a/test/unit/redux/studio-permissions.test.js +++ b/test/unit/redux/studio-permissions.test.js @@ -443,7 +443,7 @@ describe('studio members', () => { ['logged in', false], ['unconfirmed', false], ['logged out', false], - ['muted creator', true], // Muted users do not see the transfer UI + ['muted creator', false], ['muted logged in', false] ])('%s: %s', (role, expected) => { setStateByRole(role);