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,
|
||||
// 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
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -47,6 +47,7 @@ const TransferHostSelection = ({
|
|||
image={item.profile.images['90x90']}
|
||||
isCreator={false}
|
||||
selected={item.id === selectedId}
|
||||
className="transfer-host-tile"
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue