fix: user content unavailable content issue for learner (#674)
This commit is contained in:
@@ -1,30 +1,21 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import fetchTab from './data/thunks';
|
|
||||||
import Tabs from './tabs/Tabs';
|
import Tabs from './tabs/Tabs';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
import './navBar.scss';
|
import './navBar.scss';
|
||||||
|
|
||||||
const CourseTabsNavigation = ({
|
const CourseTabsNavigation = () => {
|
||||||
activeTab, className, courseId, rootSlug,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const tabs = useSelector(state => state.courseTabs.tabs);
|
const tabs = useSelector(state => state.courseTabs.tabs);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchTab(courseId, rootSlug));
|
|
||||||
}, [courseId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation px-4 bg-white', className)}>
|
<div id="courseTabsNavigation" className="course-tabs-navigation px-4 bg-white">
|
||||||
{!!tabs.length && (
|
{!!tabs.length && (
|
||||||
<Tabs
|
<Tabs
|
||||||
className="nav-underline-tabs"
|
className="nav-underline-tabs"
|
||||||
@@ -33,7 +24,7 @@ const CourseTabsNavigation = ({
|
|||||||
{tabs.map(({ url, title, slug }) => (
|
{tabs.map(({ url, title, slug }) => (
|
||||||
<a
|
<a
|
||||||
key={slug}
|
key={slug}
|
||||||
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTab })}
|
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === 'discussion' })}
|
||||||
href={url}
|
href={url}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -45,17 +36,4 @@ const CourseTabsNavigation = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseTabsNavigation.propTypes = {
|
|
||||||
activeTab: PropTypes.string,
|
|
||||||
className: PropTypes.string,
|
|
||||||
rootSlug: PropTypes.string,
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
CourseTabsNavigation.defaultProps = {
|
|
||||||
activeTab: undefined,
|
|
||||||
className: null,
|
|
||||||
rootSlug: 'outline',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(CourseTabsNavigation);
|
export default React.memo(CourseTabsNavigation);
|
||||||
|
|||||||
@@ -5,15 +5,12 @@ import { getApiBaseUrl } from '../../../data/constants';
|
|||||||
|
|
||||||
export const getCourseMetadataApiUrl = (courseId) => `${getApiBaseUrl()}/api/course_home/course_metadata/${courseId}`;
|
export const getCourseMetadataApiUrl = (courseId) => `${getApiBaseUrl()}/api/course_home/course_metadata/${courseId}`;
|
||||||
|
|
||||||
function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
|
function normalizeCourseHomeCourseMetadata(metadata) {
|
||||||
const data = camelCaseObject(metadata);
|
const data = camelCaseObject(metadata);
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
tabs: data.tabs.map(tab => ({
|
tabs: data.tabs.map(tab => ({
|
||||||
// The API uses "courseware" as a slug for both courseware and the outline tab.
|
slug: tab.tabId === 'courseware' ? 'outline' : tab.tabId,
|
||||||
// If needed, we switch it to "outline" here for
|
|
||||||
// use within the MFE to differentiate between course home and courseware.
|
|
||||||
slug: tab.tabId === 'courseware' ? rootSlug : tab.tabId,
|
|
||||||
title: tab.title,
|
title: tab.title,
|
||||||
url: tab.url,
|
url: tab.url,
|
||||||
})),
|
})),
|
||||||
@@ -21,10 +18,9 @@ function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCourseHomeCourseMetadata(courseId, rootSlug) {
|
export async function getCourseHomeCourseMetadata(courseId) {
|
||||||
const url = getCourseMetadataApiUrl(courseId);
|
const url = getCourseMetadataApiUrl(courseId);
|
||||||
// don't know the context of adding timezone in url. hence omitting it
|
|
||||||
// url = appendBrowserTimezoneToUrl(url);
|
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
return normalizeCourseHomeCourseMetadata(data, rootSlug);
|
|
||||||
|
return normalizeCourseHomeCourseMetadata(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
fetchTabSuccess,
|
fetchTabSuccess,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
|
|
||||||
export default function fetchTab(courseId, rootSlug) {
|
export default function fetchTab(courseId) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(fetchTabRequest({ courseId }));
|
dispatch(fetchTabRequest({ courseId }));
|
||||||
try {
|
try {
|
||||||
const courseHomeCourseMetadata = await getCourseHomeCourseMetadata(courseId, rootSlug);
|
const courseHomeCourseMetadata = await getCourseHomeCourseMetadata(courseId);
|
||||||
if (!courseHomeCourseMetadata.courseAccess.hasAccess) {
|
if (!courseHomeCourseMetadata.courseAccess.hasAccess) {
|
||||||
dispatch(fetchTabDenied({ courseId }));
|
dispatch(fetchTabDenied({ courseId }));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import selectCourseTabs from '../../components/NavigationBar/data/selectors';
|
|||||||
import { useIsOnDesktop, useIsOnXLDesktop } from '../data/hooks';
|
import { useIsOnDesktop, useIsOnXLDesktop } from '../data/hooks';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const CourseContentUnavailable = ({ subTitleMessage }) => {
|
const ContentUnavailable = ({ subTitleMessage }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const isOnDesktop = useIsOnDesktop();
|
const isOnDesktop = useIsOnDesktop();
|
||||||
const isOnXLDesktop = useIsOnXLDesktop();
|
const isOnXLDesktop = useIsOnXLDesktop();
|
||||||
@@ -31,7 +31,9 @@ const CourseContentUnavailable = ({ subTitleMessage }) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ContentUnavailableIcon />
|
<ContentUnavailableIcon />
|
||||||
<h3 className="pt-3 font-weight-bold text-primary-500 text-center">{intl.formatMessage(messages.contentUnavailableTitle)}</h3>
|
<h3 className="pt-3 font-weight-bold text-primary-500 text-center">
|
||||||
|
{intl.formatMessage(messages.contentUnavailableTitle)}
|
||||||
|
</h3>
|
||||||
<p className="pb-2 text-gray-500 text-center">{intl.formatMessage(subTitleMessage)}</p>
|
<p className="pb-2 text-gray-500 text-center">{intl.formatMessage(subTitleMessage)}</p>
|
||||||
<Button onClick={redirectToDashboard} variant="outline-dark" className="font-size-14 py-2 px-2.5">
|
<Button onClick={redirectToDashboard} variant="outline-dark" className="font-size-14 py-2 px-2.5">
|
||||||
{intl.formatMessage(messages.contentUnavailableAction)}
|
{intl.formatMessage(messages.contentUnavailableAction)}
|
||||||
@@ -41,7 +43,7 @@ const CourseContentUnavailable = ({ subTitleMessage }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseContentUnavailable.propTypes = {
|
ContentUnavailable.propTypes = {
|
||||||
subTitleMessage: propTypes.shape({
|
subTitleMessage: propTypes.shape({
|
||||||
id: propTypes.string,
|
id: propTypes.string,
|
||||||
defaultMessage: propTypes.string,
|
defaultMessage: propTypes.string,
|
||||||
@@ -49,4 +51,4 @@ CourseContentUnavailable.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(CourseContentUnavailable);
|
export default React.memo(ContentUnavailable);
|
||||||
@@ -13,6 +13,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
|||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||||
|
|
||||||
|
import fetchTab from '../../components/NavigationBar/data/thunks';
|
||||||
import { RequestStatus, Routes } from '../../data/constants';
|
import { RequestStatus, Routes } from '../../data/constants';
|
||||||
import { selectTopicsUnderCategory } from '../../data/selectors';
|
import { selectTopicsUnderCategory } from '../../data/selectors';
|
||||||
import fetchCourseBlocks from '../../data/thunks';
|
import fetchCourseBlocks from '../../data/thunks';
|
||||||
@@ -79,6 +80,7 @@ export function useCourseDiscussionData(courseId) {
|
|||||||
async function fetchBaseData() {
|
async function fetchBaseData() {
|
||||||
await dispatch(fetchCourseConfig(courseId));
|
await dispatch(fetchCourseConfig(courseId));
|
||||||
await dispatch(fetchCourseBlocks(courseId, authenticatedUser.username));
|
await dispatch(fetchCourseBlocks(courseId, authenticatedUser.username));
|
||||||
|
await dispatch(fetchTab(courseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchBaseData();
|
fetchBaseData();
|
||||||
|
|||||||
@@ -76,10 +76,12 @@ export const selectIsUserLearner = createSelector(
|
|||||||
userIsCourseAdmin,
|
userIsCourseAdmin,
|
||||||
userIsCourseStaff,
|
userIsCourseStaff,
|
||||||
) => (
|
) => (
|
||||||
!userHasModerationPrivileges
|
(
|
||||||
&& !userIsGroupTa
|
!userHasModerationPrivileges
|
||||||
&& !userIsStaff
|
&& !userIsGroupTa
|
||||||
&& !userIsCourseAdmin
|
&& !userIsStaff
|
||||||
&& !userIsCourseStaff
|
&& !userIsCourseAdmin
|
||||||
|
&& !userIsCourseStaff
|
||||||
|
) || false
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import { LearningHeader as Header } from '@edx/frontend-component-header';
|
|||||||
|
|
||||||
import { Spinner } from '../../components';
|
import { Spinner } from '../../components';
|
||||||
import selectCourseTabs from '../../components/NavigationBar/data/selectors';
|
import selectCourseTabs from '../../components/NavigationBar/data/selectors';
|
||||||
import { LOADING } from '../../components/NavigationBar/data/slice';
|
import { LOADED } from '../../components/NavigationBar/data/slice';
|
||||||
import { ALL_ROUTES, DiscussionProvider, Routes as ROUTES } from '../../data/constants';
|
import { ALL_ROUTES, DiscussionProvider, Routes as ROUTES } from '../../data/constants';
|
||||||
import DiscussionContext from '../common/context';
|
import DiscussionContext from '../common/context';
|
||||||
import ContentUnavailable from '../course-content-unavailable/CourseContentUnavailable';
|
import ContentUnavailable from '../content-unavailable/ContentUnavailable';
|
||||||
import {
|
import {
|
||||||
useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useSidebarVisible,
|
useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useSidebarVisible,
|
||||||
} from '../data/hooks';
|
} from '../data/hooks';
|
||||||
@@ -80,55 +80,48 @@ const DiscussionsHome = () => {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={(<Spinner />)}>
|
<Suspense fallback={(<Spinner />)}>
|
||||||
<DiscussionContext.Provider value={discussionContextValue}>
|
<DiscussionContext.Provider value={discussionContextValue}>
|
||||||
{!enableInContextSidebar && (
|
{!enableInContextSidebar && (<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />)}
|
||||||
<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />
|
|
||||||
)}
|
|
||||||
<main className="container-fluid d-flex flex-column p-0 w-100" id="main" tabIndex="-1">
|
<main className="container-fluid d-flex flex-column p-0 w-100" id="main" tabIndex="-1">
|
||||||
{!enableInContextSidebar && <CourseTabsNavigation activeTab="discussion" courseId={courseId} />}
|
{!enableInContextSidebar && <CourseTabsNavigation />}
|
||||||
{(isEnrolled || !isUserLearner || enableInContextSidebar) && (
|
{(isEnrolled || !isUserLearner) && (
|
||||||
<div
|
|
||||||
className={classNames('header-action-bar bg-white position-sticky', {
|
|
||||||
'shadow-none border-light-300 border-bottom': enableInContextSidebar,
|
|
||||||
})}
|
|
||||||
ref={postActionBarRef}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={classNames('d-flex flex-row justify-content-between navbar fixed-top', {
|
className={classNames('header-action-bar bg-white position-sticky', {
|
||||||
|
'shadow-none border-light-300 border-bottom': enableInContextSidebar,
|
||||||
|
})}
|
||||||
|
ref={postActionBarRef}
|
||||||
|
>
|
||||||
|
<div className={classNames('d-flex flex-row justify-content-between navbar fixed-top', {
|
||||||
'pl-4 pr-2 py-0': enableInContextSidebar,
|
'pl-4 pr-2 py-0': enableInContextSidebar,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!enableInContextSidebar && (
|
{!enableInContextSidebar && (<NavigationBar />)}
|
||||||
<NavigationBar />
|
<PostActionsBar />
|
||||||
)}
|
</div>
|
||||||
<PostActionsBar />
|
<DiscussionsRestrictionBanner />
|
||||||
</div>
|
</div>
|
||||||
<DiscussionsRestrictionBanner />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{provider === DiscussionProvider.LEGACY && (
|
{provider === DiscussionProvider.LEGACY && (
|
||||||
<Suspense fallback={(<Spinner />)}>
|
<Suspense fallback={(<Spinner />)}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{[
|
{[
|
||||||
ROUTES.TOPICS.CATEGORY,
|
ROUTES.TOPICS.CATEGORY,
|
||||||
ROUTES.TOPICS.CATEGORY_POST,
|
ROUTES.TOPICS.CATEGORY_POST,
|
||||||
ROUTES.TOPICS.CATEGORY_POST_EDIT,
|
ROUTES.TOPICS.CATEGORY_POST_EDIT,
|
||||||
ROUTES.TOPICS.TOPIC,
|
ROUTES.TOPICS.TOPIC,
|
||||||
ROUTES.TOPICS.TOPIC_POST,
|
ROUTES.TOPICS.TOPIC_POST,
|
||||||
ROUTES.TOPICS.TOPIC_POST_EDIT,
|
ROUTES.TOPICS.TOPIC_POST_EDIT,
|
||||||
].map((route) => (
|
].map((route) => (
|
||||||
<Route
|
<Route
|
||||||
key={route}
|
key={route}
|
||||||
path={route}
|
path={route}
|
||||||
element={<LegacyBreadcrumbMenu />}
|
element={<LegacyBreadcrumbMenu />}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
{(courseStatus !== LOADING || enableInContextSidebar) && (
|
{(courseStatus === LOADED) && (
|
||||||
<div>
|
!isEnrolled && isUserLearner ? (
|
||||||
{ isEnrolled === false && isUserLearner ? (
|
|
||||||
<Suspense fallback={(<Spinner />)}>
|
<Suspense fallback={(<Spinner />)}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{ALL_ROUTES.map((route) => (
|
{ALL_ROUTES.map((route) => (
|
||||||
@@ -140,18 +133,17 @@ const DiscussionsHome = () => {
|
|||||||
))}
|
))}
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
) : (
|
||||||
: (
|
<div className="d-flex flex-row position-relative">
|
||||||
<div className="d-flex flex-row position-relative">
|
<Suspense fallback={(<Spinner />)}>
|
||||||
<Suspense fallback={(<Spinner />)}>
|
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={postActionBarRef} />
|
||||||
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={postActionBarRef} />
|
</Suspense>
|
||||||
</Suspense>
|
{displayContentArea && (
|
||||||
{displayContentArea && (
|
|
||||||
<Suspense fallback={(<Spinner />)}>
|
<Suspense fallback={(<Spinner />)}>
|
||||||
<DiscussionContent />
|
<DiscussionContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
{!displayContentArea && (
|
{!displayContentArea && (
|
||||||
<Routes>
|
<Routes>
|
||||||
<>
|
<>
|
||||||
{ROUTES.TOPICS.PATH.map(route => (
|
{ROUTES.TOPICS.PATH.map(route => (
|
||||||
@@ -176,14 +168,11 @@ const DiscussionsHome = () => {
|
|||||||
<Route path={ROUTES.LEARNERS.PATH} element={<EmptyLearners />} />
|
<Route path={ROUTES.LEARNERS.PATH} element={<EmptyLearners />} />
|
||||||
</>
|
</>
|
||||||
</Routes>
|
</Routes>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!enableInContextSidebar && (
|
|
||||||
<DiscussionsProductTour />
|
|
||||||
)}
|
)}
|
||||||
|
{!enableInContextSidebar && (<DiscussionsProductTour />)}
|
||||||
</main>
|
</main>
|
||||||
{!enableInContextSidebar && <Footer />}
|
{!enableInContextSidebar && <Footer />}
|
||||||
</DiscussionContext.Provider>
|
</DiscussionContext.Provider>
|
||||||
|
|||||||
Reference in New Issue
Block a user