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
15 changed files with 2960 additions and 1190 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,7 +12,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Nodejs

4037
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -14,5 +14,5 @@ export const selectLearnerSorting = () => state => state.learners.sortedBy;
export const selectLearnerNextPage = () => state => state.learners.nextPage;
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';
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 { AppProvider } from '@edx/frontend-platform/react';
@@ -738,6 +740,20 @@ describe('ThreadView', () => {
expect(screen.queryByTestId('comment-comment-4'))
.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', () => {

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ const defaultProps = {
closed: false,
createdAt: '2025-09-23T10:00:00Z',
lastEdit: null,
postUsers: {
commentUsers: {
'test-user': {
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 * as timeago from 'timeago.js';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import HTMLLoader from '../../../../components/HTMLLoader';
@@ -24,7 +25,7 @@ import CommentEditor from './CommentEditor';
const Reply = ({ responseId }) => {
timeago.register('time-locale', timeLocale);
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,
} = useSelector(selectCommentOrResponseById(responseId));
const intl = useIntl();
@@ -78,6 +79,10 @@ const Reply = ({ responseId }) => {
[ContentActions.REPORT]: handleAbusedFlag,
}), [handleEditContent, handleReplyEndorse, showDeleteConfirmation, handleAbusedFlag]);
const profileImage = getConfig()?.ENABLE_PROFILE_IMAGE === 'true'
? Object.values(replyUsers ?? {})[0]?.profile?.image
: null;
return (
<div className="d-flex flex-column mt-2.5 " data-testid={`reply-${id}`} role="listitem">
<Confirmation
@@ -123,7 +128,7 @@ const Reply = ({ responseId }) => {
<Avatar
className={`ml-0.5 mt-0.5 border-0 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`}
alt={author}
src={authorAvatar?.imageUrlSmall}
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar?.imageUrlSmall}
style={{
width: '32px',
height: '32px',

View File

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

View File

@@ -97,7 +97,7 @@ export const postThread = async (
content,
{
following,
groupId,
cohort,
anonymous,
anonymousToPeers,
notifyAllLearners,
@@ -114,7 +114,7 @@ export const postThread = async (
following,
anonymous,
anonymousToPeers,
groupId,
groupId: cohort,
enableInContextSidebar,
notifyAllLearners,
captchaToken: recaptchaToken,
@@ -154,7 +154,6 @@ export const updateThread = async (threadId, {
pinned,
editReasonCode,
closeReasonCode,
groupId,
} = {}) => {
const url = `${getThreadsApiUrl()}${threadId}/`;
const patchData = snakeCaseObject({
@@ -170,7 +169,6 @@ export const updateThread = async (threadId, {
pinned,
editReasonCode,
closeReasonCode,
groupId,
});
const { data } = await getAuthenticatedHttpClient()
.patch(url, patchData, { headers: { 'Content-Type': 'application/merge-patch+json' } });

View File

@@ -208,7 +208,7 @@ export function createNewThread({
following,
anonymous,
anonymousToPeers,
groupId,
cohort,
enableInContextSidebar,
notifyAllLearners,
recaptchaToken,
@@ -224,12 +224,12 @@ export function createNewThread({
following,
anonymous,
anonymousToPeers,
groupId,
cohort,
notifyAllLearners,
recaptchaToken,
}));
const data = await postThread(courseId, topicId, type, title, content, {
groupId,
cohort,
following,
anonymous,
anonymousToPeers,
@@ -252,8 +252,7 @@ export function createNewThread({
}
export function updateExistingThread(threadId, {
flagged, voted, read, topicId, type, title, content, following,
closed, pinned, closeReasonCode, editReasonCode, groupId,
flagged, voted, read, topicId, type, title, content, following, closed, pinned, closeReasonCode, editReasonCode,
}) {
return async (dispatch) => {
try {
@@ -271,7 +270,6 @@ export function updateExistingThread(threadId, {
pinned,
editReasonCode,
closeReasonCode,
groupId,
}));
const data = await updateThread(threadId, {
flagged,
@@ -286,7 +284,6 @@ export function updateExistingThread(threadId, {
pinned,
editReasonCode,
closeReasonCode,
groupId,
});
dispatch(updateThreadSuccess(camelCaseObject(data)));
} catch (error) {

View File

@@ -140,7 +140,7 @@ const PostEditor = ({
notifyAllLearners: false,
anonymous: allowAnonymous ? false : undefined,
anonymousToPeers: allowAnonymousToPeers ? false : undefined,
cohort: post?.groupId || 'default',
cohort: post?.cohort || 'default',
editReasonCode: post?.lastEdit?.reasonCode || (
userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined
),
@@ -175,8 +175,6 @@ const PostEditor = ({
const submitForm = useCallback(async (values, { resetForm }) => {
let recaptchaToken;
const groupId = canSelectCohort(values.topic) ? selectedCohort(values.cohort) : undefined;
if (shouldRequireCaptcha && executeRecaptcha) {
try {
recaptchaToken = await executeRecaptcha('submit_post');
@@ -198,9 +196,10 @@ const PostEditor = ({
title: values.title,
content: values.comment,
editReasonCode: values.editReasonCode || undefined,
groupId,
}));
} else {
const cohort = canSelectCohort(values.topic) ? selectedCohort(values.cohort) : undefined;
await dispatchSubmit(createNewThread({
courseId,
topicId: values.topic,
@@ -210,7 +209,7 @@ const PostEditor = ({
following: values.follow,
anonymous: allowAnonymous ? values.anonymous : undefined,
anonymousToPeers: allowAnonymousToPeers ? values.anonymousToPeers : undefined,
groupId,
cohort,
enableInContextSidebar,
notifyAllLearners: values.notifyAllLearners,
...(shouldRequireCaptcha && recaptchaToken ? { recaptchaToken } : {}),

View File

@@ -1,5 +1,7 @@
import PropTypes from 'prop-types';
import { mergeConfig } from '@edx/frontend-platform';
import '@testing-library/jest-dom/extend-expect';
import 'babel-polyfill';
@@ -66,3 +68,10 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({
}));
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');