Compare commits
1 Commits
master
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d31288ca13 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Nodejs
|
- name: Setup Nodejs
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
5008
package-lock.json
generated
5008
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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.47.0",
|
"core-js": "3.21.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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={cohort.name}
|
label={capitalize(cohort.name)}
|
||||||
value={toString(cohort.id)}
|
value={toString(cohort.id)}
|
||||||
selected={selectedFilters.cohort}
|
selected={selectedFilters.cohort}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const HoverCard = ({
|
|||||||
const actionFunction = actionHandlers[endorseIcons.action];
|
const actionFunction = actionHandlers[endorseIcons.action];
|
||||||
actionFunction();
|
actionFunction();
|
||||||
}}
|
}}
|
||||||
className="text-primary"
|
className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'}
|
||||||
size="sm"
|
size="sm"
|
||||||
alt="Endorse"
|
alt="Endorse"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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,
|
||||||
@@ -21,13 +20,10 @@ 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';
|
||||||
@@ -36,7 +32,6 @@ import { checkPermissions, discussionsPath } from '../utils';
|
|||||||
import { ContentSelectors } from './constants';
|
import { ContentSelectors } from './constants';
|
||||||
import {
|
import {
|
||||||
selectAreThreadsFiltered,
|
selectAreThreadsFiltered,
|
||||||
selectDiscussionProvider,
|
|
||||||
selectEnableInContext,
|
selectEnableInContext,
|
||||||
selectIsPostingEnabled,
|
selectIsPostingEnabled,
|
||||||
selectIsUserLearner,
|
selectIsUserLearner,
|
||||||
@@ -109,21 +104,6 @@ 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();
|
||||||
|
|||||||
@@ -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, useTopicsData,
|
useCourseBlockData, useCourseDiscussionData, useIsOnTablet, useRedirectToThread, useSidebarVisible,
|
||||||
} 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,7 +62,6 @@ 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.
|
||||||
|
|||||||
@@ -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 '../../components/NavigationBar/data/__factories__/navigationBar.factory';
|
|
||||||
import '../in-context-topics/data/__factories__/inContextTopics.factory';
|
|
||||||
import '../posts/data/__factories__/threads.factory';
|
import '../posts/data/__factories__/threads.factory';
|
||||||
|
import '../in-context-topics/data/__factories__/inContextTopics.factory';
|
||||||
import '../topics/data/__factories__/topics.factory';
|
import '../topics/data/__factories__/topics.factory';
|
||||||
|
import '../../components/NavigationBar/data/__factories__/navigationBar.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, provider: 'legacy',
|
enable_in_context: false, hasModerationPrivileges: true, isEmailVerified: true,
|
||||||
});
|
});
|
||||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||||
await renderComponent(`/${courseId}/topics`);
|
await renderComponent(`/${courseId}/topics`);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import 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';
|
||||||
@@ -7,7 +9,8 @@ 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 { selectAreThreadsFiltered } from '../data/selectors';
|
import DiscussionContext from '../common/context';
|
||||||
|
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';
|
||||||
@@ -16,6 +19,7 @@ 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 = () => {
|
||||||
@@ -67,12 +71,20 @@ 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());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {
|
import React, {
|
||||||
useCallback, useContext, useEffect, useMemo,
|
useCallback, useContext, useEffect, useMemo,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import React, { 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';
|
||||||
|
|||||||
@@ -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?.imageUrlSmall
|
state.learners.learnerProfiles[author]?.profileImage?.imageUrlLarge
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import {
|
|||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { Factory } from 'rosie';
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
import {
|
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||||
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';
|
||||||
|
|
||||||
@@ -740,20 +738,6 @@ 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', () => {
|
||||||
|
|||||||
@@ -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: commentUsers,
|
editByLabel, closedByLabel, users: postUsers,
|
||||||
} = 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}
|
||||||
commentUsers={commentUsers}
|
postUsers={postUsers}
|
||||||
/>
|
/>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<CommentEditor
|
<CommentEditor
|
||||||
@@ -226,7 +226,7 @@ const Comment = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<HTMLLoader
|
<HTMLLoader
|
||||||
cssClassName="comment-body html-loader text-break mt-14px font-style text-gray-700"
|
cssClassName="comment-body html-loader text-break mt-14px font-style text-primary-500"
|
||||||
componentId="comment"
|
componentId="comment"
|
||||||
htmlNode={renderedBody}
|
htmlNode={renderedBody}
|
||||||
testId={id}
|
testId={id}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const CommentHeader = ({
|
|||||||
closed,
|
closed,
|
||||||
createdAt,
|
createdAt,
|
||||||
lastEdit,
|
lastEdit,
|
||||||
commentUsers,
|
postUsers,
|
||||||
}) => {
|
}) => {
|
||||||
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(commentUsers ?? {})[0]?.profile?.image
|
? Object.values(postUsers ?? {})[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?.imageUrlSmall}
|
src={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar}
|
||||||
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,
|
||||||
}),
|
}),
|
||||||
commentUsers: PropTypes.shape({}).isRequired,
|
postUsers: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
CommentHeader.defaultProps = {
|
CommentHeader.defaultProps = {
|
||||||
|
|||||||
@@ -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,
|
||||||
commentUsers: {
|
postUsers: {
|
||||||
'test-user': {
|
'test-user': {
|
||||||
profile: { image: { hasImage: true, imageUrlSmall: 'http://avatar.test/img.png' } },
|
profile: { image: { hasImage: true, imageUrlSmall: 'http://avatar.test/img.png' } },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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';
|
||||||
@@ -25,7 +24,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, users: replyUsers,
|
id, abuseFlagged, author, authorLabel, endorsed, lastEdit, closed, closedBy,
|
||||||
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();
|
||||||
@@ -79,10 +78,6 @@ 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
|
||||||
@@ -128,7 +123,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={profileImage?.hasImage ? profileImage?.imageUrlSmall : authorAvatar?.imageUrlSmall}
|
src={authorAvatar?.imageUrlSmall}
|
||||||
style={{
|
style={{
|
||||||
width: '32px',
|
width: '32px',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
@@ -173,7 +168,7 @@ const Reply = ({ responseId }) => {
|
|||||||
<HTMLLoader
|
<HTMLLoader
|
||||||
componentId="reply"
|
componentId="reply"
|
||||||
htmlNode={renderedBody}
|
htmlNode={renderedBody}
|
||||||
cssClassName="html-loader text-break font-style text-gray-700"
|
cssClassName="html-loader text-break font-style text-primary-500"
|
||||||
testId={id}
|
testId={id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -30,16 +30,6 @@ 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')
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import React, { useCallback, useContext, useMemo } from 'react';
|
import 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';
|
||||||
@@ -43,12 +51,27 @@ CategoryPostsList.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PostsView = () => {
|
const PostsView = () => {
|
||||||
const { topicId, category } = useContext(DiscussionContext);
|
const {
|
||||||
|
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(''));
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
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';
|
||||||
@@ -230,7 +232,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', {
|
||||||
@@ -278,7 +280,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' },
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const PostFilterBar = () => {
|
|||||||
<ActionItem
|
<ActionItem
|
||||||
key={cohort.id}
|
key={cohort.id}
|
||||||
id={toString(cohort.id)}
|
id={toString(cohort.id)}
|
||||||
label={cohort.name}
|
label={capitalize(cohort.name)}
|
||||||
value={toString(cohort.id)}
|
value={toString(cohort.id)}
|
||||||
selected={currentFilters.cohort}
|
selected={currentFilters.cohort}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ 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 && (
|
||||||
@@ -153,6 +152,7 @@ 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-gray-700">
|
<div className="d-flex mt-14px text-break font-style text-primary-500">
|
||||||
<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) && (
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -126,7 +126,7 @@ const PostHeader = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<h5
|
<h5
|
||||||
className="mb-0 font-style"
|
className="mb-0 font-style text-primary-500"
|
||||||
style={{ lineHeight: '21px' }}
|
style={{ lineHeight: '21px' }}
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import 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';
|
||||||
@@ -60,11 +64,19 @@ 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));
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
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';
|
||||||
|
|
||||||
@@ -68,10 +66,3 @@ 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');
|
|
||||||
|
|||||||
Reference in New Issue
Block a user