Merge pull request #5988 from ericrosenbaum/transfer-modal5

Transfer host modal updates
This commit is contained in:
Eric Rosenbaum 2021-08-27 11:05:17 -04:00 committed by GitHub
commit b1239e72fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 43 additions and 11 deletions

View file

@ -59,6 +59,7 @@ const selectCanTransfer = (state, managerId) => {
// classroomId is loaded only for educator and admin users. Only educators can create class studios, // 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. // 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 (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 (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 (managerId === state.studio.owner) { // and the selected manager is the current owner/host,
if (isHost(state)) return true; // Owner/host can transfer if (isHost(state)) return true; // Owner/host can transfer

View file

@ -10,6 +10,7 @@ const Errors = keyMirror({
NETWORK: null, NETWORK: null,
SERVER: null, SERVER: null,
PERMISSION: null, PERMISSION: null,
PASSWORD: null,
DUPLICATE: null, DUPLICATE: null,
USER_MUTED: null, USER_MUTED: null,
UNKNOWN_USERNAME: null, UNKNOWN_USERNAME: null,
@ -27,6 +28,9 @@ const normalizeError = (err, body, res) => {
return Errors.MANAGER_LIMIT; return Errors.MANAGER_LIMIT;
} }
if (res.statusCode === 403 && body.mute_status) return Errors.USER_MUTED; 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 === 401 || res.statusCode === 403) return Errors.PERMISSION;
if (res.statusCode === 404) return Errors.UNKNOWN_USERNAME; if (res.statusCode === 404) return Errors.UNKNOWN_USERNAME;
if (res.statusCode === 409) return Errors.CANNOT_BE_HOST; if (res.statusCode === 409) return Errors.CANNOT_BE_HOST;
@ -41,10 +45,10 @@ const normalizeError = (err, body, res) => {
return null; return null;
}; };
const loadManagers = () => ((dispatch, getState) => { const loadManagers = (reloadAll = false) => ((dispatch, getState) => {
const state = getState(); const state = getState();
const studioId = selectStudioId(state); const studioId = selectStudioId(state);
const managerCount = managers.selector(state).items.length; const managerCount = reloadAll ? 0 : managers.selector(state).items.length;
const opts = { const opts = {
uri: `/studios/${studioId}/managers/`, uri: `/studios/${studioId}/managers/`,
params: {limit: PER_PAGE_LIMIT, offset: managerCount} params: {limit: PER_PAGE_LIMIT, offset: managerCount}
@ -52,6 +56,7 @@ const loadManagers = () => ((dispatch, getState) => {
api(withAdmin(opts, state), (err, body, res) => { api(withAdmin(opts, state), (err, body, res) => {
const error = normalizeError(err, body, res); const error = normalizeError(err, body, res);
if (error) return dispatch(managers.actions.error(error)); if (error) return dispatch(managers.actions.error(error));
if (reloadAll) dispatch(managers.actions.clear());
dispatch(managers.actions.append(body, body.length === PER_PAGE_LIMIT)); dispatch(managers.actions.append(body, body.length === PER_PAGE_LIMIT));
}); });
}); });

View file

@ -13,7 +13,7 @@ import ValidationMessage from '../../../components/forms/validation-message.jsx'
import {managers} from '../lib/redux-modules'; import {managers} from '../lib/redux-modules';
import {useAlertContext} from '../../../components/alert/alert-context'; 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'; import './transfer-host-modal.scss';
@ -21,6 +21,7 @@ const TransferHostConfirmation = ({
handleBack, handleBack,
handleClose, handleClose,
handleTransferHost, handleTransferHost,
handleLoadManagers,
intl, intl,
items, items,
hostId, hostId,
@ -31,6 +32,7 @@ const TransferHostConfirmation = ({
const newHostUsername = items.find(item => item.id === selectedId).username; const newHostUsername = items.find(item => item.id === selectedId).username;
const newHostImage = items.find(item => item.id === selectedId).profile.images['90x90']; const newHostImage = items.find(item => item.id === selectedId).profile.images['90x90'];
const [passwordInputValue, setPasswordInputValue] = useState(''); const [passwordInputValue, setPasswordInputValue] = useState('');
const [submitting, setSubmitting] = useState(false);
const [validationError, setValidationError] = useState(null); const [validationError, setValidationError] = useState(null);
const {errorAlert, successAlert} = useAlertContext(); const {errorAlert, successAlert} = useAlertContext();
@ -43,9 +45,11 @@ const TransferHostConfirmation = ({
}; };
const handleSubmit = () => { const handleSubmit = () => {
setSubmitting(true);
handleTransferHost(passwordInputValue, newHostUsername, selectedId) handleTransferHost(passwordInputValue, newHostUsername, selectedId)
.then(() => { .then(() => {
handleClose(); handleClose();
handleLoadManagers(true); // reload the list of managers, to get them in the correct order
successAlert({ successAlert({
id: 'studio.alertTransfer', id: 'studio.alertTransfer',
values: {name: newHostUsername} values: {name: newHostUsername}
@ -53,7 +57,8 @@ const TransferHostConfirmation = ({
}) })
.catch(e => { .catch(e => {
// For password errors, show validation alert without closing the modal // For password errors, show validation alert without closing the modal
if (e === Errors.PERMISSION) { if (e === Errors.PASSWORD) {
setSubmitting(false);
setValidationError(e); setValidationError(e);
return; return;
} }
@ -149,7 +154,7 @@ const TransferHostConfirmation = ({
<button <button
className="button" className="button"
type="submit" type="submit"
disabled={passwordInputValue === ''} disabled={passwordInputValue === '' || submitting || validationError}
> >
<FormattedMessage id="studio.confirm" /> <FormattedMessage id="studio.confirm" />
</button> </button>
@ -173,6 +178,7 @@ TransferHostConfirmation.propTypes = {
}) })
})), })),
handleTransferHost: PropTypes.func, handleTransferHost: PropTypes.func,
handleLoadManagers: PropTypes.func,
selectedId: PropTypes.number, selectedId: PropTypes.number,
hostId: PropTypes.number hostId: PropTypes.number
}; };
@ -182,7 +188,8 @@ const connectedConfirmationStep = connect(
hostId: state.studio.owner, hostId: state.studio.owner,
...managers.selector(state) ...managers.selector(state)
}), { }), {
handleTransferHost: transferHost handleTransferHost: transferHost,
handleLoadManagers: loadManagers
} }
)(TransferHostConfirmation); )(TransferHostConfirmation);

View file

@ -122,6 +122,10 @@
margin: auto 8px; margin: auto 8px;
} }
.transfer-host-tile {
cursor: pointer;
}
.transfer-host-tile-selected { .transfer-host-tile-selected {
background: $ui-aqua; background: $ui-aqua;
} }
@ -146,6 +150,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 1rem; margin-bottom: 1rem;
height: 4rem;
} }
.transfer-password-validation { .transfer-password-validation {

View file

@ -47,6 +47,7 @@ const TransferHostSelection = ({
image={item.profile.images['90x90']} image={item.profile.images['90x90']}
isCreator={false} isCreator={false}
selected={item.id === selectedId} selected={item.id === selectedId}
className="transfer-host-tile"
/>) />)
)} )}
{moreToLoad && {moreToLoad &&

View file

@ -29,7 +29,7 @@ import removeIcon from './icons/remove-icon.svg';
import promoteIcon from './icons/curator-icon.svg'; import promoteIcon from './icons/curator-icon.svg';
const StudioMemberTile = ({ const StudioMemberTile = ({
canRemove, canPromote, onRemove, canTransferHost, onPromote, canRemove, canPromote, onRemove, canTransferHost, onPromote, studioTransferLaunched,
isCreator, hasReachedManagerLimit, // mapState props isCreator, hasReachedManagerLimit, // mapState props
username, image // own props username, image // own props
}) => { }) => {
@ -52,7 +52,12 @@ const StudioMemberTile = ({
href={userUrl} href={userUrl}
className="studio-member-name" className="studio-member-name"
>{username}</a> >{username}</a>
{isCreator && <div className="studio-member-role"><FormattedMessage id="studio.creatorRole" /></div>} {isCreator &&
<div className="studio-member-role">
{studioTransferLaunched ?
<FormattedMessage id="studio.hostRole" /> :
<FormattedMessage id="studio.creatorRole" />}
</div>}
</div> </div>
{(canRemove || canPromote || canTransferHost) && {(canRemove || canPromote || canTransferHost) &&
<OverflowMenu> <OverflowMenu>
@ -149,7 +154,8 @@ StudioMemberTile.propTypes = {
username: PropTypes.string, username: PropTypes.string,
image: PropTypes.string, image: PropTypes.string,
isCreator: PropTypes.bool, isCreator: PropTypes.bool,
hasReachedManagerLimit: PropTypes.bool hasReachedManagerLimit: PropTypes.bool,
studioTransferLaunched: PropTypes.bool
}; };
const ManagerTile = connect( const ManagerTile = connect(
@ -158,7 +164,8 @@ const ManagerTile = connect(
canPromote: false, canPromote: false,
canTransferHost: selectCanTransfer(state, ownProps.id) && canTransferHost: selectCanTransfer(state, ownProps.id) &&
selectStudioTransferLaunched(state), selectStudioTransferLaunched(state),
isCreator: state.studio.owner === ownProps.id isCreator: state.studio.owner === ownProps.id,
studioTransferLaunched: selectStudioTransferLaunched(state)
}), }),
{ {
onRemove: removeManager onRemove: removeManager

View file

@ -45,6 +45,12 @@ describe('loadManagers', () => {
expect(api.mock.calls[1][0].params.offset).toBe(3); expect(api.mock.calls[1][0].params.offset).toBe(3);
items = managers.selector(store.getState()).items; items = managers.selector(store.getState()).items;
expect(items.length).toBe(6); 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', () => { test('it correctly uses the admin route when possible', () => {

View file

@ -443,7 +443,7 @@ describe('studio members', () => {
['logged in', false], ['logged in', false],
['unconfirmed', false], ['unconfirmed', false],
['logged out', false], ['logged out', false],
['muted creator', true], // Muted users do not see the transfer UI ['muted creator', false],
['muted logged in', false] ['muted logged in', false]
])('%s: %s', (role, expected) => { ])('%s: %s', (role, expected) => {
setStateByRole(role); setStateByRole(role);