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,
// 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

View file

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

View file

@ -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 = ({
<button
className="button"
type="submit"
disabled={passwordInputValue === ''}
disabled={passwordInputValue === '' || submitting || validationError}
>
<FormattedMessage id="studio.confirm" />
</button>
@ -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);

View file

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

View file

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

View file

@ -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}</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>
{(canRemove || canPromote || canTransferHost) &&
<OverflowMenu>
@ -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

View file

@ -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', () => {

View file

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