+
)}
diff --git a/src/instructor-toolbar/masquerade-widget/MasqueradeUserNameInput.jsx b/src/instructor-toolbar/masquerade-widget/MasqueradeUserNameInput.jsx
new file mode 100644
index 00000000..d9fc3e75
--- /dev/null
+++ b/src/instructor-toolbar/masquerade-widget/MasqueradeUserNameInput.jsx
@@ -0,0 +1,60 @@
+import React, {
+ Component,
+} from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Input } from '@edx/paragon';
+
+import messages from './messages';
+
+class MasqueradeUserNameInput extends Component {
+ onError(...args) {
+ return this.props.onError(...args);
+ }
+
+ onKeyPress(event) {
+ if (event.key === 'Enter') {
+ return this.onSubmit(event);
+ }
+ return true;
+ }
+
+ onSubmit(event) {
+ const payload = {
+ role: 'student',
+ user_name: event.target.value,
+ };
+ this.props.onSubmit(payload).then((data) => {
+ if (data && data.success) {
+ global.location.reload();
+ } else {
+ const error = (data && data.error) || '';
+ this.onError(error);
+ }
+ }).catch(() => {
+ const message = this.props.intl.formatMessage(messages['userName.error.generic']);
+ this.onError(message);
+ });
+ return true;
+ }
+
+ render() {
+ return (
+
this.onKeyPress(event)}
+ placeholder={this.props.intl.formatMessage(messages['userName.input.placeholder'])}
+ type="text"
+ />
+ );
+ }
+}
+MasqueradeUserNameInput.propTypes = {
+ intl: intlShape.isRequired,
+ onError: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+};
+export default injectIntl(MasqueradeUserNameInput);
diff --git a/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.jsx b/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.jsx
index 938e161b..c8f22bd9 100644
--- a/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.jsx
+++ b/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.jsx
@@ -2,10 +2,21 @@ import React, {
Component,
} from 'react';
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 MasqueradeUserNameInput from './MasqueradeUserNameInput';
import MasqueradeWidgetOption from './MasqueradeWidgetOption';
-import { getMasqueradeOptions } from './data/api';
+import {
+ getMasqueradeOptions,
+ postMasqueradeOptions,
+} from './data/api';
+import messages from './messages';
class MasqueradeWidget extends Component {
constructor(props) {
@@ -13,33 +24,100 @@ class MasqueradeWidget extends Component {
this.courseId = props.courseId;
this.state = {
options: [],
+ shouldShowUserNameInput: false,
};
}
componentDidMount() {
getMasqueradeOptions(this.courseId).then((data) => {
if (data.success) {
- const options = this.parseAvailableOptions(data);
- this.setState({
- options,
- });
+ this.onSuccess(data);
} else {
+ // This was explicitly denied by the backend;
+ // assume it's disabled/unavailable.
// eslint-disable-next-line no-console
- console.warn('Unable to get masquerade options', data);
+ this.onError('Unable to get masquerade options');
}
}).catch((response) => {
+ // There's not much we can do to recover;
+ // if we can't fetch masquerade options,
+ // assume it's disabled/unavailable.
// eslint-disable-next-line no-console
console.error('Unable to get masquerade options', response);
});
}
- parseAvailableOptions(payload) {
- const data = payload || {};
+ onError(message) {
+ if (message) {
+ this.errorAlertId = this.context.add({
+ text: message,
+ topic: 'course',
+ type: ALERT_TYPES.ERROR,
+ dismissible: false,
+ });
+ }
+ }
+
+ async onSubmit(payload) {
+ this.context.remove(this.errorAlertId);
+ const options = await postMasqueradeOptions(this.courseId, payload);
+ return options;
+ }
+
+ onSuccess(data) {
+ const options = this.parseAvailableOptions(data);
+ 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;
+ }
+
+ toggle(show) {
+ let shouldShow;
+ if (show === undefined) {
+ shouldShow = !this.state.shouldShowUserNameInput;
+ } else {
+ shouldShow = show;
+ }
+ this.setState({
+ shouldShowUserNameInput: shouldShow,
+ });
+ }
+
+ parseAvailableOptions(postData) {
+ const data = postData || {};
const active = data.active || {};
const available = data.available || [];
const options = available.map((group) => (
this.toggle(...args)}
+ onSubmit={(payload) => this.onSubmit(payload)}
/>
));
return options;
@@ -57,18 +138,34 @@ class MasqueradeWidget extends Component {
options,
} = this.state;
return (
-
-
- View this course as
-
-
- {options}
-
-
+ <>
+
+
+ View this course as
+
+
+ {options}
+
+
+ {this.state.shouldShowUserNameInput && (
+ this.onError(errorMessage)}
+ onSubmit={(payload) => this.onSubmit(payload)}
+ ref={(input) => { this.userNameInput = input; }}
+ />
+ )}
+ >
);
}
}
MasqueradeWidget.propTypes = {
courseId: PropTypes.string.isRequired,
+ intl: intlShape.isRequired,
};
-export default MasqueradeWidget;
+MasqueradeWidget.contextType = UserMessagesContext;
+export default injectIntl(MasqueradeWidget);
diff --git a/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.jsx b/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.jsx
index e57c76c1..5e4fa0a7 100644
--- a/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.jsx
+++ b/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.jsx
@@ -4,18 +4,28 @@ import React, {
import PropTypes from 'prop-types';
import { Dropdown } from '@edx/paragon';
-import { postMasqueradeOptions } from './data/api';
-
class MasqueradeWidgetOption extends Component {
- handleClick() {
+ onClick(event) {
+ // TODO: Remove this hack when we upgrade Paragon
+ // Note: The current version of Paragon does _not_ close dropdown components
+ // automatically (or easily programmatically) when you click on an item.
+ // We can simulate this behavior by programmatically clicking the
+ // toggle button on behalf of the user.
+ // The newest version of Paragon already contains this behavior,
+ // so we can remove this when we upgrade to that point.
+ event.target.parentNode.parentNode.click();
const {
- courseId,
groupId,
role,
userName,
userPartitionId,
+ userNameInputToggle,
} = this.props;
const payload = {};
+ if (userName || userName === '') {
+ userNameInputToggle(true);
+ return false;
+ }
if (role) {
payload.role = role;
}
@@ -23,21 +33,24 @@ class MasqueradeWidgetOption extends Component {
payload.group_id = parseInt(groupId, 10);
payload.user_partition_id = parseInt(userPartitionId, 10);
}
- if (userName) {
- payload.user_name = userName;
- }
- postMasqueradeOptions(courseId, payload).then(() => {
+ this.props.onSubmit(payload).then(() => {
global.location.reload();
});
+ return true;
}
isSelected() {
- const selected = this.props.selected || {};
- const isEqual = (
- selected.userPartitionId === (this.props.userPartitionId || null)
- && selected.groupId === (this.props.groupId || null)
- && selected.role === this.props.role
- );
+ /* eslint-disable arrow-body-style */
+ const isEqual = [
+ 'groupId',
+ 'role',
+ 'userName',
+ 'userPartitionId',
+ ].reduce((accumulator, currentValue) => {
+ return accumulator && (
+ this.props[currentValue] === this.props.selected[currentValue]
+ );
+ }, true);
return isEqual;
}
@@ -57,7 +70,7 @@ class MasqueradeWidgetOption extends Component {
this.handleClick(event)}
+ onClick={(event) => this.onClick(event)}
>
{groupName}
@@ -65,9 +78,9 @@ class MasqueradeWidgetOption extends Component {
}
}
MasqueradeWidgetOption.propTypes = {
- courseId: PropTypes.string.isRequired,
groupId: PropTypes.number,
groupName: PropTypes.string.isRequired,
+ onSubmit: PropTypes.func.isRequired,
role: PropTypes.string,
selected: PropTypes.shape({
courseKey: PropTypes.string.isRequired,
@@ -77,6 +90,7 @@ MasqueradeWidgetOption.propTypes = {
userPartitionId: PropTypes.number,
}),
userName: PropTypes.string,
+ userNameInputToggle: PropTypes.func.isRequired,
userPartitionId: PropTypes.number,
};
MasqueradeWidgetOption.defaultProps = {
diff --git a/src/instructor-toolbar/masquerade-widget/data/api.js b/src/instructor-toolbar/masquerade-widget/data/api.js
index e88d1f1e..ccc81a8a 100644
--- a/src/instructor-toolbar/masquerade-widget/data/api.js
+++ b/src/instructor-toolbar/masquerade-widget/data/api.js
@@ -7,8 +7,8 @@ export async function getMasqueradeOptions(courseId) {
return camelCaseObject(data);
}
-export async function postMasqueradeOptions(courseId, data) {
+export async function postMasqueradeOptions(courseId, payload) {
const url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`);
- const { response } = await getAuthenticatedHttpClient().post(url.href, data);
- return camelCaseObject(response);
+ const { data } = await getAuthenticatedHttpClient().post(url.href, payload);
+ return camelCaseObject(data);
}
diff --git a/src/instructor-toolbar/masquerade-widget/messages.js b/src/instructor-toolbar/masquerade-widget/messages.js
new file mode 100644
index 00000000..ca6358e7
--- /dev/null
+++ b/src/instructor-toolbar/masquerade-widget/messages.js
@@ -0,0 +1,36 @@
+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': {
+ 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': {
+ id: 'masquerade-widget.userName.input.placeholder',
+ defaultMessage: 'username or email',
+ description: 'Placeholder text to prompt for a user to masquerade as',
+ },
+});
+
+export default messages;