feat: added implementation for new sidebar (#1260)
* feat: added implementation for new sidebar * test: fixed test cases * refactor: fixed naming convention and combine useeffects * refactor: improved sidebar UI and renamed sidebar flag * refactor: remove additional states * refactor: fixed UI and logic related issue * refactor: simplified condition * refactor: toggle sidebar action * refactor: fixed toggle issues * refactor: back arrow component * refactor: changed useeffect position --------- Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
This commit is contained in:
1
.env
1
.env
@@ -13,6 +13,7 @@ DISCOVERY_API_BASE_URL=''
|
||||
DISCUSSIONS_MFE_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NEW_SIDEBAR=''
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
||||
EXAMS_BASE_URL=''
|
||||
|
||||
@@ -13,6 +13,7 @@ DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NEW_SIDEBAR=''
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||
EXAMS_BASE_URL=''
|
||||
|
||||
@@ -13,6 +13,7 @@ DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NEW_SIDEBAR=''
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||
EXAMS_BASE_URL='http://localhost:18740'
|
||||
|
||||
@@ -15,6 +15,8 @@ import ContentTools from './content-tools';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
import SidebarProvider from './sidebar/SidebarContextProvider';
|
||||
import SidebarTriggers from './sidebar/SidebarTriggers';
|
||||
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
|
||||
import NewSidebarTriggers from './new-sidebar/SidebarTriggers';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
@@ -34,6 +36,7 @@ const Course = ({
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const section = useModel('sections', sequence ? sequence.sectionId : null);
|
||||
const enableNewSidebar = getConfig().ENABLE_NEW_SIDEBAR;
|
||||
|
||||
const pageTitleBreadCrumbs = [
|
||||
sequence,
|
||||
@@ -64,12 +67,14 @@ const Course = ({
|
||||
));
|
||||
}, [sequenceId]);
|
||||
|
||||
const SidebarProviderComponent = enableNewSidebar === 'true' ? NewSidebarProvider : SidebarProvider;
|
||||
|
||||
return (
|
||||
<SidebarProvider courseId={courseId} unitId={unitId}>
|
||||
<SidebarProviderComponent courseId={courseId} unitId={unitId}>
|
||||
<Helmet>
|
||||
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="position-relative d-flex align-items-start">
|
||||
<div className="position-relative d-flex align-items-center mb-4 mt-1">
|
||||
<CourseBreadcrumbs
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
@@ -87,7 +92,7 @@ const Course = ({
|
||||
contentToolsEnabled={course.showCalculator || course.notes.enabled}
|
||||
unitId={unitId}
|
||||
/>
|
||||
<SidebarTriggers />
|
||||
{enableNewSidebar === 'true' ? <NewSidebarTriggers /> : <SidebarTriggers /> }
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -113,7 +118,7 @@ const Course = ({
|
||||
onClose={() => setWeeklyGoalCelebrationOpen(false)}
|
||||
/>
|
||||
<ContentTools course={course} />
|
||||
</SidebarProvider>
|
||||
</SidebarProviderComponent>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ const CourseBreadcrumbs = ({
|
||||
}, [courseStatus, sequenceStatus, allSequencesInSections]);
|
||||
|
||||
return (
|
||||
<nav aria-label="breadcrumb" className="my-4 d-inline-block col-sm-10">
|
||||
<nav aria-label="breadcrumb" className="d-inline-block col-sm-10">
|
||||
<ol className="list-unstyled d-flex flex-nowrap align-items-center m-0">
|
||||
<li className="list-unstyled col-auto m-0 p-0">
|
||||
<Link
|
||||
|
||||
17
src/courseware/course/new-sidebar/Sidebar.jsx
Normal file
17
src/courseware/course/new-sidebar/Sidebar.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import SidebarContext from './SidebarContext';
|
||||
import { SIDEBARS } from './sidebars';
|
||||
|
||||
const Sidebar = () => {
|
||||
const { currentSidebar } = useContext(SidebarContext);
|
||||
|
||||
if (currentSidebar === null) { return null; }
|
||||
const SidebarToRender = SIDEBARS[currentSidebar].Sidebar;
|
||||
|
||||
return (
|
||||
<SidebarToRender />
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
5
src/courseware/course/new-sidebar/SidebarContext.js
Normal file
5
src/courseware/course/new-sidebar/SidebarContext.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const SidebarContext = React.createContext({});
|
||||
|
||||
export default SidebarContext;
|
||||
103
src/courseware/course/new-sidebar/SidebarContextProvider.jsx
Normal file
103
src/courseware/course/new-sidebar/SidebarContextProvider.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, {
|
||||
useCallback, useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
|
||||
import { getLocalStorage, setLocalStorage } from '../../../data/localStorage';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import WIDGETS from './constants';
|
||||
import SidebarContext from './SidebarContext';
|
||||
import { SIDEBARS } from './sidebars';
|
||||
|
||||
const SidebarProvider = ({
|
||||
courseId,
|
||||
unitId,
|
||||
children,
|
||||
}) => {
|
||||
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth;
|
||||
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth;
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const initialSidebar = (shouldDisplaySidebarOpen || query.get('sidebar') === 'true')
|
||||
? SIDEBARS.DISCUSSIONS_NOTIFICATIONS.ID : null;
|
||||
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);
|
||||
const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`));
|
||||
const [hideDiscussionbar, setHideDiscussionbar] = useState(false);
|
||||
const [hideNotificationbar, setHideNotificationbar] = useState(false);
|
||||
const [upgradeNotificationCurrentState, setUpgradeNotificationCurrentState] = useState(
|
||||
getLocalStorage(`upgradeNotificationCurrentState.${courseId}`),
|
||||
);
|
||||
const topic = useModel('discussionTopics', unitId);
|
||||
const { verifiedMode } = useModel('courseHomeMeta', courseId);
|
||||
const isDiscussionbarAvailable = topic?.id && topic?.enabledInContext;
|
||||
const isNotificationbarAvailable = !isEmpty(verifiedMode);
|
||||
|
||||
const onNotificationSeen = useCallback(() => {
|
||||
setNotificationStatus('inactive');
|
||||
setLocalStorage(`notificationStatus.${courseId}`, 'inactive');
|
||||
}, [courseId]);
|
||||
|
||||
useEffect(() => {
|
||||
setHideDiscussionbar(!isDiscussionbarAvailable);
|
||||
setHideNotificationbar(!isNotificationbarAvailable);
|
||||
setCurrentSidebar(SIDEBARS.DISCUSSIONS_NOTIFICATIONS.ID);
|
||||
}, [unitId, topic]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hideDiscussionbar && hideNotificationbar) {
|
||||
setCurrentSidebar(null);
|
||||
}
|
||||
}, [hideDiscussionbar, hideNotificationbar]);
|
||||
|
||||
const toggleSidebar = useCallback((sidebarId = null, widgetId = null) => {
|
||||
if (widgetId) {
|
||||
setHideDiscussionbar(prevWidgetId => (widgetId === WIDGETS.DISCUSSIONS ? true : prevWidgetId));
|
||||
setHideNotificationbar(prevWidgetId => (widgetId === WIDGETS.NOTIFICATIONS ? true : prevWidgetId));
|
||||
} else {
|
||||
setCurrentSidebar(prevSidebar => (sidebarId === prevSidebar ? null : sidebarId));
|
||||
setHideDiscussionbar(!isDiscussionbarAvailable);
|
||||
setHideNotificationbar(!isNotificationbarAvailable);
|
||||
}
|
||||
}, [isDiscussionbarAvailable, isNotificationbarAvailable]);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
toggleSidebar,
|
||||
onNotificationSeen,
|
||||
setNotificationStatus,
|
||||
currentSidebar,
|
||||
notificationStatus,
|
||||
upgradeNotificationCurrentState,
|
||||
setUpgradeNotificationCurrentState,
|
||||
shouldDisplaySidebarOpen,
|
||||
shouldDisplayFullScreen,
|
||||
courseId,
|
||||
unitId,
|
||||
hideDiscussionbar,
|
||||
hideNotificationbar,
|
||||
isNotificationbarAvailable,
|
||||
isDiscussionbarAvailable,
|
||||
}), [courseId, currentSidebar, notificationStatus, onNotificationSeen, shouldDisplayFullScreen,
|
||||
shouldDisplaySidebarOpen, toggleSidebar, unitId, upgradeNotificationCurrentState, hideDiscussionbar,
|
||||
hideNotificationbar, isNotificationbarAvailable, isDiscussionbarAvailable]);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
SidebarProvider.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
unitId: PropTypes.string.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
SidebarProvider.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
export default SidebarProvider;
|
||||
21
src/courseware/course/new-sidebar/SidebarTriggers.jsx
Normal file
21
src/courseware/course/new-sidebar/SidebarTriggers.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import SidebarContext from './SidebarContext';
|
||||
import { SIDEBAR_ORDER, SIDEBARS } from './sidebars';
|
||||
|
||||
const SidebarTriggers = () => {
|
||||
const { toggleSidebar } = useContext(SidebarContext);
|
||||
|
||||
return (
|
||||
<div className="d-flex ml-auto">
|
||||
{SIDEBAR_ORDER.map((sidebarId) => {
|
||||
const { Trigger } = SIDEBARS[sidebarId];
|
||||
return (
|
||||
<Trigger onClick={() => toggleSidebar(sidebarId)} key={sidebarId} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarTriggers;
|
||||
113
src/courseware/course/new-sidebar/common/SidebarBase.jsx
Normal file
113
src/courseware/course/new-sidebar/common/SidebarBase.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
import { ArrowBackIos, Close } from '@edx/paragon/icons';
|
||||
|
||||
import { useEventListener } from '../../../../generic/hooks';
|
||||
import WIDGETS from '../constants';
|
||||
import messages from '../messages';
|
||||
import SidebarContext from '../SidebarContext';
|
||||
|
||||
const SidebarBase = ({
|
||||
title,
|
||||
ariaLabel,
|
||||
sidebarId,
|
||||
className,
|
||||
children,
|
||||
showTitleBar,
|
||||
width,
|
||||
allowFullHeight,
|
||||
showBorder,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
toggleSidebar,
|
||||
shouldDisplayFullScreen,
|
||||
currentSidebar,
|
||||
} = useContext(SidebarContext);
|
||||
|
||||
const receiveMessage = useCallback(({ data }) => {
|
||||
const { type } = data;
|
||||
if (type === 'learning.events.sidebar.close') {
|
||||
toggleSidebar(currentSidebar, WIDGETS.DISCUSSIONS);
|
||||
}
|
||||
}, [toggleSidebar]);
|
||||
|
||||
useEventListener('message', receiveMessage);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={classNames('ml-0 ml-lg-4 h-auto align-top', {
|
||||
'min-vh-100': !shouldDisplayFullScreen && allowFullHeight,
|
||||
'bg-white m-0 border-0 fixed-top vh-100 rounded-0': shouldDisplayFullScreen,
|
||||
'd-none': currentSidebar !== sidebarId,
|
||||
'border border-light-400 rounded-sm': showBorder,
|
||||
}, className)}
|
||||
data-testid={`sidebar-${sidebarId}`}
|
||||
style={{ width: shouldDisplayFullScreen ? '100%' : width }}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{shouldDisplayFullScreen
|
||||
&& (
|
||||
<div
|
||||
className="pt-2 pb-2.5 border-bottom border-light-400 d-flex align-items-center ml-2"
|
||||
onClick={() => toggleSidebar(null)}
|
||||
onKeyDown={() => toggleSidebar(null)}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
alt={intl.formatMessage(messages.responsiveCloseSidebarTray)}
|
||||
>
|
||||
<Icon src={ArrowBackIos} />
|
||||
<span className="font-weight-bold m-2 d-inline-block">
|
||||
{intl.formatMessage(messages.responsiveCloseSidebarTray)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{showTitleBar && (
|
||||
<>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="p-2.5 d-inline-block">{title}</span>
|
||||
<div className="d-inline-flex mr-2 mt-1.5 ml-auto">
|
||||
<IconButton
|
||||
src={Close}
|
||||
size="sm"
|
||||
iconAs={Icon}
|
||||
onClick={() => toggleSidebar(sidebarId)}
|
||||
alt={intl.formatMessage(messages.closeTrigger)}
|
||||
className="icon-hover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-1 bg-gray-100 border-top border-bottom border-light-400" />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
SidebarBase.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
ariaLabel: PropTypes.string.isRequired,
|
||||
sidebarId: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.element.isRequired,
|
||||
showTitleBar: PropTypes.bool,
|
||||
width: PropTypes.string,
|
||||
allowFullHeight: PropTypes.bool,
|
||||
showBorder: PropTypes.bool,
|
||||
};
|
||||
|
||||
SidebarBase.defaultProps = {
|
||||
width: '50rem',
|
||||
allowFullHeight: false,
|
||||
showTitleBar: true,
|
||||
className: '',
|
||||
showBorder: true,
|
||||
};
|
||||
|
||||
export default SidebarBase;
|
||||
6
src/courseware/course/new-sidebar/constants.js
Normal file
6
src/courseware/course/new-sidebar/constants.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const WIDGETS = {
|
||||
DISCUSSIONS: 'DISCUSSIONS',
|
||||
NOTIFICATIONS: 'NOTIFICATIONS',
|
||||
};
|
||||
|
||||
export default WIDGETS;
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const RightSidebarFilled = (props) => (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 22V2h20v20H2ZM14 4H4v16h10V4Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default RightSidebarFilled;
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const RightSidebarOutlined = (props) => (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 2v20h20V2H2Zm18 2h-4v16h4V4ZM4 4h10v16H4V4Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default RightSidebarOutlined;
|
||||
2
src/courseware/course/new-sidebar/icons/index.js
Normal file
2
src/courseware/course/new-sidebar/icons/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as RightSidebarFilled } from './RightSidebarFilled';
|
||||
export { default as RightSidebarOutlined } from './RightSidebarOutlined';
|
||||
36
src/courseware/course/new-sidebar/messages.js
Normal file
36
src/courseware/course/new-sidebar/messages.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
discussionsTitle: {
|
||||
id: 'discussions.sidebar.title',
|
||||
defaultMessage: 'Discussions',
|
||||
description: 'Title text for a forum where users are able to discuss course topics',
|
||||
},
|
||||
discussionNotificationTray: {
|
||||
id: 'discussions.notification.tray.container',
|
||||
defaultMessage: 'Discussion and Notification tray',
|
||||
description: 'Discussion and Notification tray container',
|
||||
},
|
||||
notificationTitle: {
|
||||
id: 'notification.tray.title',
|
||||
defaultMessage: 'Notifications',
|
||||
description: 'Title text displayed for the notification tray',
|
||||
},
|
||||
closeTrigger: {
|
||||
id: 'tray.close.button',
|
||||
defaultMessage: 'Close tray',
|
||||
description: 'Button for the learner to close the sidebar',
|
||||
},
|
||||
openSidebarTrigger: {
|
||||
id: 'sidebar.open.button',
|
||||
defaultMessage: 'Show sidebar tray',
|
||||
description: 'Button to open the sidebar tray and shows notifications and didcussions',
|
||||
},
|
||||
responsiveCloseSidebarTray: {
|
||||
id: 'responsive.close.sidebar',
|
||||
defaultMessage: 'Back to course',
|
||||
description: 'Responsive button to go back to course and close the sidebar tray',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SidebarBase from '../../common/SidebarBase';
|
||||
import messages from '../../messages';
|
||||
import SidebarContext from '../../SidebarContext';
|
||||
import DiscussionsSidebar from './discussions/DiscussionsWidget';
|
||||
import NotificationTray from './notifications/NotificationsWidget';
|
||||
import { ID } from './DiscussionsNotificationsTrigger';
|
||||
|
||||
const DiscussionsNotificationsSidebar = () => {
|
||||
const intl = useIntl();
|
||||
const { hideNotificationbar } = useContext(SidebarContext);
|
||||
|
||||
return (
|
||||
<SidebarBase
|
||||
ariaLabel={intl.formatMessage(messages.discussionNotificationTray)}
|
||||
sidebarId={ID}
|
||||
className="d-flex flex-column flex-fill"
|
||||
showTitleBar={false}
|
||||
showBorder={false}
|
||||
>
|
||||
<NotificationTray />
|
||||
{!hideNotificationbar && <div className="my-1.5" />}
|
||||
<DiscussionsSidebar />
|
||||
</SidebarBase>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscussionsNotificationsSidebar;
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { useContext, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
|
||||
import { getLocalStorage, setLocalStorage } from '../../../../../data/localStorage';
|
||||
import { getSessionStorage, setSessionStorage } from '../../../../../data/sessionStorage';
|
||||
import { useModel } from '../../../../../generic/model-store';
|
||||
import { getCourseDiscussionTopics } from '../../../../data/thunks';
|
||||
import { RightSidebarFilled, RightSidebarOutlined } from '../../icons';
|
||||
import messages from '../../messages';
|
||||
import SidebarContext from '../../SidebarContext';
|
||||
|
||||
export const ID = 'DISCUSSIONS_NOTIFICATIONS';
|
||||
|
||||
const DiscussionsNotificationsTrigger = ({ onClick }) => {
|
||||
const {
|
||||
courseId,
|
||||
currentSidebar,
|
||||
setNotificationStatus,
|
||||
upgradeNotificationCurrentState,
|
||||
isNotificationbarAvailable,
|
||||
isDiscussionbarAvailable,
|
||||
} = useContext(SidebarContext);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const { tabs } = useModel('courseHomeMeta', courseId);
|
||||
const baseUrl = getConfig().DISCUSSIONS_MFE_BASE_URL;
|
||||
const edxProvider = useMemo(
|
||||
() => tabs?.find(tab => tab.slug === 'discussion'),
|
||||
[tabs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (baseUrl && edxProvider) {
|
||||
dispatch(getCourseDiscussionTopics(courseId));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [courseId, baseUrl, edxProvider]);
|
||||
|
||||
/* Re-show a red dot beside the notification trigger for each of the 7 UpgradeNotification stages
|
||||
The upgradeNotificationCurrentState prop will be available after UpgradeNotification mounts. Once available,
|
||||
compare with the last state they've seen, and if it's different then set dot back to red */
|
||||
function updateUpgradeNotificationLastSeen() {
|
||||
if (upgradeNotificationCurrentState) {
|
||||
if (getLocalStorage(`upgradeNotificationLastSeen.${courseId}`) !== upgradeNotificationCurrentState) {
|
||||
setNotificationStatus('active');
|
||||
setLocalStorage(`notificationStatus.${courseId}`, 'active');
|
||||
setLocalStorage(`upgradeNotificationLastSeen.${courseId}`, upgradeNotificationCurrentState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!getLocalStorage(`notificationStatus.${courseId}`)) {
|
||||
setLocalStorage(`notificationStatus.${courseId}`, 'active'); // Show red dot on notificationTrigger until seen
|
||||
}
|
||||
|
||||
if (!getLocalStorage(`upgradeNotificationCurrentState.${courseId}`)) {
|
||||
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'initialize');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateUpgradeNotificationLastSeen();
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
if (getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open') {
|
||||
setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed');
|
||||
} else {
|
||||
setSessionStorage(`notificationTrayStatus.${courseId}`, 'open');
|
||||
}
|
||||
onClick();
|
||||
};
|
||||
|
||||
if (!isDiscussionbarAvailable && !isNotificationbarAvailable) { return null; }
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
src={currentSidebar ? RightSidebarFilled : RightSidebarOutlined}
|
||||
iconAs={Icon}
|
||||
onClick={handleClick}
|
||||
alt={intl.formatMessage(messages.openSidebarTrigger)}
|
||||
className="icon-hover"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionsNotificationsTrigger.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DiscussionsNotificationsTrigger;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import classNames from 'classnames';
|
||||
import SidebarContext from '../../../SidebarContext';
|
||||
import messages from '../../../messages';
|
||||
|
||||
ensureConfig(['DISCUSSIONS_MFE_BASE_URL']);
|
||||
|
||||
const DiscussionsWidget = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
unitId,
|
||||
courseId,
|
||||
hideDiscussionbar,
|
||||
isDiscussionbarAvailable,
|
||||
shouldDisplayFullScreen,
|
||||
} = useContext(SidebarContext);
|
||||
const discussionsUrl = `${getConfig().DISCUSSIONS_MFE_BASE_URL}/${courseId}/category/${unitId}`;
|
||||
|
||||
if (hideDiscussionbar || !isDiscussionbarAvailable) { return null; }
|
||||
|
||||
return (
|
||||
<iframe
|
||||
src={`${discussionsUrl}?inContextSidebar`}
|
||||
className={classNames('d-flex w-100 flex-fill border border-light-400 rounded-sm', { 'vh-100': !shouldDisplayFullScreen })}
|
||||
title={intl.formatMessage(messages.discussionsTitle)}
|
||||
allow="clipboard-write"
|
||||
loading="lazy"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscussionsWidget;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import {
|
||||
initializeMockApp, initializeTestStore, render, screen,
|
||||
} from '../../../../../../setupTest';
|
||||
import { executeThunk } from '../../../../../../utils';
|
||||
import { buildTopicsFromUnits } from '../../../../../data/__factories__/discussionTopics.factory';
|
||||
import { getCourseDiscussionTopics } from '../../../../../data/thunks';
|
||||
import SidebarContext from '../../../SidebarContext';
|
||||
import DiscussionsWidget from './DiscussionsWidget';
|
||||
|
||||
initializeMockApp();
|
||||
|
||||
describe('DiscussionsWidget', () => {
|
||||
let axiosMock;
|
||||
let mockData;
|
||||
let courseId;
|
||||
let unitId;
|
||||
|
||||
beforeEach(async () => {
|
||||
const store = await initializeTestStore({
|
||||
excludeFetchCourse: false,
|
||||
excludeFetchSequence: false,
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const state = store.getState();
|
||||
courseId = state.courseware.courseId;
|
||||
[unitId] = Object.keys(state.models.units);
|
||||
|
||||
mockData = {
|
||||
courseId,
|
||||
unitId,
|
||||
currentSidebar: 'NEWSIDEBAR',
|
||||
hideDiscussionbar: false,
|
||||
isDiscussionbarAvailable: true,
|
||||
};
|
||||
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(
|
||||
200,
|
||||
{
|
||||
provider: 'openedx',
|
||||
},
|
||||
);
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`)
|
||||
.reply(200, buildTopicsFromUnits(state.models.units));
|
||||
await executeThunk(getCourseDiscussionTopics(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
function renderWithProvider(testData = {}) {
|
||||
const { container } = render(
|
||||
<SidebarContext.Provider value={{ ...mockData, ...testData }}>
|
||||
<DiscussionsWidget />
|
||||
</SidebarContext.Provider>,
|
||||
);
|
||||
return container;
|
||||
}
|
||||
|
||||
it('should show up if unit discussions associated with it', async () => {
|
||||
renderWithProvider();
|
||||
expect(screen.queryByTitle('Discussions')).toBeInTheDocument();
|
||||
expect(screen.queryByTitle('Discussions'))
|
||||
.toHaveAttribute('src', `http://localhost:2002/${courseId}/category/${unitId}?inContextSidebar`);
|
||||
});
|
||||
|
||||
it('should show nothing if unit has no discussions associated with it', async () => {
|
||||
renderWithProvider({ isDiscussionbarAvailable: false });
|
||||
expect(screen.queryByTitle('Discussions')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Sidebar } from './DiscussionsNotificationsSidebar';
|
||||
export { default as Trigger, ID } from './DiscussionsNotificationsTrigger';
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
|
||||
import { useModel } from '../../../../../../generic/model-store';
|
||||
import UpgradeNotification from '../../../../../../generic/upgrade-notification/UpgradeNotification';
|
||||
import WIDGETS from '../../../constants';
|
||||
import SidebarContext from '../../../SidebarContext';
|
||||
|
||||
const NotificationsWidget = () => {
|
||||
const {
|
||||
courseId,
|
||||
onNotificationSeen,
|
||||
upgradeNotificationCurrentState,
|
||||
setUpgradeNotificationCurrentState,
|
||||
hideNotificationbar,
|
||||
toggleSidebar,
|
||||
isNotificationbarAvailable,
|
||||
currentSidebar,
|
||||
} = useContext(SidebarContext);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
|
||||
const {
|
||||
accessExpiration,
|
||||
contentTypeGatingEnabled,
|
||||
marketingUrl,
|
||||
offer,
|
||||
timeOffsetMillis,
|
||||
userTimezone,
|
||||
} = course;
|
||||
|
||||
const {
|
||||
org,
|
||||
verifiedMode,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
// After three seconds, update notificationSeen (to hide red dot)
|
||||
useEffect(() => { setTimeout(onNotificationSeen, 3000); }, []);
|
||||
|
||||
if (hideNotificationbar || !isNotificationbarAvailable) { return null; }
|
||||
|
||||
return (
|
||||
<div className="border border-light-400 rounded-sm">
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
contentTypeGatingEnabled={contentTypeGatingEnabled}
|
||||
marketingUrl={marketingUrl}
|
||||
upsellPageName="in_course"
|
||||
userTimezone={userTimezone}
|
||||
shouldDisplayBorder={false}
|
||||
timeOffsetMillis={timeOffsetMillis}
|
||||
courseId={courseId}
|
||||
org={org}
|
||||
upgradeNotificationCurrentState={upgradeNotificationCurrentState}
|
||||
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
|
||||
toggleSidebar={() => toggleSidebar(currentSidebar, WIDGETS.NOTIFICATIONS)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsWidget;
|
||||
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React from 'react';
|
||||
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { breakpoints } from '@edx/paragon';
|
||||
|
||||
import { initializeMockApp, render, screen } from '../../../../../../setupTest';
|
||||
import initializeStore from '../../../../../../store';
|
||||
import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../utils';
|
||||
import { fetchCourse } from '../../../../../data';
|
||||
import SidebarContext from '../../../SidebarContext';
|
||||
import NotificationsWidget from './NotificationsWidget';
|
||||
|
||||
initializeMockApp();
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
describe('NotificationsWidget', () => {
|
||||
let axiosMock;
|
||||
let store;
|
||||
const ID = 'NEWSIDEBAR';
|
||||
|
||||
const defaultMetadata = Factory.build('courseMetadata');
|
||||
const courseId = defaultMetadata.id;
|
||||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`;
|
||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata');
|
||||
const courseHomeMetadataUrl = appendBrowserTimezoneToUrl(`${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`);
|
||||
|
||||
function setMetadata(attributes, options) {
|
||||
const updatedCourseHomeMetadata = Factory.build('courseHomeMetadata', attributes, options);
|
||||
axiosMock.onGet(courseHomeMetadataUrl).reply(200, updatedCourseHomeMetadata);
|
||||
}
|
||||
|
||||
async function fetchAndRender(component) {
|
||||
await executeThunk(fetchCourse(defaultMetadata.id), store.dispatch);
|
||||
render(component, { store });
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
global.innerWidth = breakpoints.large.minWidth;
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
|
||||
});
|
||||
|
||||
it('renders upgrade card', async () => {
|
||||
await fetchAndRender(
|
||||
<SidebarContext.Provider value={{
|
||||
currentSidebar: ID,
|
||||
courseId,
|
||||
hideNotificationbar: false,
|
||||
isNotificationbarAvailable: true,
|
||||
}}
|
||||
>
|
||||
<NotificationsWidget />
|
||||
</SidebarContext.Provider>,
|
||||
);
|
||||
const UpgradeNotification = document.querySelector('.upgrade-notification');
|
||||
|
||||
expect(UpgradeNotification)
|
||||
.toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Upgrade for $149' }))
|
||||
.toBeInTheDocument();
|
||||
expect(screen.queryByText('You have no new notifications at this time.'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no notifications bar if no verified mode', async () => {
|
||||
setMetadata({ verified_mode: null });
|
||||
await fetchAndRender(
|
||||
<SidebarContext.Provider value={{
|
||||
currentSidebar: ID,
|
||||
courseId,
|
||||
hideNotificationbar: true,
|
||||
isNotificationbarAvailable: false,
|
||||
}}
|
||||
>
|
||||
<NotificationsWidget />
|
||||
</SidebarContext.Provider>,
|
||||
);
|
||||
expect(screen.queryByText('Notifications'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('marks notification as seen 3 seconds later', async () => {
|
||||
jest.useFakeTimers();
|
||||
const onNotificationSeen = jest.fn();
|
||||
await fetchAndRender(
|
||||
<SidebarContext.Provider value={{
|
||||
currentSidebar: ID,
|
||||
courseId,
|
||||
onNotificationSeen,
|
||||
hideNotificationbar: false,
|
||||
isNotificationbarAvailable: true,
|
||||
}}
|
||||
>
|
||||
<NotificationsWidget />
|
||||
</SidebarContext.Provider>,
|
||||
);
|
||||
expect(onNotificationSeen).toHaveBeenCalledTimes(0);
|
||||
jest.advanceTimersByTime(3000);
|
||||
expect(onNotificationSeen).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
13
src/courseware/course/new-sidebar/sidebars/index.js
Normal file
13
src/courseware/course/new-sidebar/sidebars/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as discussionsNotifications from './discussions-notifications';
|
||||
|
||||
export const SIDEBARS = {
|
||||
[discussionsNotifications.ID]: {
|
||||
ID: discussionsNotifications.ID,
|
||||
Sidebar: discussionsNotifications.Sidebar,
|
||||
Trigger: discussionsNotifications.Trigger,
|
||||
},
|
||||
};
|
||||
|
||||
export const SIDEBAR_ORDER = [
|
||||
discussionsNotifications.ID,
|
||||
];
|
||||
@@ -2,13 +2,14 @@
|
||||
import React, {
|
||||
useEffect, useState,
|
||||
} from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
sendTrackEvent,
|
||||
sendTrackingLogEvent,
|
||||
} from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import SequenceExamWrapper from '@edx/frontend-lib-special-exams';
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
@@ -19,7 +20,9 @@ import { useSequenceBannerTextAlert, useSequenceEntranceExamAlert } from '../../
|
||||
|
||||
import CourseLicense from '../course-license';
|
||||
import Sidebar from '../sidebar/Sidebar';
|
||||
import NewSidebar from '../new-sidebar/Sidebar';
|
||||
import SidebarTriggers from '../sidebar/SidebarTriggers';
|
||||
import NewSidebarTriggers from '../new-sidebar/SidebarTriggers';
|
||||
import messages from './messages';
|
||||
import HiddenAfterDue from './hidden-after-due';
|
||||
import { SequenceNavigation, UnitNavigation } from './sequence-navigation';
|
||||
@@ -32,8 +35,8 @@ const Sequence = ({
|
||||
unitNavigationHandler,
|
||||
nextSequenceHandler,
|
||||
previousSequenceHandler,
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const {
|
||||
isStaff,
|
||||
@@ -44,6 +47,7 @@ const Sequence = ({
|
||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||
const sequenceMightBeUnit = useSelector(state => state.courseware.sequenceMightBeUnit);
|
||||
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
|
||||
const enableNewSidebar = getConfig().ENABLE_NEW_SIDEBAR;
|
||||
|
||||
const handleNext = () => {
|
||||
const nextIndex = sequence.unitIds.indexOf(unitId) + 1;
|
||||
@@ -159,7 +163,9 @@ const Sequence = ({
|
||||
handlePrevious();
|
||||
}}
|
||||
/>
|
||||
{shouldDisplayNotificationTriggerInSequence && <SidebarTriggers />}
|
||||
{shouldDisplayNotificationTriggerInSequence && (
|
||||
enableNewSidebar === 'true' ? <NewSidebarTriggers /> : <SidebarTriggers />
|
||||
)}
|
||||
|
||||
<div className="unit-container flex-grow-1">
|
||||
<SequenceContent
|
||||
@@ -185,7 +191,7 @@ const Sequence = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Sidebar />
|
||||
{enableNewSidebar === 'true' ? <NewSidebar /> : <Sidebar />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -221,7 +227,6 @@ Sequence.propTypes = {
|
||||
unitNavigationHandler: PropTypes.func.isRequired,
|
||||
nextSequenceHandler: PropTypes.func.isRequired,
|
||||
previousSequenceHandler: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
Sequence.defaultProps = {
|
||||
@@ -229,4 +234,4 @@ Sequence.defaultProps = {
|
||||
unitId: null,
|
||||
};
|
||||
|
||||
export default injectIntl(Sequence);
|
||||
export default Sequence;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useIntl, FormattedDate, FormattedMessage, injectIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { FormattedDate, FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Button, Icon, IconButton } from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import { setLocalStorage } from '../../data/localStorage';
|
||||
import { UpgradeButton } from '../upgrade-button';
|
||||
import {
|
||||
@@ -12,6 +15,7 @@ import {
|
||||
FullAccessBullet,
|
||||
SupportMissionBullet,
|
||||
} from '../upsell-bullets/UpsellBullets';
|
||||
import messages from '../messages';
|
||||
|
||||
const UpsellNoFBECardContent = () => (
|
||||
<ul className="fa-ul upgrade-notification-ul pt-0">
|
||||
@@ -122,7 +126,7 @@ const ExpirationCountdown = ({
|
||||
expirationText = (
|
||||
<FormattedMessage
|
||||
id="learning.generic.upgradeNotification.expirationDays"
|
||||
defaultMessage={`{dayCount, number} {dayCount, plural,
|
||||
defaultMessage={`{dayCount, number} {dayCount, plural,
|
||||
one {day}
|
||||
other {days}} left`}
|
||||
values={{
|
||||
@@ -283,7 +287,9 @@ const UpgradeNotification = ({
|
||||
upsellPageName,
|
||||
userTimezone,
|
||||
verifiedMode,
|
||||
toggleSidebar,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dateNow = Date.now();
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
const correctedTime = new Date(dateNow + timeOffsetMillis);
|
||||
@@ -483,8 +489,25 @@ const UpgradeNotification = ({
|
||||
return (
|
||||
<section className={classNames('upgrade-notification small', { 'card mb-4': shouldDisplayBorder })}>
|
||||
<div id="courseHome-upgradeNotification">
|
||||
<h2 className="h5 upgrade-notification-header" id="outline-sidebar-upgrade-header">
|
||||
<h2
|
||||
className={classNames('h5 upgrade-notification-header', {
|
||||
'd-flex align-items-center mr-2 ml-4 my-1.5 font-size-18': !!toggleSidebar,
|
||||
})}
|
||||
id="outline-sidebar-upgrade-header"
|
||||
>
|
||||
{upgradeNotificationHeaderText}
|
||||
{!!toggleSidebar && (
|
||||
<div className="d-inline-flex ml-auto">
|
||||
<IconButton
|
||||
src={Close}
|
||||
size="sm"
|
||||
iconAs={Icon}
|
||||
onClick={toggleSidebar}
|
||||
className="icon-hover"
|
||||
alt={intl.formatMessage(messages.close)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</h2>
|
||||
{expirationBanner}
|
||||
<div className="upgrade-notification-message">
|
||||
@@ -512,6 +535,7 @@ UpgradeNotification.propTypes = {
|
||||
percentage: PropTypes.number,
|
||||
code: PropTypes.string,
|
||||
}),
|
||||
toggleSidebar: PropTypes.func,
|
||||
shouldDisplayBorder: PropTypes.bool,
|
||||
setupgradeNotificationCurrentState: PropTypes.func,
|
||||
timeOffsetMillis: PropTypes.number,
|
||||
@@ -534,6 +558,7 @@ UpgradeNotification.defaultProps = {
|
||||
timeOffsetMillis: 0,
|
||||
userTimezone: null,
|
||||
verifiedMode: null,
|
||||
toggleSidebar: null,
|
||||
};
|
||||
|
||||
export default injectIntl(UpgradeNotification);
|
||||
|
||||
@@ -41,3 +41,7 @@
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
}
|
||||
|
||||
.font-size-18 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ initialize({
|
||||
DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null,
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
|
||||
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
|
||||
ENABLE_NEW_SIDEBAR: process.env.ENABLE_NEW_SIDEBAR || null,
|
||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
|
||||
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
||||
|
||||
@@ -361,6 +361,13 @@
|
||||
box-shadow: 0 .0625rem .125rem rgba(0, 0, 0, .2) !important;
|
||||
}
|
||||
|
||||
.icon-hover {
|
||||
&:hover {
|
||||
color: $primary-500 !important;
|
||||
background-color: $light-300 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Import component-specific sass files
|
||||
@import "courseware/course/celebration/CelebrationModal.scss";
|
||||
@import "courseware/course/sidebar/sidebars/notifications/NotificationIcon.scss";
|
||||
|
||||
Reference in New Issue
Block a user