Compare commits

...

9 Commits

Author SHA1 Message Date
Brian Smith
e1383d1e65 fix(deps): regenerate package-lock.json
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-02-13 17:14:44 -05:00
bydawen
f7b5615660 fix: matching styles for report/delete post buttons (#794)
* fix: matching styles for report/delete post buttons

* feat: add tests for the Post.jsx
2026-02-10 09:17:20 -06:00
bydawen
0d5e3b8a1c fix: add space gap between modals buttons + fixed reply/post/comment sections text colors (#805) 2026-01-28 12:43:40 -06:00
Feanil Patel
4e90bbc756 Merge pull request #793 from raccoongang/buhaienko/fix/remove-primary-color-from-discussion-post-and-messages-text
fix: remove primary color from discussion post and messages text
2026-01-16 13:51:16 -05:00
oleksandr.buhaienko
b4dcaca660 fix: remove primary color from discussion post and messages text 2026-01-16 19:22:25 +02:00
Awais Ansari
5defe5cbd4 fix: profile picture is not showing in forum responses (#849) 2026-01-02 14:57:49 +05:00
Awais Ansari
911b8b3fc5 fix: cohort name difference in filter and post view (#841)
* fix: cohort name difference in filter and post view

* test: fix cohort test cases

---------

Co-authored-by: Awais  Ansari <awais.ansari@A006-01824.local>
2025-12-16 20:07:40 +05:00
Awais Ansari
4917da3245 fix: fetch topics on MFE load (#840)
* fix: added fetch topic API call in learners tab

* refactor: fetch topics on MFE load

* test: added legacy param in legacy topic test cases

---------

Co-authored-by: Awais  Ansari <awais.ansari@A006-01824.local>
2025-12-16 19:39:36 +05:00
renovate[bot]
e5388690b2 fix(deps): update dependency core-js to v3.47.0 2025-12-10 12:05:24 +05:00
25 changed files with 3885 additions and 1458 deletions

5008
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@
"@tinymce/tinymce-react": "5.1.1", "@tinymce/tinymce-react": "5.1.1",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"classnames": "2.5.1", "classnames": "2.5.1",
"core-js": "3.21.1", "core-js": "3.47.0",
"dompurify": "^2.4.3", "dompurify": "^2.4.3",
"formik": "2.4.9", "formik": "2.4.9",
"lodash.snakecase": "4.1.1", "lodash.snakecase": "4.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

@@ -75,7 +75,7 @@ const HoverCard = ({
const actionFunction = actionHandlers[endorseIcons.action]; const actionFunction = actionHandlers[endorseIcons.action];
actionFunction(); actionFunction();
}} }}
className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'} className="text-primary"
size="sm" size="sm"
alt="Endorse" alt="Endorse"
/> />

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
@@ -226,7 +226,7 @@ const Comment = ({
/> />
) : ( ) : (
<HTMLLoader <HTMLLoader
cssClassName="comment-body html-loader text-break mt-14px font-style text-primary-500" cssClassName="comment-body html-loader text-break mt-14px font-style text-gray-700"
componentId="comment" componentId="comment"
htmlNode={renderedBody} htmlNode={renderedBody}
testId={id} testId={id}

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',
@@ -168,7 +173,7 @@ const Reply = ({ responseId }) => {
<HTMLLoader <HTMLLoader
componentId="reply" componentId="reply"
htmlNode={renderedBody} htmlNode={renderedBody}
cssClassName="html-loader text-break font-style text-primary-500" cssClassName="html-loader text-break font-style text-gray-700"
testId={id} testId={id}
/> />
)} )}

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

@@ -143,6 +143,7 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
onClose={hideDeleteConfirmation} onClose={hideDeleteConfirmation}
confirmAction={handleDeleteConfirmation} confirmAction={handleDeleteConfirmation}
closeButtonVariant="tertiary" closeButtonVariant="tertiary"
confirmButtonVariant="danger"
confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)} confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)}
/> />
{!abuseFlagged && ( {!abuseFlagged && (
@@ -152,7 +153,6 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
description={intl.formatMessage(messages.reportPostDescription)} description={intl.formatMessage(messages.reportPostDescription)}
onClose={hideReportConfirmation} onClose={hideReportConfirmation}
confirmAction={handleReportConfirmation} confirmAction={handleReportConfirmation}
confirmButtonVariant="danger"
/> />
)} )}
<HoverCard <HoverCard
@@ -189,7 +189,7 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
title={title} title={title}
postUsers={postUsers} postUsers={postUsers}
/> />
<div className="d-flex mt-14px text-break font-style text-primary-500"> <div className="d-flex mt-14px text-break font-style text-gray-700">
<HTMLLoader htmlNode={renderedBody} componentId="post" cssClassName="html-loader w-100" testId={postId} /> <HTMLLoader htmlNode={renderedBody} componentId="post" cssClassName="html-loader w-100" testId={postId} />
</div> </div>
{(topicContext || topic) && ( {(topicContext || topic) && (

View File

@@ -0,0 +1,157 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { useSelector } from 'react-redux';
import Post from './Post';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => jest.fn(),
useSelector: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({ pathname: '/test' }),
useNavigate: () => jest.fn(),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
useIntl: () => ({
formatMessage: (msg) => ((msg && msg.defaultMessage) ? msg.defaultMessage : 'test-message'),
}),
defineMessages: (msgs) => msgs,
}));
jest.mock('@openedx/paragon', () => {
const actual = jest.requireActual('@openedx/paragon');
// eslint-disable-next-line global-require
const PropTypes = require('prop-types');
const MockHyperlink = ({ children }) => <div>{children}</div>;
MockHyperlink.propTypes = { children: PropTypes.node.isRequired };
return {
...actual,
Hyperlink: MockHyperlink,
useToggle: actual.useToggle,
};
});
jest.mock('../../common', () => {
// eslint-disable-next-line global-require
const PropTypes = require('prop-types');
const MockConfirmation = ({ confirmButtonVariant, isOpen }) => {
if (!isOpen) { return null; }
return (
<div data-testid="mock-confirmation" data-variant={confirmButtonVariant}>
Mock Confirmation
</div>
);
};
MockConfirmation.propTypes = {
confirmButtonVariant: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
};
// eslint-disable-next-line react/prop-types
const MockAlertBanner = () => <div />;
return {
Confirmation: MockConfirmation,
AlertBanner: MockAlertBanner,
};
});
jest.mock('./PostHeader', () => function MockPostHeader() { return <div>PostHeader</div>; });
jest.mock('./PostFooter', () => function MockPostFooter() { return <div>PostFooter</div>; });
jest.mock('./ClosePostReasonModal', () => function MockCloseModal() { return <div />; });
jest.mock('../../../components/HTMLLoader', () => function MockLoader() { return <div>Body Content</div>; });
jest.mock('../../common/HoverCard', () => {
// eslint-disable-next-line global-require
const { ContentActions } = require('../../../data/constants');
// eslint-disable-next-line global-require
const PropTypes = require('prop-types');
const MockHoverCard = ({ actionHandlers }) => (
<div>
<button
type="button"
data-testid="trigger-delete"
onClick={() => actionHandlers[ContentActions.DELETE] && actionHandlers[ContentActions.DELETE]()}
>
Delete Post
</button>
<button
type="button"
data-testid="trigger-report"
onClick={() => actionHandlers[ContentActions.REPORT] && actionHandlers[ContentActions.REPORT]()}
>
Report Post
</button>
</div>
);
MockHoverCard.propTypes = {
actionHandlers: PropTypes.shape({}).isRequired,
};
return MockHoverCard;
});
describe('Post Component - Delete/Report Confirmation', () => {
const mockPostId = '123';
beforeEach(() => {
useSelector.mockReturnValue({
topicId: 'topic-1',
abuseFlagged: false,
closed: false,
pinned: false,
voted: false,
following: false,
author: {},
title: 'Test Post',
renderedBody: '<div>Hello</div>',
users: {},
});
});
const renderPost = () => {
// eslint-disable-next-line global-require
const DiscussionContext = require('../../common/context').default;
return render(
<DiscussionContext.Provider value={{ postId: mockPostId, enableInContextSidebar: false, courseId: 'course-1' }}>
<Post
handleAddResponseButton={jest.fn()}
openRestrictionDialogue={jest.fn()}
/>
</DiscussionContext.Provider>,
);
};
it('passes "danger" variant to Confirmation modal when deleting a post', () => {
renderPost();
const deleteBtn = screen.getByTestId('trigger-delete');
fireEvent.click(deleteBtn);
const confirmation = screen.getByTestId('mock-confirmation');
expect(confirmation).toHaveAttribute('data-variant', 'danger');
});
it('does NOT pass "danger" variant to Confirmation modal when reporting a post', () => {
renderPost();
const reportBtn = screen.getByTestId('trigger-report');
fireEvent.click(reportBtn);
const confirmation = screen.getByTestId('mock-confirmation');
expect(confirmation).not.toHaveAttribute('data-variant', 'danger');
});
});

View File

@@ -126,7 +126,7 @@ const PostHeader = ({
</div> </div>
) : ( ) : (
<h5 <h5
className="mb-0 font-style text-primary-500" className="mb-0 font-style"
style={{ lineHeight: '21px' }} style={{ lineHeight: '21px' }}
aria-level="1" aria-level="1"
tabIndex="-1" tabIndex="-1"

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