AA-305: Update Masquerade Widget UI (#188)

* AA-305: Update Masquerade Widget UI

This will start showing the user being masqueraded as in the Toolbar
and will show any error messages next to the Toolbar as well.

* Further masquerade widget fixes

Co-authored-by: Dillon Dumesnil <ddumesnil@edx.org>
This commit is contained in:
Michael Terry
2020-08-21 12:49:12 -04:00
committed by GitHub
parent 86d28136de
commit b65bd0ff44
4 changed files with 114 additions and 120 deletions

View File

@@ -1,8 +1,10 @@
import React from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { ALERT_TYPES } from '../generic/user-messages';
import Alert from '../generic/user-messages/Alert';
import MasqueradeWidget from './masquerade-widget';
function getInsightsUrl(courseId) {
@@ -48,28 +50,47 @@ export default function InstructorToolbar(props) {
return activeUnit ? activeUnit.lmsWebUrl : undefined;
});
const urlStudio = getStudioUrl(courseId, unitId);
const [masqueradeErrorMessage, showMasqueradeError] = useState(null);
return (
<div className="bg-primary text-light">
<div className="container-fluid py-3 d-md-flex justify-content-end align-items-center">
<div className="align-items-center flex-grow-1 d-md-flex mx-1 my-1">
<MasqueradeWidget courseId={courseId} />
<div>
<div className="bg-primary text-light">
<div className="container-fluid py-3 d-md-flex justify-content-end align-items-start">
<div className="align-items-center flex-grow-1 d-md-flex mx-1 my-1">
<MasqueradeWidget courseId={courseId} onError={showMasqueradeError} />
</div>
{(urlLms || urlStudio || urlInsights) && (
<>
<hr className="border-light" />
<span className="mr-2 mt-1 col-form-label">View course in:</span>
</>
)}
{urlLms && (
<span className="mx-1 my-1">
<a className="btn btn-outline-light" href={urlLms}>Existing experience</a>
</span>
)}
{urlStudio && (
<span className="mx-1 my-1">
<a className="btn btn-outline-light" href={urlStudio}>Studio</a>
</span>
)}
{urlInsights && (
<span className="mx-1 my-1">
<a className="btn btn-outline-light" href={urlInsights}>Insights</a>
</span>
)}
</div>
{urlLms && (
<div className="flex-shrink-0 mx-1 my-1">
<a className="btn d-block btn-outline-light" href={urlLms}>View in the existing experience</a>
</div>
)}
{urlStudio && (
<div className="flex-shrink-0 mx-1 my-1">
<a className="btn d-block btn-outline-light" href={urlStudio}>View in Studio</a>
</div>
)}
{urlInsights && (
<div className="flex-shrink-0 mx-1 my-1">
<a className="btn d-block btn-outline-light" href={urlInsights}>View in Insights</a>
</div>
)}
</div>
{masqueradeErrorMessage && (
<div className="container-fluid mt-3">
<Alert
type={ALERT_TYPES.ERROR}
dismissible={false}
>
{masqueradeErrorMessage}
</Alert>
</div>
)}
</div>
);
}

View File

@@ -32,22 +32,25 @@ class MasqueradeUserNameInput extends Component {
this.onError(error);
}
}).catch(() => {
const message = this.props.intl.formatMessage(messages['userName.error.generic']);
const message = this.props.intl.formatMessage(messages.genericError);
this.onError(message);
});
return true;
}
render() {
const {
intl,
onError,
onSubmit,
...rest
} = this.props;
return (
<Input
autoFocus
className="flex-shrink-1"
defaultValue=""
label={this.props.intl.formatMessage(messages['userName.input.label'])}
label={intl.formatMessage(messages.userNameLabel)}
onKeyPress={(event) => this.onKeyPress(event)}
placeholder={this.props.intl.formatMessage(messages['userName.input.placeholder'])}
type="text"
{...rest}
/>
);
}

View File

@@ -5,10 +5,7 @@ import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon';
import {
ALERT_TYPES,
UserMessagesContext,
} from '../../generic/user-messages';
import { UserMessagesContext } from '../../generic/user-messages';
import MasqueradeUserNameInput from './MasqueradeUserNameInput';
import MasqueradeWidgetOption from './MasqueradeWidgetOption';
@@ -23,8 +20,11 @@ class MasqueradeWidget extends Component {
super(props);
this.courseId = props.courseId;
this.state = {
autoFocus: false,
masquerade: 'Staff',
options: [],
shouldShowUserNameInput: false,
masqueradeUsername: null,
};
}
@@ -48,18 +48,11 @@ class MasqueradeWidget extends Component {
}
onError(message) {
if (message) {
this.errorAlertId = this.context.add({
text: message,
topic: 'course',
type: ALERT_TYPES.ERROR,
dismissible: false,
});
}
this.props.onError(message);
}
async onSubmit(payload) {
this.context.remove(this.errorAlertId);
this.clearError();
const options = await postMasqueradeOptions(this.courseId, payload);
return options;
}
@@ -69,47 +62,18 @@ class MasqueradeWidget extends Component {
this.setState({
options,
});
const active = data.active || {};
const message = this.getStatusMessage(active);
if (message) {
this.context.add({
text: message,
topic: 'course',
type: ALERT_TYPES.INFO,
dismissible: false,
});
}
}
getStatusMessage(active) {
const {
groupName,
} = active;
let message = '';
if (active.userName) {
message = this.props.intl.formatMessage(messages['status.userName'], {
userName: active.userName,
});
} else if (groupName) {
message = this.props.intl.formatMessage(messages['status.groupName'], {
groupName,
});
} else if (active.role === 'student') {
message = this.props.intl.formatMessage(messages['status.learner']);
}
return message;
clearError() {
this.props.onError('');
}
toggle(show) {
let shouldShow;
if (show === undefined) {
shouldShow = !this.state.shouldShowUserNameInput;
} else {
shouldShow = show;
}
this.setState({
shouldShowUserNameInput: shouldShow,
});
this.setState(prevState => ({
autoFocus: true,
masquerade: 'Specific Student...',
shouldShowUserNameInput: show === undefined ? !prevState.shouldShowUserNameInput : show,
}));
}
parseAvailableOptions(postData) {
@@ -125,47 +89,68 @@ class MasqueradeWidget extends Component {
selected={active}
userName={group.userName}
userPartitionId={group.userPartitionId}
userNameInput={this.userNameInput}
userNameInputToggle={(...args) => this.toggle(...args)}
onSubmit={(payload) => this.onSubmit(payload)}
/>
));
if (active.userName) {
this.setState({
autoFocus: false,
masquerade: 'Specific Student...',
masqueradeUsername: active.userName,
shouldShowUserNameInput: true,
});
} else if (active.groupName) {
this.setState({ masquerade: active.groupName });
} else if (active.role === 'student') {
this.setState({ masquerade: 'Learner' });
}
return options;
}
render() {
const {
autoFocus,
masquerade,
options,
shouldShowUserNameInput,
masqueradeUsername,
} = this.state;
const specificLearnerInputText = this.props.intl.formatMessage(messages.placeholder);
return (
<>
<Dropdown
className="flex-shrink-1 mx-1 my-1"
style={{ textAlign: 'center' }}
>
<Dropdown.Toggle variant="light">
View this course as
</Dropdown.Toggle>
<Dropdown.Menu>
{options}
</Dropdown.Menu>
</Dropdown>
{this.state.shouldShowUserNameInput && (
<MasqueradeUserNameInput
className="flex-shrink-0 mx-1 my-1"
label="test"
onError={(errorMessage) => this.onError(errorMessage)}
onSubmit={(payload) => this.onSubmit(payload)}
ref={(input) => { this.userNameInput = input; }}
/>
<div className="flex-grow-1">
<div className="row">
<span className="col-auto col-form-label pl-3">View this course as:</span>
<Dropdown className="flex-shrink-1 mx-1">
<Dropdown.Toggle variant="light">
{masquerade}
</Dropdown.Toggle>
<Dropdown.Menu>
{options}
</Dropdown.Menu>
</Dropdown>
</div>
{shouldShowUserNameInput && (
<div className="row mt-2">
<span className="col-auto col-form-label pl-3">{`${specificLearnerInputText}:`}</span>
<MasqueradeUserNameInput
id="masquerade-search"
className="col-4 form-control"
autoFocus={autoFocus}
defaultValue={masqueradeUsername}
onError={(errorMessage) => this.onError(errorMessage)}
onSubmit={(payload) => this.onSubmit(payload)}
/>
</div>
)}
</>
</div>
);
}
}
MasqueradeWidget.propTypes = {
courseId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
onError: PropTypes.func.isRequired,
};
MasqueradeWidget.contextType = UserMessagesContext;
export default injectIntl(MasqueradeWidget);

View File

@@ -1,36 +1,21 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'status.groupName': {
id: 'masquerade-widget.status.groupName',
defaultMessage: 'You are masquerading as a learner in the {groupName} group.',
description: 'Message when masquerading as a generic user in a specific track',
},
'status.learner': {
id: 'masquerade-widget.status.learner',
defaultMessage: 'You are masquerading as a learner.',
description: 'Message when masquerading as a specific user',
},
'status.userName': {
id: 'masquerade-widget.status.userName',
defaultMessage: 'You are masquerading as the following user: {userName}',
description: 'Message when masquerading as a specific user',
},
'userName.input.label': {
id: 'masquerade-widget.userName.input.label',
defaultMessage: 'Masquerade as this user',
description: 'Label for the masquerade user input',
},
'userName.error.generic': {
genericError: {
id: 'masquerade-widget.userName.error.generic',
defaultMessage: 'An error has occurred; please try again.',
description: 'Message shown after a general error when attempting to masquerade',
},
'userName.input.placeholder': {
placeholder: {
id: 'masquerade-widget.userName.input.placeholder',
defaultMessage: 'username or email',
defaultMessage: 'Username or email',
description: 'Placeholder text to prompt for a user to masquerade as',
},
userNameLabel: {
id: 'masquerade-widget.userName.input.label',
defaultMessage: 'Masquerade as this user',
description: 'Label for the masquerade user input',
},
});
export default messages;