Compare commits

..

16 Commits

Author SHA1 Message Date
Awais Ansari
e216d847eb feat: added cohort update option in post edit 2025-12-18 16:39:24 +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
edX requirements bot
cefc8d9d35 chore: enable github action auto update in dependabot.yml (#737) 2025-12-10 10:40:23 +05:00
renovate[bot]
f5c5913d3f chore(deps): update dependency glob to v7.2.3 2025-12-09 20:50:15 +00:00
renovate[bot]
687dae6b21 fix(deps): update dependency tinymce to v5.10.9 (#587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:26:31 +00:00
renovate[bot]
b812b96d77 fix(deps): update dependency formik to v2.4.9 2025-12-09 14:59:27 +00:00
renovate[bot]
142abd8dd4 fix(deps): update dependency @edx/openedx-atlas to ^0.7.0 (#815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 14:47:28 +00:00
renovate[bot]
ece4432f58 chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#747)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 14:17:40 +00:00
renovate[bot]
c599046813 chore(deps): update dependency axios to ^0.30.0 (#780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 14:13:55 +00:00
renovate[bot]
c323c80bc8 chore(deps): update actions/checkout action to v6 (#833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 19:02:49 +05:00
renovate[bot]
37cec76dcb chore(deps): update dependency @openedx/paragon to v23.18.1 (#814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 18:50:31 +05:00
Awais Ansari
00440fc15a chore: update header version to 8 (#829) 2025-11-14 15:31:25 +05:00
Tobias Macey
a4826ae62d fix: prevent generatePath error when author is invalid in AuthorLabel (#821)
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-13 16:08:51 -05:00
renovate[bot]
16c49b2404 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-13 14:36:31 -05:00
15 changed files with 1191 additions and 2961 deletions

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
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@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Nodejs

4039
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": "^6.2.0",
"@edx/frontend-component-header": "^8.1.0",
"@edx/frontend-platform": "^8.3.3",
"@edx/openedx-atlas": "^0.6.0",
"@edx/openedx-atlas": "^0.7.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.21.1",
"core-js": "3.47.0",
"dompurify": "^2.4.3",
"formik": "2.4.5",
"formik": "2.4.9",
"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.7",
"tinymce": "5.10.9",
"yup": "0.32.11"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/browserslist-config": "1.5.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.28.0",
"axios": "^0.30.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.0",
"glob": "7.2.3",
"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?.imageUrlSmall
state.learners.learnerProfiles[author]?.profileImage?.imageUrlLarge
);

View File

@@ -9,9 +9,7 @@ import {
} from 'react-router-dom';
import { Factory } from 'rosie';
import {
camelCaseObject, getConfig, initializeMockApp, setConfig,
} from '@edx/frontend-platform';
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
@@ -740,20 +738,6 @@ 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: commentUsers,
editByLabel, closedByLabel, users: postUsers,
} = comment;
const intl = useIntl();
const hasChildren = childCount > 0;
@@ -209,7 +209,7 @@ const Comment = ({
closed={closed}
createdAt={createdAt}
lastEdit={lastEdit}
commentUsers={commentUsers}
postUsers={postUsers}
/>
{isEditing ? (
<CommentEditor

View File

@@ -19,7 +19,7 @@ const CommentHeader = ({
closed,
createdAt,
lastEdit,
commentUsers,
postUsers,
}) => {
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(commentUsers ?? {})[0]?.profile?.image
? Object.values(postUsers ?? {})[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?.imageUrlSmall}
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar}
style={{
width: '32px',
height: '32px',
@@ -72,7 +72,7 @@ CommentHeader.propTypes = {
editorUsername: PropTypes.string,
reason: PropTypes.string,
}),
commentUsers: PropTypes.shape({}).isRequired,
postUsers: PropTypes.shape({}).isRequired,
};
CommentHeader.defaultProps = {

View File

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

View File

@@ -5,7 +5,6 @@ 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';
@@ -25,7 +24,7 @@ import CommentEditor from './CommentEditor';
const Reply = ({ responseId }) => {
timeago.register('time-locale', timeLocale);
const {
id, abuseFlagged, author, authorLabel, endorsed, lastEdit, closed, closedBy, users: replyUsers,
id, abuseFlagged, author, authorLabel, endorsed, lastEdit, closed, closedBy,
closeReason, createdAt, threadId, parentId, rawBody, renderedBody, editByLabel, closedByLabel,
} = useSelector(selectCommentOrResponseById(responseId));
const intl = useIntl();
@@ -79,10 +78,6 @@ 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
@@ -128,7 +123,7 @@ const Reply = ({ responseId }) => {
<Avatar
className={`ml-0.5 mt-0.5 border-0 ${colorClass ? `outline-${colorClass}` : 'outline-anonymous'}`}
alt={author}
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar?.imageUrlSmall}
src={authorAvatar?.imageUrlSmall}
style={{
width: '32px',
height: '32px',

View File

@@ -30,16 +30,6 @@ 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,
cohort,
groupId,
anonymous,
anonymousToPeers,
notifyAllLearners,
@@ -114,7 +114,7 @@ export const postThread = async (
following,
anonymous,
anonymousToPeers,
groupId: cohort,
groupId,
enableInContextSidebar,
notifyAllLearners,
captchaToken: recaptchaToken,
@@ -154,6 +154,7 @@ export const updateThread = async (threadId, {
pinned,
editReasonCode,
closeReasonCode,
groupId,
} = {}) => {
const url = `${getThreadsApiUrl()}${threadId}/`;
const patchData = snakeCaseObject({
@@ -169,6 +170,7 @@ 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,
cohort,
groupId,
enableInContextSidebar,
notifyAllLearners,
recaptchaToken,
@@ -224,12 +224,12 @@ export function createNewThread({
following,
anonymous,
anonymousToPeers,
cohort,
groupId,
notifyAllLearners,
recaptchaToken,
}));
const data = await postThread(courseId, topicId, type, title, content, {
cohort,
groupId,
following,
anonymous,
anonymousToPeers,
@@ -252,7 +252,8 @@ export function createNewThread({
}
export function updateExistingThread(threadId, {
flagged, voted, read, topicId, type, title, content, following, closed, pinned, closeReasonCode, editReasonCode,
flagged, voted, read, topicId, type, title, content, following,
closed, pinned, closeReasonCode, editReasonCode, groupId,
}) {
return async (dispatch) => {
try {
@@ -270,6 +271,7 @@ export function updateExistingThread(threadId, {
pinned,
editReasonCode,
closeReasonCode,
groupId,
}));
const data = await updateThread(threadId, {
flagged,
@@ -284,6 +286,7 @@ 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?.cohort || 'default',
cohort: post?.groupId || 'default',
editReasonCode: post?.lastEdit?.reasonCode || (
userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined
),
@@ -175,6 +175,8 @@ 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');
@@ -196,10 +198,9 @@ 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,
@@ -209,7 +210,7 @@ const PostEditor = ({
following: values.follow,
anonymous: allowAnonymous ? values.anonymous : undefined,
anonymousToPeers: allowAnonymousToPeers ? values.anonymousToPeers : undefined,
cohort,
groupId,
enableInContextSidebar,
notifyAllLearners: values.notifyAllLearners,
...(shouldRequireCaptcha && recaptchaToken ? { recaptchaToken } : {}),

View File

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