mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 23:57:55 -05:00
Merge pull request #5988 from ericrosenbaum/transfer-modal5
Transfer host modal updates
This commit is contained in:
commit
b1239e72fc
8 changed files with 43 additions and 11 deletions
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue