Adding user-messages module and implementing course-level messaging.

This commit is contained in:
David Joy
2020-01-15 13:59:05 -05:00
parent 2fba819c34
commit d36b5bd0b0
8 changed files with 177 additions and 25 deletions

View File

@@ -43,6 +43,7 @@
"@fortawesome/free-regular-svg-icons": "^5.12.0",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/react-fontawesome": "^0.1.8",
"classnames": "^2.2.6",
"core-js": "^3.6.2",
"prop-types": "^15.7.2",
"react": "^16.12.0",

View File

@@ -13,6 +13,7 @@ import Footer, { messages as footerMessages } from '@edx/frontend-component-foot
import appMessages from './i18n';
import CourseTabsNavigation from './components/CourseTabsNavigation';
import LearningSequencePage from './learning-sequence/LearningSequencePage';
import UserMessagesProvider from './user-messages/UserMessagesProvider';
import './index.scss';
import './assets/favicon.ico';
@@ -31,15 +32,17 @@ function courseLinks() {
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider>
<Header />
<div className="container pt-2">
<CourseTabsNavigation activeTabSlug="course" />
</div>
<Switch>
<Route exact path="/" render={courseLinks} />
<Route path="/course" component={LearningSequencePage} />
</Switch>
<Footer />
<UserMessagesProvider>
<Header />
<div className="container pt-2">
<CourseTabsNavigation activeTabSlug="course" />
</div>
<Switch>
<Route exact path="/" render={courseLinks} />
<Route path="/course" component={LearningSequencePage} />
</Switch>
<Footer />
</UserMessagesProvider>
</AppProvider>,
document.getElementById('root'),
);

View File

@@ -5,6 +5,7 @@ import { history } from '@edx/frontend-platform';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SequenceContainer from './SequenceContainer';
import { createSequenceIdList } from '../utils';
import AlertList from '../../user-messages/AlertList';
export default function Course({
courseUsageKey, courseId, sequenceId, unitId, models,
@@ -33,6 +34,7 @@ export default function Course({
return (
<main className="container-fluid d-flex flex-column flex-grow-1">
<AlertList topic="course" className="mt-3" />
<CourseBreadcrumbs
courseUsageKey={courseUsageKey}
courseId={courseId}

View File

@@ -118,29 +118,29 @@ function Sequence({
}
Sequence.propTypes = {
id: PropTypes.string.isRequired,
activeUnitId: PropTypes.string.isRequired,
bannerText: PropTypes.string,
courseUsageKey: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
intl: intlShape.isRequired,
isGated: PropTypes.bool.isRequired,
isTimeLimited: PropTypes.bool.isRequired,
onNavigateUnit: PropTypes.func,
onNext: PropTypes.func.isRequired,
onPrevious: PropTypes.func.isRequired,
savePosition: PropTypes.bool.isRequired,
showCompletion: PropTypes.bool.isRequired,
prerequisite: PropTypes.shape({
name: PropTypes.string,
id: PropTypes.string,
}).isRequired,
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
units: PropTypes.objectOf(PropTypes.shape({
id: PropTypes.string.isRequired,
complete: PropTypes.bool,
pageTitle: PropTypes.string.isRequired,
})).isRequired,
displayName: PropTypes.string.isRequired,
activeUnitId: PropTypes.string.isRequired,
showCompletion: PropTypes.bool.isRequired,
isTimeLimited: PropTypes.bool.isRequired,
bannerText: PropTypes.string,
onNext: PropTypes.func.isRequired,
onPrevious: PropTypes.func.isRequired,
onNavigateUnit: PropTypes.func,
isGated: PropTypes.bool.isRequired,
prerequisite: PropTypes.shape({
name: PropTypes.string,
id: PropTypes.string,
}).isRequired,
savePosition: PropTypes.bool.isRequired,
intl: intlShape.isRequired,
};
Sequence.defaultProps = {

View File

@@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { faExclamationTriangle, faInfoCircle, faCheckCircle, faMinusCircle, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button } from '@edx/paragon';
function getAlertClass(type) {
if (type === 'error') {
return 'alert-warning';
} else if (type === 'danger') {
return 'alert-danger';
} else if (type === 'success') {
return 'alert-success';
}
return 'alert-info';
}
function getAlertIcon(type) {
if (type === 'error') {
return faExclamationTriangle;
} else if (type === 'danger') {
return faMinusCircle;
} else if (type === 'success') {
return faCheckCircle;
}
return faInfoCircle;
}
function Alert({
type, dismissible, children, onDismiss,
}) {
return (
<div className={classNames('alert', { 'alert-dismissible': dismissible }, getAlertClass(type))}>
<div className="d-flex align-items-start">
<div className="mr-2">
<FontAwesomeIcon icon={getAlertIcon(type)} />
</div>
<div role="alert" className="flex-grow-1">
{children}
</div>
</div>
{dismissible && <Button className="close" onClick={onDismiss}><FontAwesomeIcon size="sm" icon={faTimes} /></Button>}
</div>
);
}
Alert.propTypes = {
type: PropTypes.oneOf(['error', 'danger', 'info', 'success']).isRequired,
dismissible: PropTypes.bool,
children: PropTypes.node,
onDismiss: PropTypes.func,
};
Alert.defaultProps = {
dismissible: false,
children: undefined,
onDismiss: null,
};
export default Alert;

View File

@@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import UserMessagesContext from './UserMessagesContext';
import Alert from './Alert';
export default function AlertList({ topic, className }) {
const { remove, messages } = useContext(UserMessagesContext);
return (
<div className={className}>
{messages.filter(message => !topic || message.topic === topic).map(message => (
<Alert
key={message.id}
type={message.type}
dismissible={message.dismissible}
onDismiss={() => remove(message.id)}
>
{message.text}
</Alert>
))}
</div>
);
}
AlertList.propTypes = {
className: PropTypes.string,
topic: PropTypes.string,
};
AlertList.defaultProps = {
topic: null,
className: null,
};

View File

@@ -0,0 +1,5 @@
import React from 'react';
const UserMessagesContext = React.createContext({});
export default UserMessagesContext;

View File

@@ -0,0 +1,44 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import UserMessagesContext from './UserMessagesContext';
export default function UserMessagesProvider({ children }) {
const [messages, setMessages] = useState([
{
code: null,
dismissible: true,
id: 0,
text: 'This is a course level message.',
type: 'info',
topic: 'course',
},
]);
const [nextId, setNextId] = useState(1);
const add = (message) => {
setMessages([...messages, { ...message, id: nextId }]);
setNextId(nextId + 1);
};
const remove = id => setMessages(messages.filter(message => message.id !== id));
const value = {
add,
remove,
messages,
};
return (
<UserMessagesContext.Provider value={value}>
{children}
</UserMessagesContext.Provider>
);
}
UserMessagesProvider.propTypes = {
children: PropTypes.node,
};
UserMessagesProvider.defaultProps = {
children: null,
};