Compare commits

..

5 Commits

Author SHA1 Message Date
Awais Ansari
16901009cb fix: profile picture is not showing in forum responses (#850) 2026-01-02 14:57:44 +05:00
Awais Ansari
52a4e5c94c fix: cohort name difference in filter and post view (#844) 2025-12-18 18:34:51 +05:00
Awais Ansari
29da3f5939 fix: fetch topics on MFE load (#843) 2025-12-18 18:34:27 +05:00
Tobias Macey
1268442415 fix: prevent generatePath error when author is invalid in AuthorLabel
Wrap learnerPostsLink creation in useMemo with guard to prevent
'Missing :learnerUsername param' error. The generatePath function
was being called unconditionally during render even when the link
wouldn't be displayed, causing errors when author was null, undefined,
or the 'anonymous' string.

The fix ensures generatePath is only called when showUserNameAsLink
is true, which validates that author is a valid username.
2025-11-14 12:26:00 -05:00
renovate[bot]
60996abdc5 chore(deps): update dependency @edx/frontend-platform to v8.5.2 (#775)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-14 12:26:00 -05:00
23 changed files with 2983 additions and 1235 deletions

View File

@@ -1,7 +0,0 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -12,11 +12,11 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Install dependencies - name: Install dependencies

4029
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,9 +31,9 @@
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0", "@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^8.1.0", "@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-platform": "^8.3.3", "@edx/frontend-platform": "^8.3.3",
"@edx/openedx-atlas": "^0.7.0", "@edx/openedx-atlas": "^0.6.0",
"@openedx/paragon": "^23.4.5", "@openedx/paragon": "^23.4.5",
"@reduxjs/toolkit": "1.9.7", "@reduxjs/toolkit": "1.9.7",
"@tinymce/tinymce-react": "5.1.1", "@tinymce/tinymce-react": "5.1.1",
@@ -41,7 +41,7 @@
"classnames": "2.5.1", "classnames": "2.5.1",
"core-js": "3.21.1", "core-js": "3.21.1",
"dompurify": "^2.4.3", "dompurify": "^2.4.3",
"formik": "2.4.9", "formik": "2.4.5",
"lodash.snakecase": "4.1.1", "lodash.snakecase": "4.1.1",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"raw-loader": "4.0.2", "raw-loader": "4.0.2",
@@ -56,20 +56,20 @@
"redux": "4.2.1", "redux": "4.2.1",
"regenerator-runtime": "0.14.1", "regenerator-runtime": "0.14.1",
"timeago.js": "4.0.2", "timeago.js": "4.0.2",
"tinymce": "5.10.9", "tinymce": "5.10.7",
"yup": "0.32.11" "yup": "0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "1.5.0", "@edx/browserslist-config": "1.2.0",
"@openedx/frontend-build": "^14.6.2", "@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "5.17.0", "@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "14.3.1", "@testing-library/react": "14.3.1",
"@testing-library/user-event": "13.5.0", "@testing-library/user-event": "13.5.0",
"axios": "^0.30.0", "axios": "^0.28.0",
"axios-mock-adapter": "1.22.0", "axios-mock-adapter": "1.22.0",
"babel-plugin-react-intl": "8.2.25", "babel-plugin-react-intl": "8.2.25",
"eslint-plugin-simple-import-sort": "7.0.0", "eslint-plugin-simple-import-sort": "7.0.0",
"glob": "7.2.3", "glob": "7.2.0",
"jest": "29.7.0", "jest": "29.7.0",
"rosie": "2.1.1" "rosie": "2.1.1"
} }

View File

@@ -175,7 +175,7 @@ const FilterBar = ({
<ActionItem <ActionItem
key={toString(cohort.id)} key={toString(cohort.id)}
id={toString(cohort.id)} id={toString(cohort.id)}
label={capitalize(cohort.name)} label={cohort.name}
value={toString(cohort.id)} value={toString(cohort.id)}
selected={selectedFilters.cohort} selected={selectedFilters.cohort}
/> />

View File

@@ -4,6 +4,7 @@ import {
} from 'react'; } from 'react';
import { breakpoints, useWindowSize } from '@openedx/paragon'; import { breakpoints, useWindowSize } from '@openedx/paragon';
import isEmpty from 'lodash/isEmpty';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { import {
matchPath, useLocation, useMatch, useNavigate, matchPath, useLocation, useMatch, useNavigate,
@@ -20,10 +21,13 @@ import { ContentActions, 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';
import DiscussionContext from '../common/context'; import DiscussionContext from '../common/context';
import { selectTopics as selectInContextTopics } from '../in-context-topics/data/selectors';
import fetchCourseTopicsV3 from '../in-context-topics/data/thunks';
import PostCommentsContext from '../post-comments/postCommentsContext'; import PostCommentsContext from '../post-comments/postCommentsContext';
import { clearRedirect } from '../posts/data'; import { clearRedirect } from '../posts/data';
import { threadsLoadingStatus } from '../posts/data/selectors'; import { threadsLoadingStatus } from '../posts/data/selectors';
import { selectTopics } from '../topics/data/selectors'; import { selectTopics } from '../topics/data/selectors';
import fetchCourseTopics from '../topics/data/thunks';
import tourCheckpoints from '../tours/constants'; import tourCheckpoints from '../tours/constants';
import selectTours from '../tours/data/selectors'; import selectTours from '../tours/data/selectors';
import { updateTourShowStatus } from '../tours/data/thunks'; import { updateTourShowStatus } from '../tours/data/thunks';
@@ -32,6 +36,7 @@ import { checkPermissions, discussionsPath } from '../utils';
import { ContentSelectors } from './constants'; import { ContentSelectors } from './constants';
import { import {
selectAreThreadsFiltered, selectAreThreadsFiltered,
selectDiscussionProvider,
selectEnableInContext, selectEnableInContext,
selectIsPostingEnabled, selectIsPostingEnabled,
selectIsUserLearner, selectIsUserLearner,
@@ -104,6 +109,21 @@ export function useCourseBlockData(courseId) {
}, [courseId, isEnrolled, courseStatus, isUserLearner]); }, [courseId, isEnrolled, courseStatus, isUserLearner]);
} }
export function useTopicsData(courseId, enableInContextSidebar) {
const dispatch = useDispatch();
const enableInContext = useSelector(selectEnableInContext);
const provider = useSelector(selectDiscussionProvider);
const topics = useSelector(enableInContext ? selectInContextTopics : selectTopics);
useEffect(() => {
if (isEmpty(topics) && provider) {
dispatch((enableInContext || enableInContextSidebar)
? fetchCourseTopicsV3(courseId)
: fetchCourseTopics(courseId));
}
}, [topics, provider, enableInContext, enableInContextSidebar]);
}
export function useRedirectToThread(courseId, enableInContextSidebar) { export function useRedirectToThread(courseId, enableInContextSidebar) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();

View File

@@ -16,7 +16,7 @@ import { ALL_ROUTES, DiscussionProvider, Routes as ROUTES } from '../../data/con
import DiscussionContext from '../common/context'; import DiscussionContext from '../common/context';
import ContentUnavailable from '../content-unavailable/ContentUnavailable'; import ContentUnavailable from '../content-unavailable/ContentUnavailable';
import { import {
useCourseBlockData, useCourseDiscussionData, useIsOnTablet, useRedirectToThread, useSidebarVisible, useCourseBlockData, useCourseDiscussionData, useIsOnTablet, useRedirectToThread, useSidebarVisible, useTopicsData,
} from '../data/hooks'; } from '../data/hooks';
import { selectDiscussionProvider, selectEnableInContext, selectIsUserLearner } from '../data/selectors'; import { selectDiscussionProvider, selectEnableInContext, selectIsUserLearner } from '../data/selectors';
import { EmptyLearners, EmptyTopics } from '../empty-posts'; import { EmptyLearners, EmptyTopics } from '../empty-posts';
@@ -62,6 +62,7 @@ const DiscussionsHome = () => {
useCourseDiscussionData(courseId); useCourseDiscussionData(courseId);
useRedirectToThread(courseId, enableInContextSidebar); useRedirectToThread(courseId, enableInContextSidebar);
useCourseBlockData(courseId); useCourseBlockData(courseId);
useTopicsData(courseId, enableInContextSidebar);
useFeedbackWrapper(); useFeedbackWrapper();
/* Display the content area if we are currently viewing/editing a post or creating one. /* Display the content area if we are currently viewing/editing a post or creating one.
If the window is larger than a particular size, show the sidebar for navigating between posts/topics. If the window is larger than a particular size, show the sidebar for navigating between posts/topics.

View File

@@ -27,10 +27,10 @@ import { fetchThreads } from '../posts/data/thunks';
import fetchCourseTopics from '../topics/data/thunks'; import fetchCourseTopics from '../topics/data/thunks';
import DiscussionsHome from './DiscussionsHome'; import DiscussionsHome from './DiscussionsHome';
import '../posts/data/__factories__/threads.factory';
import '../in-context-topics/data/__factories__/inContextTopics.factory';
import '../topics/data/__factories__/topics.factory';
import '../../components/NavigationBar/data/__factories__/navigationBar.factory'; import '../../components/NavigationBar/data/__factories__/navigationBar.factory';
import '../in-context-topics/data/__factories__/inContextTopics.factory';
import '../posts/data/__factories__/threads.factory';
import '../topics/data/__factories__/topics.factory';
const courseConfigApiUrl = getCourseConfigApiUrl(); const courseConfigApiUrl = getCourseConfigApiUrl();
let axiosMock; let axiosMock;
@@ -224,7 +224,7 @@ describe('DiscussionsHome', () => {
it('should display post editor form when click on add a post button in legacy topics view', async () => { it('should display post editor form when click on add a post button in legacy topics view', async () => {
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
enable_in_context: false, hasModerationPrivileges: true, isEmailVerified: true, enable_in_context: false, hasModerationPrivileges: true, isEmailVerified: true, provider: 'legacy',
}); });
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
await renderComponent(`/${courseId}/topics`); await renderComponent(`/${courseId}/topics`);

View File

@@ -1,6 +1,4 @@
import React, { import { useCallback, useEffect, useMemo } from 'react';
useCallback, useContext, useEffect, useMemo,
} from 'react';
import { Spinner } from '@openedx/paragon'; import { Spinner } from '@openedx/paragon';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -9,8 +7,7 @@ import { useDispatch, useSelector } from 'react-redux';
import SearchInfo from '../../components/SearchInfo'; import SearchInfo from '../../components/SearchInfo';
import { RequestStatus } from '../../data/constants'; import { RequestStatus } from '../../data/constants';
import DiscussionContext from '../common/context'; import { selectAreThreadsFiltered } from '../data/selectors';
import { selectAreThreadsFiltered, selectDiscussionProvider } from '../data/selectors';
import { clearFilter, clearSort } from '../posts/data/slices'; import { clearFilter, clearSort } from '../posts/data/slices';
import NoResults from '../posts/NoResults'; import NoResults from '../posts/NoResults';
import { handleKeyDown } from '../utils'; import { handleKeyDown } from '../utils';
@@ -19,7 +16,6 @@ import {
selectNonCoursewareTopics, selectTopicFilter, selectTopics, selectNonCoursewareTopics, selectTopicFilter, selectTopics,
} from './data/selectors'; } from './data/selectors';
import { setFilter } from './data/slices'; import { setFilter } from './data/slices';
import fetchCourseTopicsV3 from './data/thunks';
import { ArchivedBaseGroup, SectionBaseGroup, Topic } from './topic'; import { ArchivedBaseGroup, SectionBaseGroup, Topic } from './topic';
const TopicsList = () => { const TopicsList = () => {
@@ -71,20 +67,12 @@ const TopicsList = () => {
const TopicsView = () => { const TopicsView = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { courseId } = useContext(DiscussionContext);
const provider = useSelector(selectDiscussionProvider);
const topicFilter = useSelector(selectTopicFilter); const topicFilter = useSelector(selectTopicFilter);
const filteredTopics = useSelector(selectFilteredTopics); const filteredTopics = useSelector(selectFilteredTopics);
const loadingStatus = useSelector(selectLoadingStatus); const loadingStatus = useSelector(selectLoadingStatus);
const isPostsFiltered = useSelector(selectAreThreadsFiltered); const isPostsFiltered = useSelector(selectAreThreadsFiltered);
const topics = useSelector(selectTopics); const topics = useSelector(selectTopics);
useEffect(() => {
if (provider) {
dispatch(fetchCourseTopicsV3(courseId));
}
}, [provider]);
useEffect(() => { useEffect(() => {
if (isPostsFiltered) { if (isPostsFiltered) {
dispatch(clearFilter()); dispatch(clearFilter());

View File

@@ -1,4 +1,4 @@
import React, { import {
useCallback, useContext, useEffect, useMemo, useCallback, useContext, useEffect, useMemo,
} from 'react'; } from 'react';

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { Button, Spinner } from '@openedx/paragon'; import { Button, Spinner } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';

View File

@@ -14,5 +14,5 @@ export const selectLearnerSorting = () => state => state.learners.sortedBy;
export const selectLearnerNextPage = () => state => state.learners.nextPage; export const selectLearnerNextPage = () => state => state.learners.nextPage;
export const selectLearnerAvatar = author => state => ( export const selectLearnerAvatar = author => state => (
state.learners.learnerProfiles[author]?.profileImage?.imageUrlLarge state.learners.learnerProfiles[author]?.profileImage?.imageUrlSmall
); );

View File

@@ -9,7 +9,9 @@ import {
} from 'react-router-dom'; } from 'react-router-dom';
import { Factory } from 'rosie'; import { Factory } from 'rosie';
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform'; import {
camelCaseObject, getConfig, initializeMockApp, setConfig,
} from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
@@ -738,6 +740,20 @@ describe('ThreadView', () => {
expect(screen.queryByTestId('comment-comment-4')) expect(screen.queryByTestId('comment-comment-4'))
.toBeInTheDocument(); .toBeInTheDocument();
}); });
it('it show avatar for reply author when ENABLE_PROFILE_IMAGE is true', async () => {
setConfig({
...getConfig(),
ENABLE_PROFILE_IMAGE: 'true',
});
await waitFor(() => renderComponent(discussionPostId));
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
expect(comment).toBeInTheDocument();
const replyAuthorAvatar = within(comment).getAllByAltText('edx');
expect(replyAuthorAvatar.length).toBeGreaterThan(0);
});
}); });
describe('for question thread', () => { describe('for question thread', () => {

View File

@@ -46,7 +46,7 @@ const Comment = ({
const { const {
id, parentId, childCount, abuseFlagged, endorsed, threadId, endorsedAt, endorsedBy, endorsedByLabel, renderedBody, id, parentId, childCount, abuseFlagged, endorsed, threadId, endorsedAt, endorsedBy, endorsedByLabel, renderedBody,
voted, following, voteCount, authorLabel, author, createdAt, lastEdit, rawBody, closed, closedBy, closeReason, voted, following, voteCount, authorLabel, author, createdAt, lastEdit, rawBody, closed, closedBy, closeReason,
editByLabel, closedByLabel, users: postUsers, editByLabel, closedByLabel, users: commentUsers,
} = comment; } = comment;
const intl = useIntl(); const intl = useIntl();
const hasChildren = childCount > 0; const hasChildren = childCount > 0;
@@ -209,7 +209,7 @@ const Comment = ({
closed={closed} closed={closed}
createdAt={createdAt} createdAt={createdAt}
lastEdit={lastEdit} lastEdit={lastEdit}
postUsers={postUsers} commentUsers={commentUsers}
/> />
{isEditing ? ( {isEditing ? (
<CommentEditor <CommentEditor

View File

@@ -19,7 +19,7 @@ const CommentHeader = ({
closed, closed,
createdAt, createdAt,
lastEdit, lastEdit,
postUsers, commentUsers,
}) => { }) => {
const colorClass = AvatarOutlineAndLabelColors[authorLabel]; const colorClass = AvatarOutlineAndLabelColors[authorLabel];
const hasAnyAlert = useAlertBannerVisible({ const hasAnyAlert = useAlertBannerVisible({
@@ -31,7 +31,7 @@ const CommentHeader = ({
const authorAvatar = useSelector(selectAuthorAvatar(author)); const authorAvatar = useSelector(selectAuthorAvatar(author));
const profileImage = getConfig()?.ENABLE_PROFILE_IMAGE === 'true' const profileImage = getConfig()?.ENABLE_PROFILE_IMAGE === 'true'
? Object.values(postUsers ?? {})[0]?.profile?.image ? Object.values(commentUsers ?? {})[0]?.profile?.image
: null; : null;
return ( return (
@@ -43,7 +43,7 @@ const CommentHeader = ({
<Avatar <Avatar
className={`border-0 ml-0.5 mr-2.5 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`} className={`border-0 ml-0.5 mr-2.5 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`}
alt={author} alt={author}
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar} src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar?.imageUrlSmall}
style={{ style={{
width: '32px', width: '32px',
height: '32px', height: '32px',
@@ -72,7 +72,7 @@ CommentHeader.propTypes = {
editorUsername: PropTypes.string, editorUsername: PropTypes.string,
reason: PropTypes.string, reason: PropTypes.string,
}), }),
postUsers: PropTypes.shape({}).isRequired, commentUsers: PropTypes.shape({}).isRequired,
}; };
CommentHeader.defaultProps = { CommentHeader.defaultProps = {

View File

@@ -22,7 +22,7 @@ const defaultProps = {
closed: false, closed: false,
createdAt: '2025-09-23T10:00:00Z', createdAt: '2025-09-23T10:00:00Z',
lastEdit: null, lastEdit: null,
postUsers: { commentUsers: {
'test-user': { 'test-user': {
profile: { image: { hasImage: true, imageUrlSmall: 'http://avatar.test/img.png' } }, profile: { image: { hasImage: true, imageUrlSmall: 'http://avatar.test/img.png' } },
}, },

View File

@@ -5,6 +5,7 @@ import { Avatar, useToggle } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import * as timeago from 'timeago.js'; import * as timeago from 'timeago.js';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import HTMLLoader from '../../../../components/HTMLLoader'; import HTMLLoader from '../../../../components/HTMLLoader';
@@ -24,7 +25,7 @@ import CommentEditor from './CommentEditor';
const Reply = ({ responseId }) => { const Reply = ({ responseId }) => {
timeago.register('time-locale', timeLocale); timeago.register('time-locale', timeLocale);
const { const {
id, abuseFlagged, author, authorLabel, endorsed, lastEdit, closed, closedBy, id, abuseFlagged, author, authorLabel, endorsed, lastEdit, closed, closedBy, users: replyUsers,
closeReason, createdAt, threadId, parentId, rawBody, renderedBody, editByLabel, closedByLabel, closeReason, createdAt, threadId, parentId, rawBody, renderedBody, editByLabel, closedByLabel,
} = useSelector(selectCommentOrResponseById(responseId)); } = useSelector(selectCommentOrResponseById(responseId));
const intl = useIntl(); const intl = useIntl();
@@ -78,6 +79,10 @@ const Reply = ({ responseId }) => {
[ContentActions.REPORT]: handleAbusedFlag, [ContentActions.REPORT]: handleAbusedFlag,
}), [handleEditContent, handleReplyEndorse, showDeleteConfirmation, handleAbusedFlag]); }), [handleEditContent, handleReplyEndorse, showDeleteConfirmation, handleAbusedFlag]);
const profileImage = getConfig()?.ENABLE_PROFILE_IMAGE === 'true'
? Object.values(replyUsers ?? {})[0]?.profile?.image
: null;
return ( return (
<div className="d-flex flex-column mt-2.5 " data-testid={`reply-${id}`} role="listitem"> <div className="d-flex flex-column mt-2.5 " data-testid={`reply-${id}`} role="listitem">
<Confirmation <Confirmation
@@ -123,7 +128,7 @@ const Reply = ({ responseId }) => {
<Avatar <Avatar
className={`ml-0.5 mt-0.5 border-0 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`} className={`ml-0.5 mt-0.5 border-0 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`}
alt={author} alt={author}
src={authorAvatar?.imageUrlSmall} src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar?.imageUrlSmall}
style={{ style={{
width: '32px', width: '32px',
height: '32px', height: '32px',

View File

@@ -30,6 +30,16 @@ Factory.define('comment')
parent_id: null, parent_id: null,
children: [], children: [],
abuse_flagged_any_user: false, abuse_flagged_any_user: false,
users: {
edx: {
profile: {
image: {
hasImage: true,
imageUrlSmall: 'http://test.site/default-avatar-small.png',
},
},
},
},
}); });
Factory.define('commentsResult') Factory.define('commentsResult')

View File

@@ -1,19 +1,11 @@
import React, { import React, { useCallback, useContext, useMemo } from 'react';
useCallback, useContext, useEffect, useMemo,
} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import isEmpty from 'lodash/isEmpty';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import SearchInfo from '../../components/SearchInfo'; import SearchInfo from '../../components/SearchInfo';
import { selectCurrentCategoryGrouping, selectTopicsUnderCategory } from '../../data/selectors'; import { selectCurrentCategoryGrouping, selectTopicsUnderCategory } from '../../data/selectors';
import DiscussionContext from '../common/context'; import DiscussionContext from '../common/context';
import { selectEnableInContext } from '../data/selectors';
import { selectTopics as selectInContextTopics } from '../in-context-topics/data/selectors';
import fetchCourseTopicsV3 from '../in-context-topics/data/thunks';
import { selectTopics } from '../topics/data/selectors';
import fetchCourseTopics from '../topics/data/thunks';
import { handleKeyDown } from '../utils'; import { handleKeyDown } from '../utils';
import { selectAllThreadsIds, selectTopicThreadsIds } from './data/selectors'; import { selectAllThreadsIds, selectTopicThreadsIds } from './data/selectors';
import { setSearchQuery } from './data/slices'; import { setSearchQuery } from './data/slices';
@@ -51,27 +43,12 @@ CategoryPostsList.propTypes = {
}; };
const PostsView = () => { const PostsView = () => {
const { const { topicId, category } = useContext(DiscussionContext);
topicId,
category,
courseId,
enableInContextSidebar,
} = useContext(DiscussionContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
const enableInContext = useSelector(selectEnableInContext);
const searchString = useSelector(({ threads }) => threads.filters.search); const searchString = useSelector(({ threads }) => threads.filters.search);
const resultsFound = useSelector(({ threads }) => threads.totalThreads); const resultsFound = useSelector(({ threads }) => threads.totalThreads);
const textSearchRewrite = useSelector(({ threads }) => threads.textSearchRewrite); const textSearchRewrite = useSelector(({ threads }) => threads.textSearchRewrite);
const loadingStatus = useSelector(({ threads }) => threads.status); const loadingStatus = useSelector(({ threads }) => threads.status);
const topics = useSelector(enableInContext ? selectInContextTopics : selectTopics);
useEffect(() => {
if (isEmpty(topics)) {
dispatch((enableInContext || enableInContextSidebar)
? fetchCourseTopicsV3(courseId)
: fetchCourseTopics(courseId));
}
}, [topics]);
const handleOnClear = useCallback(() => { const handleOnClear = useCallback(() => {
dispatch(setSearchQuery('')); dispatch(setSearchQuery(''));

View File

@@ -1,5 +1,3 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
@@ -232,7 +230,7 @@ describe('PostsView', () => {
test('test that the cohorts filter works', async () => { test('test that the cohorts filter works', async () => {
await act(async () => { await act(async () => {
fireEvent.click(screen.getByLabelText('Cohort 1')); fireEvent.click(screen.getByLabelText('cohort 1'));
}); });
dropDownButton = screen.getByRole('button', { dropDownButton = screen.getByRole('button', {
@@ -280,7 +278,7 @@ describe('PostsView', () => {
queryParam: { group_id: undefined }, queryParam: { group_id: undefined },
}, },
{ {
label: 'Cohort 1', label: 'cohort 1',
queryParam: { group_id: 'cohort-1' }, queryParam: { group_id: 'cohort-1' },
}, },
])( ])(

View File

@@ -165,7 +165,7 @@ const PostFilterBar = () => {
<ActionItem <ActionItem
key={cohort.id} key={cohort.id}
id={toString(cohort.id)} id={toString(cohort.id)}
label={capitalize(cohort.name)} label={cohort.name}
value={toString(cohort.id)} value={toString(cohort.id)}
selected={currentFilters.cohort} selected={currentFilters.cohort}
/> />

View File

@@ -1,19 +1,15 @@
import React, { import { useCallback, useEffect, useMemo } from 'react';
useCallback, useContext, useEffect, useMemo,
} from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import SearchInfo from '../../components/SearchInfo'; import SearchInfo from '../../components/SearchInfo';
import { RequestStatus } from '../../data/constants'; import { RequestStatus } from '../../data/constants';
import DiscussionContext from '../common/context';
import { selectDiscussionProvider } from '../data/selectors'; import { selectDiscussionProvider } from '../data/selectors';
import NoResults from '../posts/NoResults'; import NoResults from '../posts/NoResults';
import { handleKeyDown } from '../utils'; import { handleKeyDown } from '../utils';
import { selectCategories, selectNonCoursewareTopics, selectTopicFilter } from './data/selectors'; import { selectCategories, selectNonCoursewareTopics, selectTopicFilter } from './data/selectors';
import { setFilter, setTopicsCount } from './data/slices'; import { setFilter, setTopicsCount } from './data/slices';
import fetchCourseTopics from './data/thunks';
import LegacyTopicGroup from './topic-group/LegacyTopicGroup'; import LegacyTopicGroup from './topic-group/LegacyTopicGroup';
import Topic from './topic-group/topic/Topic'; import Topic from './topic-group/topic/Topic';
import countFilteredTopics from './utils'; import countFilteredTopics from './utils';
@@ -64,19 +60,11 @@ const TopicsView = () => {
const topicsSelector = useSelector(({ topics }) => topics); const topicsSelector = useSelector(({ topics }) => topics);
const filteredTopicsCount = useSelector(({ topics }) => topics.results.count); const filteredTopicsCount = useSelector(({ topics }) => topics.results.count);
const loadingStatus = useSelector(({ topics }) => topics.status); const loadingStatus = useSelector(({ topics }) => topics.status);
const { courseId } = useContext(DiscussionContext);
const handleOnClear = useCallback(() => { const handleOnClear = useCallback(() => {
dispatch(setFilter('')); dispatch(setFilter(''));
}, []); }, []);
useEffect(() => {
// Don't load till the provider information is available
if (provider) {
dispatch(fetchCourseTopics(courseId));
}
}, [provider]);
useEffect(() => { useEffect(() => {
const count = countFilteredTopics(topicsSelector, provider); const count = countFilteredTopics(topicsSelector, provider);
dispatch(setTopicsCount(count)); dispatch(setTopicsCount(count));

View File

@@ -1,5 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { mergeConfig } from '@edx/frontend-platform';
import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom/extend-expect';
import 'babel-polyfill'; import 'babel-polyfill';
@@ -66,3 +68,10 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({
})); }));
jest.setTimeout(1000000); jest.setTimeout(1000000);
mergeConfig({
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
LEARNER_FEEDBACK_URL: process.env.LEARNER_FEEDBACK_URL,
STAFF_FEEDBACK_URL: process.env.STAFF_FEEDBACK_URL,
ENABLE_PROFILE_IMAGE: process.env.ENABLE_PROFILE_IMAGE || 'false',
}, 'DiscussionsConfig');