fix: user content unavailable content issue for learner (#674)

This commit is contained in:
Awais Ansari
2024-02-28 18:04:02 +05:00
committed by GitHub
parent 6875165eb3
commit 3a7b7054e7
7 changed files with 75 additions and 106 deletions

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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();

View File

@@ -76,10 +76,12 @@ export const selectIsUserLearner = createSelector(
userIsCourseAdmin, userIsCourseAdmin,
userIsCourseStaff, userIsCourseStaff,
) => ( ) => (
!userHasModerationPrivileges (
&& !userIsGroupTa !userHasModerationPrivileges
&& !userIsStaff && !userIsGroupTa
&& !userIsCourseAdmin && !userIsStaff
&& !userIsCourseStaff && !userIsCourseAdmin
&& !userIsCourseStaff
) || false
), ),
); );

View File

@@ -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>