diff --git a/jest.config.js b/jest.config.js index 31737d3c3..e9b3d2335 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,10 +5,10 @@ module.exports = createConfig('jest', { // If you want to add config BEFORE jest loads, use setupFiles instead. setupFiles: ['/.env.test'], setupFilesAfterEnv: [ - '/src/setupTest.js', + '/src/setupTest.jsx', ], coveragePathIgnorePatterns: [ - 'src/setupTest.js', + 'src/setupTest.jsx', 'src/i18n', ], }); diff --git a/src/components/FilterBar.jsx b/src/components/FilterBar.jsx index 84cde4c60..cbbbe21e4 100644 --- a/src/components/FilterBar.jsx +++ b/src/components/FilterBar.jsx @@ -1,4 +1,3 @@ -/* eslint-disable react/forbid-prop-types */ import React, { useCallback, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; @@ -15,7 +14,7 @@ import { PostsStatusFilter, RequestStatus, ThreadOrdering, ThreadType, } from '../data/constants'; -import { selectCourseCohorts } from '../discussions/cohorts/data/selectors'; +import selectCourseCohorts from '../discussions/cohorts/data/selectors'; import messages from '../discussions/posts/post-filter-bar/messages'; import { ActionItem } from '../discussions/posts/post-filter-bar/PostFilterBar'; @@ -194,8 +193,16 @@ const FilterBar = ({ FilterBar.propTypes = { intl: intlShape.isRequired, - filters: PropTypes.array.isRequired, - selectedFilters: PropTypes.object.isRequired, + filters: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + filters: PropTypes.arrayOf(PropTypes.string), + })).isRequired, + selectedFilters: PropTypes.shape({ + postType: ThreadType, + status: PostsStatusFilter, + orderBy: ThreadOrdering, + cohort: PropTypes.string, + }).isRequired, onFilterChange: PropTypes.func.isRequired, showCohortsFilter: PropTypes.bool, }; diff --git a/src/components/NavigationBar/CourseTabsNavigation.jsx b/src/components/NavigationBar/CourseTabsNavigation.jsx index 5571d96a1..9d4af37d7 100644 --- a/src/components/NavigationBar/CourseTabsNavigation.jsx +++ b/src/components/NavigationBar/CourseTabsNavigation.jsx @@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { fetchTab } from './data/thunks'; +import fetchTab from './data/thunks'; import Tabs from './tabs/Tabs'; import messages from './messages'; diff --git a/src/components/NavigationBar/data/api.js b/src/components/NavigationBar/data/api.js index 3b4ce1c66..9135129bf 100644 --- a/src/components/NavigationBar/data/api.js +++ b/src/components/NavigationBar/data/api.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { camelCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; diff --git a/src/components/NavigationBar/data/api.test.js b/src/components/NavigationBar/data/api.test.js index fc3a34e64..142ec4b2d 100644 --- a/src/components/NavigationBar/data/api.test.js +++ b/src/components/NavigationBar/data/api.test.js @@ -5,9 +5,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCourseMetadataApiUrl } from './api'; -import { fetchTab } from './thunks'; +import fetchTab from './thunks'; import './__factories__'; diff --git a/src/components/NavigationBar/data/selectors.js b/src/components/NavigationBar/data/selectors.js index 0c371c513..0504c8d7d 100644 --- a/src/components/NavigationBar/data/selectors.js +++ b/src/components/NavigationBar/data/selectors.js @@ -1,3 +1,3 @@ -/* eslint-disable import/prefer-default-export */ +const selectCourseTabs = state => state.courseTabs; -export const selectCourseTabs = state => state.courseTabs; +export default selectCourseTabs; diff --git a/src/components/NavigationBar/data/slice.js b/src/components/NavigationBar/data/slice.js index 2fe22822a..17e43641f 100644 --- a/src/components/NavigationBar/data/slice.js +++ b/src/components/NavigationBar/data/slice.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import { createSlice } from '@reduxjs/toolkit'; export const LOADING = 'loading'; @@ -17,27 +16,39 @@ const slice = createSlice({ org: null, }, reducers: { - fetchTabDenied: (state, { payload }) => { - state.courseId = payload.courseId; - state.courseStatus = DENIED; - }, - fetchTabFailure: (state, { payload }) => { - state.courseId = payload.courseId; - state.courseStatus = FAILED; - }, - fetchTabRequest: (state, { payload }) => { - state.courseId = payload.courseId; - state.courseStatus = LOADING; - }, - fetchTabSuccess: (state, { payload }) => { - state.courseId = payload.courseId; - state.targetUserId = payload.targetUserId; - state.tabs = payload.tabs; - state.courseStatus = LOADED; - state.courseTitle = payload.courseTitle; - state.courseNumber = payload.courseNumber; - state.org = payload.org; - }, + fetchTabDenied: (state, { payload }) => ( + { + ...state, + courseId: payload.courseId, + courseStatus: DENIED, + } + ), + fetchTabFailure: (state, { payload }) => ( + { + ...state, + courseId: payload.courseId, + courseStatus: FAILED, + } + ), + fetchTabRequest: (state, { payload }) => ( + { + ...state, + courseId: payload.courseId, + courseStatus: LOADING, + } + ), + fetchTabSuccess: (state, { payload }) => ( + { + ...state, + courseId: payload.courseId, + targetUserId: payload.targetUserId, + tabs: payload.tabs, + courseStatus: LOADED, + courseTitle: payload.courseTitle, + courseNumber: payload.courseNumber, + org: payload.org, + } + ), }, }); diff --git a/src/components/NavigationBar/data/thunks.js b/src/components/NavigationBar/data/thunks.js index bfff811fb..621a52b34 100644 --- a/src/components/NavigationBar/data/thunks.js +++ b/src/components/NavigationBar/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export, no-unused-expressions */ import { logError } from '@edx/frontend-platform/logging'; import { getHttpErrorStatus } from '../../../discussions/utils'; @@ -10,7 +9,7 @@ import { fetchTabSuccess, } from './slice'; -export function fetchTab(courseId, rootSlug) { +export default function fetchTab(courseId, rootSlug) { return async (dispatch) => { dispatch(fetchTabRequest({ courseId })); try { diff --git a/src/components/NavigationBar/index.js b/src/components/NavigationBar/index.js deleted file mode 100644 index e2236ee72..000000000 --- a/src/components/NavigationBar/index.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -export { default as CourseTabsNavigation } from './CourseTabsNavigation'; diff --git a/src/components/Search.jsx b/src/components/Search.jsx index 5c5bfa2df..6c8af33f6 100644 --- a/src/components/Search.jsx +++ b/src/components/Search.jsx @@ -9,7 +9,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, SearchField } from '@edx/paragon'; import { Search as SearchIcon } from '@edx/paragon/icons'; -import { DiscussionContext } from '../discussions/common/context'; +import DiscussionContext from '../discussions/common/context'; import { setUsernameSearch } from '../discussions/learners/data'; import { setSearchQuery } from '../discussions/posts/data'; import postsMessages from '../discussions/posts/post-actions-bar/messages'; diff --git a/src/components/icons/InsertLink.jsx b/src/components/icons/InsertLink.jsx deleted file mode 100644 index 52a55762e..000000000 --- a/src/components/icons/InsertLink.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function InsertLink() { - return ( - - - - ); -} diff --git a/src/components/icons/Issue.jsx b/src/components/icons/Issue.jsx deleted file mode 100644 index d7bacebc9..000000000 --- a/src/components/icons/Issue.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function Issue() { - return ( - - - - - - ); -} diff --git a/src/components/icons/People.jsx b/src/components/icons/People.jsx deleted file mode 100644 index c0ed0115b..000000000 --- a/src/components/icons/People.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function People() { - return ( - - - - ); -} diff --git a/src/components/icons/PushPin.jsx b/src/components/icons/PushPin.jsx deleted file mode 100644 index 87c5fe5f6..000000000 --- a/src/components/icons/PushPin.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function PushPin() { - return ( - - - - ); -} diff --git a/src/components/icons/Question.jsx b/src/components/icons/Question.jsx deleted file mode 100644 index baf63ea78..000000000 --- a/src/components/icons/Question.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function Question() { - return ( - - - - - - ); -} diff --git a/src/components/icons/QuestionAnswer.jsx b/src/components/icons/QuestionAnswer.jsx deleted file mode 100644 index 1bdb00fdb..000000000 --- a/src/components/icons/QuestionAnswer.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function QuestionAnswer() { - return ( - - - - ); -} diff --git a/src/components/icons/QuestionAnswerOutline.jsx b/src/components/icons/QuestionAnswerOutline.jsx deleted file mode 100644 index 4b7f083ad..000000000 --- a/src/components/icons/QuestionAnswerOutline.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function QuestionAnswerOutline() { - return ( - - - - ); -} diff --git a/src/components/icons/StarFilled.jsx b/src/components/icons/StarFilled.jsx deleted file mode 100644 index 4ffb14bee..000000000 --- a/src/components/icons/StarFilled.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function StarFilled() { - return ( - - - - ); -} diff --git a/src/components/icons/StarOutline.jsx b/src/components/icons/StarOutline.jsx deleted file mode 100644 index e484171ce..000000000 --- a/src/components/icons/StarOutline.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function StarOutline() { - return ( - - - - ); -} diff --git a/src/components/icons/ThumbUpFilled.jsx b/src/components/icons/ThumbUpFilled.jsx deleted file mode 100644 index 3c01c904b..000000000 --- a/src/components/icons/ThumbUpFilled.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function ThumbUpFilled() { - return ( - - - - ); -} diff --git a/src/components/icons/ThumbUpOutline.jsx b/src/components/icons/ThumbUpOutline.jsx deleted file mode 100644 index ff6147cde..000000000 --- a/src/components/icons/ThumbUpOutline.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/function-component-definition -export default function ThumbUpOutline() { - return ( - - - - - ); -} diff --git a/src/components/icons/index.js b/src/components/icons/index.js deleted file mode 100644 index 2b0821540..000000000 --- a/src/components/icons/index.js +++ /dev/null @@ -1,11 +0,0 @@ -export { default as InsertLink } from './InsertLink'; -export { default as Issue } from './Issue'; -export { default as People } from './People'; -export { default as PushPin } from './PushPin'; -export { default as Question } from './Question'; -export { default as QuestionAnswer } from './QuestionAnswer'; -export { default as QuestionAnswerOutline } from './QuestionAnswerOutline'; -export { default as StarFilled } from './StarFilled'; -export { default as StarOutline } from './StarOutline'; -export { default as ThumbUpFilled } from './ThumbUpFilled'; -export { default as ThumbUpOutline } from './ThumbUpOutline'; diff --git a/src/data/__factories__/blocks.js b/src/data/__factories__/blocks.js index 43fb2ee8f..5119890cb 100644 --- a/src/data/__factories__/blocks.js +++ b/src/data/__factories__/blocks.js @@ -1,7 +1,5 @@ -/* eslint-disable import/prefer-default-export */ - // Course Blocks API response for the demo course. -export const getBlocksAPIResponse = (newProvider = false) => { +const getBlocksAPIResponse = (newProvider = false) => { const response = { root: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course', blocks: { @@ -936,3 +934,5 @@ export const getBlocksAPIResponse = (newProvider = false) => { } return response; }; + +export default getBlocksAPIResponse; diff --git a/src/data/__factories__/index.js b/src/data/__factories__/index.js deleted file mode 100644 index 91bf7d9c7..000000000 --- a/src/data/__factories__/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './blocks'; diff --git a/src/data/api.js b/src/data/api.js index 67591d89d..86d077c12 100644 --- a/src/data/api.js +++ b/src/data/api.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getApiBaseUrl } from './constants'; diff --git a/src/data/hooks.js b/src/data/hooks.js index cf836495d..cce436760 100644 --- a/src/data/hooks.js +++ b/src/data/hooks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -15,7 +14,7 @@ import { useDispatch } from 'react-redux'; * * @return {(boolean|(function(*=): Promise)|*)[]} */ -export function useDispatchWithState() { +export default function useDispatchWithState() { const dispatch = useDispatch(); const [isDispatching, setDispatching] = useState(false); diff --git a/src/data/redux.test.js b/src/data/redux.test.js index 0a1329d25..8d39179b1 100644 --- a/src/data/redux.test.js +++ b/src/data/redux.test.js @@ -5,11 +5,11 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../store'; -import { executeThunk } from '../test-utils'; -import { getBlocksAPIResponse } from './__factories__'; +import executeThunk from '../test-utils'; +import getBlocksAPIResponse from './__factories__/blocks'; import { getBlocksAPIURL } from './api'; import { RequestStatus } from './constants'; -import { fetchCourseBlocks } from './thunks'; +import fetchCourseBlocks from './thunks'; const blocksAPIURL = getBlocksAPIURL(); const courseId = 'course-v1:edX+DemoX+Demo_Course'; diff --git a/src/data/selectors.js b/src/data/selectors.js index afc4eb2e9..ccf2e34d7 100644 --- a/src/data/selectors.js +++ b/src/data/selectors.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - import { createSelector } from '@reduxjs/toolkit'; import { selectDiscussionProvider, selectGroupAtSubsection } from '../discussions/data/selectors'; diff --git a/src/data/slices.js b/src/data/slices.js index 08a2d8f3d..112f9efbf 100644 --- a/src/data/slices.js +++ b/src/data/slices.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ import { createSlice } from '@reduxjs/toolkit'; import { RequestStatus } from './constants'; @@ -16,19 +15,33 @@ const blocksSlice = createSlice({ blocks: {}, }, reducers: { - fetchCourseBlocksRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, - fetchCourseBlocksSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - Object.assign(state, payload); - }, - fetchCourseBlocksFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchCourseBlocksDenied: (state) => { - state.status = RequestStatus.DENIED; - }, + fetchCourseBlocksRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), + fetchCourseBlocksSuccess: (state, { payload }) => ( + { + ...state, + status: RequestStatus.SUCCESSFUL, + topics: payload.topics, + chapters: payload.chapters, + blocks: payload.blocks, + } + ), + fetchCourseBlocksFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchCourseBlocksDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), }, }); diff --git a/src/data/thunks.js b/src/data/thunks.js index e2ada391e..0d58392aa 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export, no-unused-expressions */ import { camelCaseObject } from '@edx/frontend-platform'; import { logError } from '@edx/frontend-platform/logging'; @@ -88,7 +87,7 @@ function normaliseCourseBlocks({ }; } -export function fetchCourseBlocks(courseId, username) { +export default function fetchCourseBlocks(courseId, username) { return async (dispatch) => { try { dispatch(fetchCourseBlocksRequest({ courseId })); diff --git a/src/discussions/cohorts/data/api.js b/src/discussions/cohorts/data/api.js index dab53e24f..97d60b090 100644 --- a/src/discussions/cohorts/data/api.js +++ b/src/discussions/cohorts/data/api.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; diff --git a/src/discussions/cohorts/data/redux.test.js b/src/discussions/cohorts/data/redux.test.js index 46d1b1530..8f0fb86cc 100644 --- a/src/discussions/cohorts/data/redux.test.js +++ b/src/discussions/cohorts/data/redux.test.js @@ -5,9 +5,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCohortsApiUrl } from './api'; -import { fetchCourseCohorts } from './thunks'; +import fetchCourseCohorts from './thunks'; import './__factories__'; diff --git a/src/discussions/cohorts/data/selectors.js b/src/discussions/cohorts/data/selectors.js index d9b0905a1..d2dccf683 100644 --- a/src/discussions/cohorts/data/selectors.js +++ b/src/discussions/cohorts/data/selectors.js @@ -1,3 +1,3 @@ -/* eslint-disable import/prefer-default-export */ +const selectCourseCohorts = state => state.cohorts.cohorts; -export const selectCourseCohorts = state => state.cohorts.cohorts; +export default selectCourseCohorts; diff --git a/src/discussions/cohorts/data/slices.js b/src/discussions/cohorts/data/slices.js index ea7d543ee..c96f64ed4 100644 --- a/src/discussions/cohorts/data/slices.js +++ b/src/discussions/cohorts/data/slices.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ import { createSlice } from '@reduxjs/toolkit'; import { RequestStatus } from '../../../data/constants'; @@ -10,17 +9,26 @@ const cohortsSlice = createSlice({ cohorts: [], }, reducers: { - fetchCohortsRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - state.cohorts = []; - }, - fetchCohortsSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - state.cohorts = payload; - }, - fetchCohortsFailed: (state) => { - state.status = RequestStatus.FAILED; - }, + fetchCohortsRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + cohorts: [], + } + ), + fetchCohortsSuccess: (state, { payload }) => ( + { + ...state, + status: RequestStatus.SUCCESSFUL, + cohorts: payload, + } + ), + fetchCohortsFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), }, }); diff --git a/src/discussions/cohorts/data/thunks.js b/src/discussions/cohorts/data/thunks.js index e23be09de..f818efaa2 100644 --- a/src/discussions/cohorts/data/thunks.js +++ b/src/discussions/cohorts/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { camelCaseObject } from '@edx/frontend-platform'; import { logError } from '@edx/frontend-platform/logging'; @@ -11,7 +10,7 @@ import { fetchCohortsSuccess, } from './slices'; -export function fetchCourseCohorts(courseId) { +export default function fetchCourseCohorts(courseId) { return async (dispatch) => { try { dispatch(fetchCohortsRequest()); diff --git a/src/discussions/common/ActionsDropdown.test.jsx b/src/discussions/common/ActionsDropdown.test.jsx index 570705b2b..5a40f64a0 100644 --- a/src/discussions/common/ActionsDropdown.test.jsx +++ b/src/discussions/common/ActionsDropdown.test.jsx @@ -12,13 +12,13 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { ContentActions } from '../../data/constants'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import { getCourseConfigApiUrl } from '../data/api'; -import { fetchCourseConfig } from '../data/thunks'; +import fetchCourseConfig from '../data/thunks'; import messages from '../messages'; import { getCommentsApiUrl } from '../post-comments/data/api'; import { addComment, fetchThreadComments } from '../post-comments/data/thunks'; -import { PostCommentsContext } from '../post-comments/postCommentsContext'; +import PostCommentsContext from '../post-comments/postCommentsContext'; import { getThreadsApiUrl } from '../posts/data/api'; import { fetchThread } from '../posts/data/thunks'; import { ACTIONS_LIST } from '../utils'; diff --git a/src/discussions/common/AlertBanner.test.jsx b/src/discussions/common/AlertBanner.test.jsx index 3671a76ac..4deeb128a 100644 --- a/src/discussions/common/AlertBanner.test.jsx +++ b/src/discussions/common/AlertBanner.test.jsx @@ -9,7 +9,7 @@ import { ThreadType } from '../../data/constants'; import { initializeStore } from '../../store'; import messages from '../post-comments/messages'; import AlertBanner from './AlertBanner'; -import { DiscussionContext } from './context'; +import DiscussionContext from './context'; import '../post-comments/data/__factories__'; import '../posts/data/__factories__'; diff --git a/src/discussions/common/AuthorLabel.jsx b/src/discussions/common/AuthorLabel.jsx index 5ef43da92..e30577c69 100644 --- a/src/discussions/common/AuthorLabel.jsx +++ b/src/discussions/common/AuthorLabel.jsx @@ -11,7 +11,7 @@ import { Institution, School } from '@edx/paragon/icons'; import { Routes } from '../../data/constants'; import messages from '../messages'; -import { DiscussionContext } from './context'; +import DiscussionContext from './context'; import timeLocale from './time-locale'; const AuthorLabel = ({ diff --git a/src/discussions/common/AuthorLabel.test.jsx b/src/discussions/common/AuthorLabel.test.jsx index b2136c59e..9a05912aa 100644 --- a/src/discussions/common/AuthorLabel.test.jsx +++ b/src/discussions/common/AuthorLabel.test.jsx @@ -9,11 +9,11 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import { getCourseConfigApiUrl } from '../data/api'; -import { fetchCourseConfig } from '../data/thunks'; +import fetchCourseConfig from '../data/thunks'; import AuthorLabel from './AuthorLabel'; -import { DiscussionContext } from './context'; +import DiscussionContext from './context'; const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseConfigApiUrl = getCourseConfigApiUrl(); diff --git a/src/discussions/common/EndorsedAlertBanner.jsx b/src/discussions/common/EndorsedAlertBanner.jsx index 415904783..8df3bab50 100644 --- a/src/discussions/common/EndorsedAlertBanner.jsx +++ b/src/discussions/common/EndorsedAlertBanner.jsx @@ -9,7 +9,7 @@ import { CheckCircle, Verified } from '@edx/paragon/icons'; import { ThreadType } from '../../data/constants'; import messages from '../post-comments/messages'; -import { PostCommentsContext } from '../post-comments/postCommentsContext'; +import PostCommentsContext from '../post-comments/postCommentsContext'; import AuthorLabel from './AuthorLabel'; import timeLocale from './time-locale'; diff --git a/src/discussions/common/EndorsedAlertBanner.test.jsx b/src/discussions/common/EndorsedAlertBanner.test.jsx index 4689ff3fd..3582d715a 100644 --- a/src/discussions/common/EndorsedAlertBanner.test.jsx +++ b/src/discussions/common/EndorsedAlertBanner.test.jsx @@ -8,8 +8,8 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { ThreadType } from '../../data/constants'; import { initializeStore } from '../../store'; import messages from '../post-comments/messages'; -import { PostCommentsContext } from '../post-comments/postCommentsContext'; -import { DiscussionContext } from './context'; +import PostCommentsContext from '../post-comments/postCommentsContext'; +import DiscussionContext from './context'; import EndorsedAlertBanner from './EndorsedAlertBanner'; import '../post-comments/data/__factories__'; diff --git a/src/discussions/common/HoverCard.jsx b/src/discussions/common/HoverCard.jsx index 6739ad75c..4abc69ec5 100644 --- a/src/discussions/common/HoverCard.jsx +++ b/src/discussions/common/HoverCard.jsx @@ -7,14 +7,15 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Button, Icon, IconButton, OverlayTrigger, Tooltip, } from '@edx/paragon'; - import { StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline, -} from '../../components/icons'; +} from '@edx/paragon/icons'; + +import { ThreadType } from '../../data/constants'; import { useUserPostingEnabled } from '../data/hooks'; -import { PostCommentsContext } from '../post-comments/postCommentsContext'; +import PostCommentsContext from '../post-comments/postCommentsContext'; import ActionsDropdown from './ActionsDropdown'; -import { DiscussionContext } from './context'; +import DiscussionContext from './context'; const HoverCard = ({ id, @@ -127,8 +128,22 @@ HoverCard.propTypes = { addResponseCommentButtonMessage: PropTypes.string.isRequired, onLike: PropTypes.func.isRequired, voted: PropTypes.bool.isRequired, - // eslint-disable-next-line react/forbid-prop-types - endorseIcons: PropTypes.objectOf(PropTypes.any), + endorseIcons: PropTypes.objectOf(PropTypes.shape( + { + id: PropTypes.string, + action: PropTypes.string, + icon: PropTypes.element, + label: { + id: PropTypes.string, + defaultMessage: PropTypes.string, + description: PropTypes.string, + }, + conditions: { + endorsed: PropTypes.bool, + postType: ThreadType, + }, + }, + )), onFollow: PropTypes.func, following: PropTypes.bool, }; diff --git a/src/discussions/common/HoverCard.test.jsx b/src/discussions/common/HoverCard.test.jsx index 437d80883..d791cdfbe 100644 --- a/src/discussions/common/HoverCard.test.jsx +++ b/src/discussions/common/HoverCard.test.jsx @@ -11,15 +11,15 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import { getCourseConfigApiUrl } from '../data/api'; -import { fetchCourseConfig } from '../data/thunks'; +import fetchCourseConfig from '../data/thunks'; import DiscussionContent from '../discussions-home/DiscussionContent'; import { getCommentsApiUrl } from '../post-comments/data/api'; import { fetchCommentResponses } from '../post-comments/data/thunks'; import { getThreadsApiUrl } from '../posts/data/api'; import { fetchThreads } from '../posts/data/thunks'; -import { DiscussionContext } from './context'; +import DiscussionContext from './context'; import '../posts/data/__factories__'; import '../post-comments/data/__factories__'; diff --git a/src/discussions/common/context.js b/src/discussions/common/context.js index 8f06b7f1c..65664fcd7 100644 --- a/src/discussions/common/context.js +++ b/src/discussions/common/context.js @@ -1,7 +1,6 @@ -/* eslint-disable import/prefer-default-export */ import React from 'react'; -export const DiscussionContext = React.createContext({ +const DiscussionContext = React.createContext({ page: null, courseId: null, postId: null, @@ -10,3 +9,5 @@ export const DiscussionContext = React.createContext({ category: null, learnerUsername: null, }); + +export default DiscussionContext; diff --git a/src/discussions/data/api.js b/src/discussions/data/api.js index 3f1971942..5f631ef55 100644 --- a/src/discussions/data/api.js +++ b/src/discussions/data/api.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - import { ensureConfig, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; diff --git a/src/discussions/data/hooks.js b/src/discussions/data/hooks.js index 8b3e46555..b5282c871 100644 --- a/src/discussions/data/hooks.js +++ b/src/discussions/data/hooks.js @@ -15,13 +15,13 @@ import { breakpoints, useWindowSize } from '@edx/paragon'; import { RequestStatus, Routes } from '../../data/constants'; import { selectTopicsUnderCategory } from '../../data/selectors'; -import { fetchCourseBlocks } from '../../data/thunks'; -import { DiscussionContext } from '../common/context'; +import fetchCourseBlocks from '../../data/thunks'; +import DiscussionContext from '../common/context'; import { clearRedirect } from '../posts/data'; import { threadsLoadingStatus } from '../posts/data/selectors'; import { selectTopics } from '../topics/data/selectors'; import tourCheckpoints from '../tours/constants'; -import { selectTours } from '../tours/data/selectors'; +import selectTours from '../tours/data/selectors'; import { updateTourShowStatus } from '../tours/data/thunks'; import messages from '../tours/messages'; import { discussionsPath } from '../utils'; @@ -36,7 +36,7 @@ import { selectUserIsGroupTa, selectUserIsStaff, } from './selectors'; -import { fetchCourseConfig } from './thunks'; +import fetchCourseConfig from './thunks'; export function useTotalTopicThreadCount() { const topics = useSelector(selectTopics); diff --git a/src/discussions/data/hooks.test.jsx b/src/discussions/data/hooks.test.jsx index 219f79589..ca58ebe82 100644 --- a/src/discussions/data/hooks.test.jsx +++ b/src/discussions/data/hooks.test.jsx @@ -8,11 +8,11 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; -import { DiscussionContext } from '../common/context'; +import executeThunk from '../../test-utils'; +import DiscussionContext from '../common/context'; import { getCourseConfigApiUrl } from './api'; import { useCurrentDiscussionTopic, useUserPostingEnabled } from './hooks'; -import { fetchCourseConfig } from './thunks'; +import fetchCourseConfig from './thunks'; const courseId = 'course-v1:edX+TestX+Test_Course'; const courseConfigApiUrl = getCourseConfigApiUrl(); diff --git a/src/discussions/data/selectors.js b/src/discussions/data/selectors.js index 84028a245..acdd06d2e 100644 --- a/src/discussions/data/selectors.js +++ b/src/discussions/data/selectors.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { PostsStatusFilter, ThreadType } from '../../data/constants'; export const selectAnonymousPostingConfig = state => ({ diff --git a/src/discussions/data/slices.js b/src/discussions/data/slices.js index 21ee2613c..071fc02e0 100644 --- a/src/discussions/data/slices.js +++ b/src/discussions/data/slices.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ import { createSlice } from '@reduxjs/toolkit'; import { RequestStatus } from '../../data/constants'; @@ -28,19 +27,29 @@ const configSlice = createSlice({ enableInContext: false, }, reducers: { - fetchConfigRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, + fetchConfigRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), fetchConfigSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - Object.assign(state, payload); - }, - fetchConfigFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchConfigDenied: (state) => { - state.status = RequestStatus.DENIED; + const newState = Object.assign(state, payload); + newState.status = RequestStatus.SUCCESSFUL; + return newState; }, + fetchConfigFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchConfigDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), }, }); diff --git a/src/discussions/data/thunks.js b/src/discussions/data/thunks.js index e22151d7e..f57b09974 100644 --- a/src/discussions/data/thunks.js +++ b/src/discussions/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { camelCaseObject } from '@edx/frontend-platform'; import { logError } from '@edx/frontend-platform/logging'; @@ -19,7 +18,7 @@ import { * @param {string} courseId The course ID for the course to fetch config for. * @returns {(function(*): Promise)|*} */ -export function fetchCourseConfig(courseId) { +export default function fetchCourseConfig(courseId) { return async (dispatch) => { try { let learnerSort = LearnersOrdering.BY_LAST_ACTIVITY; diff --git a/src/discussions/discussions-home/DiscussionSidebar.jsx b/src/discussions/discussions-home/DiscussionSidebar.jsx index 36ab3fea1..749ca95d7 100644 --- a/src/discussions/discussions-home/DiscussionSidebar.jsx +++ b/src/discussions/discussions-home/DiscussionSidebar.jsx @@ -13,7 +13,7 @@ import { useWindowSize } from '@edx/paragon'; import Spinner from '../../components/Spinner'; import { RequestStatus, Routes as ROUTES } from '../../data/constants'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { useContainerSize, useIsOnDesktop, useIsOnXLDesktop } from '../data/hooks'; import { selectConfigLoadingStatus, selectEnableInContext } from '../data/selectors'; diff --git a/src/discussions/discussions-home/DiscussionSidebar.test.jsx b/src/discussions/discussions-home/DiscussionSidebar.test.jsx index dda145c82..905e77ec3 100644 --- a/src/discussions/discussions-home/DiscussionSidebar.test.jsx +++ b/src/discussions/discussions-home/DiscussionSidebar.test.jsx @@ -11,7 +11,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../store'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { fetchConfigSuccess } from '../data/slices'; import { getThreadsApiUrl } from '../posts/data/api'; import DiscussionSidebar from './DiscussionSidebar'; diff --git a/src/discussions/discussions-home/DiscussionsHome.jsx b/src/discussions/discussions-home/DiscussionsHome.jsx index 0a2ecdd5f..6a44544b8 100644 --- a/src/discussions/discussions-home/DiscussionsHome.jsx +++ b/src/discussions/discussions-home/DiscussionsHome.jsx @@ -1,5 +1,6 @@ -/* eslint-disable react/jsx-no-constructed-context-values */ -import React, { lazy, Suspense, useRef } from 'react'; +import React, { + lazy, Suspense, useMemo, useRef, +} from 'react'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; @@ -10,14 +11,15 @@ import { import { LearningHeader as Header } from '@edx/frontend-component-header'; import { Spinner } from '../../components'; -import { selectCourseTabs } from '../../components/NavigationBar/data/selectors'; +import selectCourseTabs from '../../components/NavigationBar/data/selectors'; import { ALL_ROUTES, DiscussionProvider, Routes as ROUTES } from '../../data/constants'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useSidebarVisible, } from '../data/hooks'; import { selectDiscussionProvider, selectEnableInContext } from '../data/selectors'; -import { EmptyLearners, EmptyPosts, EmptyTopics } from '../empty-posts'; +import { EmptyLearners, EmptyTopics } from '../empty-posts'; +import EmptyPosts from '../empty-posts/EmptyPosts'; import { EmptyTopic as InContextEmptyTopics } from '../in-context-topics/components'; import messages from '../messages'; import { selectPostEditorVisible } from '../posts/data/selectors'; @@ -60,18 +62,19 @@ const DiscussionsHome = () => { const displayContentArea = (postId || postEditorVisible || (learnerUsername && postId)); if (displayContentArea) { displaySidebar = isOnDesktop; } + const discussionContextValue = useMemo(() => ({ + page, + courseId, + postId, + topicId, + enableInContextSidebar, + category, + learnerUsername, + })); + return ( )}> - + {!enableInContextSidebar && (
)} diff --git a/src/discussions/discussions-home/DiscussionsHome.test.jsx b/src/discussions/discussions-home/DiscussionsHome.test.jsx index b05fdb304..eb460a65e 100644 --- a/src/discussions/discussions-home/DiscussionsHome.test.jsx +++ b/src/discussions/discussions-home/DiscussionsHome.test.jsx @@ -13,18 +13,18 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { getCourseMetadataApiUrl } from '../../components/NavigationBar/data/api'; -import { fetchTab } from '../../components/NavigationBar/data/thunks'; +import fetchTab from '../../components/NavigationBar/data/thunks'; import { getApiBaseUrl } from '../../data/constants'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import { getCourseConfigApiUrl, getDiscussionsConfigUrl } from '../data/api'; -import { fetchCourseConfig } from '../data/thunks'; +import fetchCourseConfig from '../data/thunks'; import { getCourseTopicsApiUrl } from '../in-context-topics/data/api'; -import { fetchCourseTopicsV3 } from '../in-context-topics/data/thunks'; +import fetchCourseTopicsV3 from '../in-context-topics/data/thunks'; import navigationBarMessages from '../navigation/navigation-bar/messages'; import { getThreadsApiUrl } from '../posts/data/api'; import { fetchThreads } from '../posts/data/thunks'; -import { fetchCourseTopics } from '../topics/data/thunks'; +import fetchCourseTopics from '../topics/data/thunks'; import DiscussionsHome from './DiscussionsHome'; import '../posts/data/__factories__/threads.factory'; diff --git a/src/discussions/discussions-home/DiscussionsRestrictionBanner.test.jsx b/src/discussions/discussions-home/DiscussionsRestrictionBanner.test.jsx index f308e5212..70ad2e14d 100644 --- a/src/discussions/discussions-home/DiscussionsRestrictionBanner.test.jsx +++ b/src/discussions/discussions-home/DiscussionsRestrictionBanner.test.jsx @@ -5,7 +5,7 @@ import { initializeMockApp } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../store'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { fetchConfigSuccess } from '../data/slices'; import messages from '../messages'; import DiscussionsRestrictionBanner from './DiscussionsRestrictionBanner'; diff --git a/src/discussions/discussions-home/index.js b/src/discussions/discussions-home/index.js deleted file mode 100644 index 253f7e498..000000000 --- a/src/discussions/discussions-home/index.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -export { default as DiscussionsHome } from './DiscussionsHome'; diff --git a/src/discussions/empty-posts/EmptyPosts.jsx b/src/discussions/empty-posts/EmptyPosts.jsx index 8d45200f0..e5d7ccf60 100644 --- a/src/discussions/empty-posts/EmptyPosts.jsx +++ b/src/discussions/empty-posts/EmptyPosts.jsx @@ -8,8 +8,8 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useIsOnDesktop } from '../data/hooks'; import { selectAreThreadsFiltered, selectPostThreadCount } from '../data/selectors'; import messages from '../messages'; -// eslint-disable-next-line import/no-cycle -import { messages as postMessages, showPostEditor } from '../posts'; +import { showPostEditor } from '../posts/data'; +import postMessages from '../posts/post-actions-bar/messages'; import EmptyPage from './EmptyPage'; const EmptyPosts = ({ subTitleMessage }) => { diff --git a/src/discussions/empty-posts/EmptyPosts.test.jsx b/src/discussions/empty-posts/EmptyPosts.test.jsx index 88f591b07..6498f083b 100644 --- a/src/discussions/empty-posts/EmptyPosts.test.jsx +++ b/src/discussions/empty-posts/EmptyPosts.test.jsx @@ -10,7 +10,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import messages from '../messages'; import { getThreadsApiUrl } from '../posts/data/api'; import { fetchThreads } from '../posts/data/thunks'; diff --git a/src/discussions/empty-posts/EmptyTopics.jsx b/src/discussions/empty-posts/EmptyTopics.jsx index b2420803e..d662f0d49 100644 --- a/src/discussions/empty-posts/EmptyTopics.jsx +++ b/src/discussions/empty-posts/EmptyTopics.jsx @@ -8,8 +8,8 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useIsOnDesktop, useTotalTopicThreadCount } from '../data/hooks'; import { selectTopicThreadCount } from '../data/selectors'; import messages from '../messages'; -// eslint-disable-next-line import/no-cycle -import { messages as postMessages, showPostEditor } from '../posts'; +import { showPostEditor } from '../posts/data'; +import postMessages from '../posts/post-actions-bar/messages'; import EmptyPage from './EmptyPage'; const EmptyTopics = () => { diff --git a/src/discussions/empty-posts/EmptyTopics.test.jsx b/src/discussions/empty-posts/EmptyTopics.test.jsx index 31072a19a..dc36ce6f6 100644 --- a/src/discussions/empty-posts/EmptyTopics.test.jsx +++ b/src/discussions/empty-posts/EmptyTopics.test.jsx @@ -11,9 +11,9 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { getApiBaseUrl, Routes as ROUTES } from '../../data/constants'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import messages from '../messages'; -import { fetchCourseTopics } from '../topics/data/thunks'; +import fetchCourseTopics from '../topics/data/thunks'; import EmptyTopics from './EmptyTopics'; import '../topics/data/__factories__'; diff --git a/src/discussions/empty-posts/index.js b/src/discussions/empty-posts/index.js index 1854b63c8..c65077f37 100644 --- a/src/discussions/empty-posts/index.js +++ b/src/discussions/empty-posts/index.js @@ -1,5 +1,3 @@ export { default as EmptyLearners } from './EmptyLearners'; export { default as EmptyPage } from './EmptyPage'; -// eslint-disable-next-line import/no-cycle -export { default as EmptyPosts } from './EmptyPosts'; export { default as EmptyTopics } from './EmptyTopics'; diff --git a/src/discussions/in-context-topics/TopicPostsView.jsx b/src/discussions/in-context-topics/TopicPostsView.jsx index 9fc050a6c..7fda6a88b 100644 --- a/src/discussions/in-context-topics/TopicPostsView.jsx +++ b/src/discussions/in-context-topics/TopicPostsView.jsx @@ -9,7 +9,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Spinner } from '@edx/paragon'; import { RequestStatus, Routes } from '../../data/constants'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { selectDiscussionProvider } from '../data/selectors'; import { selectTopicThreadsIds } from '../posts/data/selectors'; import PostsList from '../posts/PostsList'; @@ -18,7 +18,7 @@ import { selectArchivedTopic, selectLoadingStatus, selectNonCoursewareTopics, selectSubsection, selectSubsectionUnits, selectUnits, } from './data/selectors'; -import { fetchCourseTopicsV3 } from './data/thunks'; +import fetchCourseTopicsV3 from './data/thunks'; import { BackButton, NoResults } from './components'; import messages from './messages'; import { Topic } from './topic'; diff --git a/src/discussions/in-context-topics/TopicPostsView.test.jsx b/src/discussions/in-context-topics/TopicPostsView.test.jsx index 0679808a8..0ac1073ea 100644 --- a/src/discussions/in-context-topics/TopicPostsView.test.jsx +++ b/src/discussions/in-context-topics/TopicPostsView.test.jsx @@ -16,13 +16,13 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { PostActionsBar } from '../../components'; import { Routes as ROUTES } from '../../data/constants'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; -import { DiscussionContext } from '../common/context'; +import executeThunk from '../../test-utils'; +import DiscussionContext from '../common/context'; import { getThreadsApiUrl } from '../posts/data/api'; import { fetchThreads } from '../posts/data/thunks'; import { getCourseTopicsApiUrl } from './data/api'; import { selectCoursewareTopics } from './data/selectors'; -import { fetchCourseTopicsV3 } from './data/thunks'; +import fetchCourseTopicsV3 from './data/thunks'; import TopicPostsView from './TopicPostsView'; import TopicsView from './TopicsView'; diff --git a/src/discussions/in-context-topics/TopicsView.jsx b/src/discussions/in-context-topics/TopicsView.jsx index 0c84d0888..d42b85fbd 100644 --- a/src/discussions/in-context-topics/TopicsView.jsx +++ b/src/discussions/in-context-topics/TopicsView.jsx @@ -10,7 +10,7 @@ import { Spinner } from '@edx/paragon'; import SearchInfo from '../../components/SearchInfo'; import { RequestStatus } from '../../data/constants'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { selectAreThreadsFiltered, selectDiscussionProvider } from '../data/selectors'; import { clearFilter, clearSort } from '../posts/data/slices'; import NoResults from '../posts/NoResults'; @@ -20,7 +20,7 @@ import { selectNonCoursewareTopics, selectTopicFilter, selectTopics, } from './data/selectors'; import { setFilter } from './data/slices'; -import { fetchCourseTopicsV3 } from './data/thunks'; +import fetchCourseTopicsV3 from './data/thunks'; import { ArchivedBaseGroup, SectionBaseGroup, Topic } from './topic'; const TopicsList = () => { diff --git a/src/discussions/in-context-topics/TopicsView.test.jsx b/src/discussions/in-context-topics/TopicsView.test.jsx index 4cc62c2e5..70aabdd25 100644 --- a/src/discussions/in-context-topics/TopicsView.test.jsx +++ b/src/discussions/in-context-topics/TopicsView.test.jsx @@ -15,11 +15,11 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; -import { DiscussionContext } from '../common/context'; +import executeThunk from '../../test-utils'; +import DiscussionContext from '../common/context'; import { getCourseTopicsApiUrl } from './data/api'; import { selectCoursewareTopics, selectNonCoursewareTopics } from './data/selectors'; -import { fetchCourseTopicsV3 } from './data/thunks'; +import fetchCourseTopicsV3 from './data/thunks'; import TopicPostsView from './TopicPostsView'; import TopicsView from './TopicsView'; diff --git a/src/discussions/in-context-topics/components/EmptyTopics.jsx b/src/discussions/in-context-topics/components/EmptyTopics.jsx index 0f66d3489..6ecc9f2b6 100644 --- a/src/discussions/in-context-topics/components/EmptyTopics.jsx +++ b/src/discussions/in-context-topics/components/EmptyTopics.jsx @@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import { useIsOnDesktop } from '../../data/hooks'; import { selectPostThreadCount } from '../../data/selectors'; import EmptyPage from '../../empty-posts/EmptyPage'; diff --git a/src/discussions/in-context-topics/components/index.js b/src/discussions/in-context-topics/components/index.js index 634fa6151..2a0265c9e 100644 --- a/src/discussions/in-context-topics/components/index.js +++ b/src/discussions/in-context-topics/components/index.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ export { default as BackButton } from './BackButton'; export { default as EmptyTopic } from './EmptyTopics'; export { default as NoResults } from './NoResults'; diff --git a/src/discussions/in-context-topics/data/api.js b/src/discussions/in-context-topics/data/api.js index 4745c8aea..049d64fd7 100644 --- a/src/discussions/in-context-topics/data/api.js +++ b/src/discussions/in-context-topics/data/api.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { camelCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; diff --git a/src/discussions/in-context-topics/data/api.test.js b/src/discussions/in-context-topics/data/api.test.js index 69adad4d0..760e89a06 100644 --- a/src/discussions/in-context-topics/data/api.test.js +++ b/src/discussions/in-context-topics/data/api.test.js @@ -5,9 +5,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCourseTopicsApiUrl, getCourseTopicsV3 } from './api'; -import { fetchCourseTopicsV3 } from './thunks'; +import fetchCourseTopicsV3 from './thunks'; import './__factories__'; diff --git a/src/discussions/in-context-topics/data/redux.test.js b/src/discussions/in-context-topics/data/redux.test.js index fd8c132ca..0eb6e232e 100644 --- a/src/discussions/in-context-topics/data/redux.test.js +++ b/src/discussions/in-context-topics/data/redux.test.js @@ -5,9 +5,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCourseTopicsApiUrl } from './api'; -import { fetchCourseTopicsV3 } from './thunks'; +import fetchCourseTopicsV3 from './thunks'; import './__factories__'; diff --git a/src/discussions/in-context-topics/data/selector.test.jsx b/src/discussions/in-context-topics/data/selector.test.jsx index 3a78fddf6..a356ff08a 100644 --- a/src/discussions/in-context-topics/data/selector.test.jsx +++ b/src/discussions/in-context-topics/data/selector.test.jsx @@ -5,7 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCourseTopicsApiUrl } from './api'; import { selectArchivedTopic, @@ -20,7 +20,7 @@ import { selectTotalTopicsThreadsCount, selectUnits, } from './selectors'; -import { fetchCourseTopicsV3 } from './thunks'; +import fetchCourseTopicsV3 from './thunks'; import './__factories__'; diff --git a/src/discussions/in-context-topics/data/selectors.js b/src/discussions/in-context-topics/data/selectors.js index 824d8fbd5..b7daca882 100644 --- a/src/discussions/in-context-topics/data/selectors.js +++ b/src/discussions/in-context-topics/data/selectors.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - import { createSelector } from '@reduxjs/toolkit'; export const selectTopicFilter = state => state.inContextTopics.filter.trim().toLowerCase(); diff --git a/src/discussions/in-context-topics/data/slices.js b/src/discussions/in-context-topics/data/slices.js index 41a614682..79efc7d63 100644 --- a/src/discussions/in-context-topics/data/slices.js +++ b/src/discussions/in-context-topics/data/slices.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ import { createSlice } from '@reduxjs/toolkit'; import { RequestStatus } from '../../../data/constants'; @@ -16,27 +15,42 @@ const topicsSlice = createSlice({ filter: '', }, reducers: { - fetchCourseTopicsRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, - fetchCourseTopicsSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - state.topics = payload.topics; - state.coursewareTopics = payload.coursewareTopics; - state.nonCoursewareTopics = payload.nonCoursewareTopics; - state.nonCoursewareIds = payload.nonCoursewareIds; - state.units = payload.units; - state.archivedTopics = payload.archivedTopics; - }, - fetchCourseTopicsFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchCourseTopicsDenied: (state) => { - state.status = RequestStatus.DENIED; - }, - setFilter: (state, { payload }) => { - state.filter = payload; - }, + fetchCourseTopicsRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), + fetchCourseTopicsSuccess: (state, { payload }) => ( + { + ...state, + status: RequestStatus.SUCCESSFUL, + topics: payload.topics, + coursewareTopics: payload.coursewareTopics, + nonCoursewareTopics: payload.nonCoursewareTopics, + nonCoursewareIds: payload.nonCoursewareIds, + units: payload.units, + archivedTopics: payload.archivedTopics, + } + ), + fetchCourseTopicsFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchCourseTopicsDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), + setFilter: (state, { payload }) => ( + { + ...state, + filter: payload, + } + ), }, }); diff --git a/src/discussions/in-context-topics/data/thunks.js b/src/discussions/in-context-topics/data/thunks.js index d4696f215..d65d1d223 100644 --- a/src/discussions/in-context-topics/data/thunks.js +++ b/src/discussions/in-context-topics/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { reduce } from 'lodash'; import { logError } from '@edx/frontend-platform/logging'; @@ -53,7 +52,7 @@ function normalizeTopicsV3(topics) { }; } -export function fetchCourseTopicsV3(courseId) { +export default function fetchCourseTopicsV3(courseId) { return async (dispatch) => { try { dispatch(fetchCourseTopicsRequest({ courseId })); diff --git a/src/discussions/in-context-topics/index.js b/src/discussions/in-context-topics/index.js index a7b55ddab..fd705bf2f 100644 --- a/src/discussions/in-context-topics/index.js +++ b/src/discussions/in-context-topics/index.js @@ -1,3 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export { default as TopicPostsView } from './TopicPostsView'; export { default as TopicsView } from './TopicsView'; diff --git a/src/discussions/in-context-topics/topic-search/TopicSearchBar.jsx b/src/discussions/in-context-topics/topic-search/TopicSearchBar.jsx index 2f4ff84ba..e9ec7e56a 100644 --- a/src/discussions/in-context-topics/topic-search/TopicSearchBar.jsx +++ b/src/discussions/in-context-topics/topic-search/TopicSearchBar.jsx @@ -6,7 +6,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, SearchField } from '@edx/paragon'; import { Search as SearchIcon } from '@edx/paragon/icons'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import postsMessages from '../../posts/post-actions-bar/messages'; import { setFilter as setTopicFilter } from '../data/slices'; diff --git a/src/discussions/in-context-topics/topic-search/index.js b/src/discussions/in-context-topics/topic-search/index.js index ff0a7dcf5..8d90d5d4b 100644 --- a/src/discussions/in-context-topics/topic-search/index.js +++ b/src/discussions/in-context-topics/topic-search/index.js @@ -1,3 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export { default as TopicSearchBar } from './TopicSearchBar'; export { default as TopicSearchResultBar } from './TopicSearchResultBar'; diff --git a/src/discussions/in-context-topics/topic/Topic.jsx b/src/discussions/in-context-topics/topic/Topic.jsx index b4f955861..fb3d0caf9 100644 --- a/src/discussions/in-context-topics/topic/Topic.jsx +++ b/src/discussions/in-context-topics/topic/Topic.jsx @@ -1,19 +1,13 @@ -/* eslint-disable react/prop-types */ -/* eslint-disable no-unused-vars, react/forbid-prop-types */ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { useSelector } from 'react-redux'; import { Link, useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon'; -import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons'; import TopicStats from '../../../components/TopicStats'; import { Routes } from '../../../data/constants'; -import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors'; import { discussionsPath } from '../../utils'; import messages from '../messages'; @@ -24,10 +18,6 @@ const Topic = ({ }) => { const intl = useIntl(); const { courseId } = useParams(); - const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); - const userIsGroupTa = useSelector(selectUserIsGroupTa); - const { inactiveFlags, activeFlags } = topic; - const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa); const isSelected = (id) => window.location.pathname.includes(id); const topicUrl = discussionsPath(Routes.TOPICS.TOPIC, { courseId, @@ -72,7 +62,8 @@ export const topicShape = PropTypes.shape({ id: PropTypes.string, usage_key: PropTypes.string, name: PropTypes.string, - thread_counts: PropTypes.shape({ + displayName: PropTypes.string, + threadCounts: PropTypes.shape({ discussions: PropTypes.number, questions: PropTypes.number, }), diff --git a/src/discussions/in-context-topics/topic/index.js b/src/discussions/in-context-topics/topic/index.js index ef5526f7a..c9393a836 100644 --- a/src/discussions/in-context-topics/topic/index.js +++ b/src/discussions/in-context-topics/topic/index.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ export { default as ArchivedBaseGroup } from './ArchivedBaseGroup'; export { default as SectionBaseGroup } from './SectionBaseGroup'; export { default as Topic } from './Topic'; diff --git a/src/discussions/index.js b/src/discussions/index.js index aa743732e..93a598842 100644 --- a/src/discussions/index.js +++ b/src/discussions/index.js @@ -1,4 +1,4 @@ -export * from './discussions-home'; -export * from './post-comments'; +export { default as DiscussionsHome } from './discussions-home/DiscussionsHome'; +export { default as PostCommentsView } from './post-comments/PostCommentsView'; export * from './posts'; -export * from './topics'; +export { default as TopicsView } from './topics/TopicsView'; diff --git a/src/discussions/learners/LearnerPostsView.jsx b/src/discussions/learners/LearnerPostsView.jsx index 48f819360..dc67354c0 100644 --- a/src/discussions/learners/LearnerPostsView.jsx +++ b/src/discussions/learners/LearnerPostsView.jsx @@ -16,9 +16,9 @@ import { RequestStatus, Routes, } from '../../data/constants'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors'; -import { usePostList } from '../posts/data/hooks'; +import usePostList from '../posts/data/hooks'; import { selectAllThreadsIds, selectThreadNextPage, diff --git a/src/discussions/learners/LearnerPostsView.test.jsx b/src/discussions/learners/LearnerPostsView.test.jsx index cee5d14df..5977d0f51 100644 --- a/src/discussions/learners/LearnerPostsView.test.jsx +++ b/src/discussions/learners/LearnerPostsView.test.jsx @@ -16,10 +16,10 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import { getCohortsApiUrl } from '../cohorts/data/api'; -import { fetchCourseCohorts } from '../cohorts/data/thunks'; -import { DiscussionContext } from '../common/context'; +import fetchCourseCohorts from '../cohorts/data/thunks'; +import DiscussionContext from '../common/context'; import { learnerPostsApiUrl } from './data/api'; import { fetchUserPosts } from './data/thunks'; import LearnerPostsView from './LearnerPostsView'; diff --git a/src/discussions/learners/LearnersView.jsx b/src/discussions/learners/LearnersView.jsx index eeb493c08..ed6605779 100644 --- a/src/discussions/learners/LearnersView.jsx +++ b/src/discussions/learners/LearnersView.jsx @@ -55,14 +55,14 @@ const LearnersView = () => { dispatch(setUsernameSearch('')); }, []); - const renderLearnersList = useMemo(() => ( - ( - courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && learners.map((learner) => ( + const renderLearnersList = useMemo(() => { + if (courseConfigLoadingStatus === RequestStatus.SUCCESSFUL) { + return learners.map((learner) => ( - )) - // eslint-disable-next-line react/jsx-no-useless-fragment - ) || <> - ), [courseConfigLoadingStatus, learners]); + )); + } + return null; + }, [courseConfigLoadingStatus, learners]); return (
diff --git a/src/discussions/learners/LearnersView.test.jsx b/src/discussions/learners/LearnersView.test.jsx index 5d20afef6..54813b4ed 100644 --- a/src/discussions/learners/LearnersView.test.jsx +++ b/src/discussions/learners/LearnersView.test.jsx @@ -1,4 +1,3 @@ -/* eslint-disable default-param-last */ import React from 'react'; import { @@ -16,10 +15,10 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { PostActionsBar } from '../../components'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; -import { DiscussionContext } from '../common/context'; +import executeThunk from '../../test-utils'; +import DiscussionContext from '../common/context'; import { getDiscussionsConfigUrl } from '../data/api'; -import { fetchCourseConfig } from '../data/thunks'; +import fetchCourseConfig from '../data/thunks'; import { getUserProfileApiUrl, learnersApiUrl } from './data/api'; import { fetchLearners } from './data/thunks'; import LearnersView from './LearnersView'; @@ -81,9 +80,9 @@ describe('LearnersView', () => { pageSize = 6, page = 1, username = ['learner-1', 'learner-2', 'learner-3'], - searchText, - activeFlags, - inactiveFlags, + searchText = null, + activeFlags = null, + inactiveFlags = null, ) { Factory.resetAll(); const learnersData = Factory.build('learnersResult', {}, { diff --git a/src/discussions/learners/data/api.js b/src/discussions/learners/data/api.js index 969320df5..53a4d9c88 100644 --- a/src/discussions/learners/data/api.js +++ b/src/discussions/learners/data/api.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import snakeCase from 'lodash/snakeCase'; import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform'; diff --git a/src/discussions/learners/data/redux.test.jsx b/src/discussions/learners/data/redux.test.jsx index 1bfeb648f..6156188fb 100644 --- a/src/discussions/learners/data/redux.test.jsx +++ b/src/discussions/learners/data/redux.test.jsx @@ -5,7 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { setupLearnerMockResponse } from '../test-utils'; import { setPostFilter, setSortedBy, setUsernameSearch } from './slices'; import { fetchLearners } from './thunks'; diff --git a/src/discussions/learners/data/selector.test.jsx b/src/discussions/learners/data/selector.test.jsx index 4a74c9a19..0c2b6409f 100644 --- a/src/discussions/learners/data/selector.test.jsx +++ b/src/discussions/learners/data/selector.test.jsx @@ -5,7 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getUserProfileApiUrl, learnersApiUrl } from './api'; import { learnersLoadingStatus, diff --git a/src/discussions/learners/data/selectors.js b/src/discussions/learners/data/selectors.js index ee327b633..8b439e0fc 100644 --- a/src/discussions/learners/data/selectors.js +++ b/src/discussions/learners/data/selectors.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - import { createSelector } from '@reduxjs/toolkit'; export const selectAllLearners = createSelector( diff --git a/src/discussions/learners/data/slices.js b/src/discussions/learners/data/slices.js index 7f35b4630..cc94aaee3 100644 --- a/src/discussions/learners/data/slices.js +++ b/src/discussions/learners/data/slices.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ import { createSlice } from '@reduxjs/toolkit'; import { @@ -28,38 +27,63 @@ const learnersSlice = createSlice({ usernameSearch: null, }, reducers: { - fetchLearnersSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - state.pages[payload.page - 1] = payload.results; - state.learnerProfiles = { - ...state.learnerProfiles, - ...(payload.learnerProfiles || {}), - }; - state.nextPage = (payload.page < payload.pagination.numPages) ? payload.page + 1 : null; - state.totalPages = payload.pagination.numPages; - state.totalLearners = payload.pagination.count; - }, - fetchLearnersFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchLearnersDenied: (state) => { - state.status = RequestStatus.DENIED; - }, - fetchLearnersRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, - setSortedBy: (state, { payload }) => { - state.pages = []; - state.sortedBy = payload; - }, - setUsernameSearch: (state, { payload }) => { - state.usernameSearch = payload; - state.pages = []; - }, - setPostFilter: (state, { payload }) => { - state.pages = []; - state.postFilter = payload; - }, + fetchLearnersSuccess: (state, { payload }) => ( + { + ...state, + status: RequestStatus.SUCCESSFUL, + pages: [ + ...state.pages.slice(0, payload.page - 1), + payload.results, + ...state.pages.slice(payload.page), + ], + learnerProfiles: { + ...state.learnerProfiles, + ...(payload.learnerProfiles || {}), + }, + nextPage: payload.page < payload.pagination.numPages ? payload.page + 1 : null, + totalPages: payload.pagination.numPages, + totalLearners: payload.pagination.count, + } + ), + fetchLearnersFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchLearnersDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), + fetchLearnersRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), + setSortedBy: (state, { payload }) => ( + { + ...state, + pages: [], + sortedBy: payload, + } + ), + setUsernameSearch: (state, { payload }) => ( + { + ...state, + usernameSearch: payload, + pages: [], + } + ), + setPostFilter: (state, { payload }) => ( + { + ...state, + pages: [], + postFilter: payload, + } + ), }, }); diff --git a/src/discussions/learners/data/thunks.js b/src/discussions/learners/data/thunks.js index 491bd4a35..93603c9cc 100644 --- a/src/discussions/learners/data/thunks.js +++ b/src/discussions/learners/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform'; import { logError } from '@edx/frontend-platform/logging'; diff --git a/src/discussions/learners/index.js b/src/discussions/learners/index.js index e4cf49c45..116dcd96c 100644 --- a/src/discussions/learners/index.js +++ b/src/discussions/learners/index.js @@ -1,3 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export { default as LearnerPostsView } from './LearnerPostsView'; export { default as LearnersView } from './LearnersView'; diff --git a/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.jsx b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.jsx index 1a643d219..fafdc8799 100644 --- a/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.jsx +++ b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.jsx @@ -8,8 +8,8 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import FilterBar from '../../../components/FilterBar'; import { PostsStatusFilter, ThreadType } from '../../../data/constants'; -import { selectCourseCohorts } from '../../cohorts/data/selectors'; -import { fetchCourseCohorts } from '../../cohorts/data/thunks'; +import selectCourseCohorts from '../../cohorts/data/selectors'; +import fetchCourseCohorts from '../../cohorts/data/thunks'; import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors'; import { setPostFilter } from '../data/slices'; diff --git a/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx index 6dd45c250..4c62f2753 100644 --- a/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx +++ b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx @@ -10,7 +10,7 @@ import { initializeMockApp } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../../store'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import LearnerPostFilterBar from './LearnerPostFilterBar'; let store; diff --git a/src/discussions/learners/learner/LearnerCard.jsx b/src/discussions/learners/learner/LearnerCard.jsx index 8304cd6c4..6f3a4c6b7 100644 --- a/src/discussions/learners/learner/LearnerCard.jsx +++ b/src/discussions/learners/learner/LearnerCard.jsx @@ -3,11 +3,11 @@ import React, { useContext } from 'react'; import { Link } from 'react-router-dom'; import { Routes } from '../../../data/constants'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import { discussionsPath } from '../../utils'; import LearnerAvatar from './LearnerAvatar'; import LearnerFooter from './LearnerFooter'; -import { learnerShape } from './proptypes'; +import learnerShape from './proptypes'; const LearnerCard = ({ learner }) => { const { diff --git a/src/discussions/learners/learner/LearnerFooter.jsx b/src/discussions/learners/learner/LearnerFooter.jsx index 4d4c6704b..92ef099f3 100644 --- a/src/discussions/learners/learner/LearnerFooter.jsx +++ b/src/discussions/learners/learner/LearnerFooter.jsx @@ -5,9 +5,10 @@ import { useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon'; -import { Edit, Report, ReportGmailerrorred } from '@edx/paragon/icons'; +import { + Edit, QuestionAnswerOutline, Report, ReportGmailerrorred, +} from '@edx/paragon/icons'; -import { QuestionAnswerOutline } from '../../../components/icons'; import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors'; import messages from '../messages'; diff --git a/src/discussions/learners/learner/index.js b/src/discussions/learners/learner/index.js index 89774d75c..b66c800ed 100644 --- a/src/discussions/learners/learner/index.js +++ b/src/discussions/learners/learner/index.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ export { default as LearnerCard } from './LearnerCard'; export { default as LearnerFilterBar } from './LearnerFilterBar'; export { default as LearnerFooter } from './LearnerFooter'; diff --git a/src/discussions/learners/learner/proptypes.js b/src/discussions/learners/learner/proptypes.js index 35fcd7d0b..ab6cdb6a7 100644 --- a/src/discussions/learners/learner/proptypes.js +++ b/src/discussions/learners/learner/proptypes.js @@ -1,7 +1,6 @@ -/* eslint-disable import/prefer-default-export */ import PropTypes from 'prop-types'; -export const learnerShape = PropTypes.shape({ +const learnerShape = PropTypes.shape({ activeFlags: PropTypes.number, inactiveFlags: PropTypes.number, username: PropTypes.string, @@ -9,3 +8,5 @@ export const learnerShape = PropTypes.shape({ responses: PropTypes.number, threads: PropTypes.number, }); + +export default learnerShape; diff --git a/src/discussions/learners/test-utils.js b/src/discussions/learners/test-utils.js index 14451f88c..a14ba17e6 100644 --- a/src/discussions/learners/test-utils.js +++ b/src/discussions/learners/test-utils.js @@ -4,9 +4,9 @@ import { Factory } from 'rosie'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import { getDiscussionsConfigUrl } from '../data/api'; -import { fetchCourseConfig } from '../data/thunks'; +import fetchCourseConfig from '../data/thunks'; import { getUserProfileApiUrl, learnerPostsApiUrl, learnersApiUrl } from './data/api'; import { fetchLearners, fetchUserPosts } from './data/thunks'; diff --git a/src/discussions/navigation/breadcrumb-menu/BreadcrumbDropdown.jsx b/src/discussions/navigation/breadcrumb-menu/BreadcrumbDropdown.jsx index 6a6ef5223..c75efb68e 100644 --- a/src/discussions/navigation/breadcrumb-menu/BreadcrumbDropdown.jsx +++ b/src/discussions/navigation/breadcrumb-menu/BreadcrumbDropdown.jsx @@ -50,11 +50,21 @@ const BreadcrumbDropdown = ({ }; BreadcrumbDropdown.propTypes = { - // eslint-disable-next-line react/forbid-prop-types - currentItem: PropTypes.any, + currentItem: PropTypes.shape({ + name: PropTypes.string, + id: PropTypes.string, + questions: PropTypes.number, + discussions: PropTypes.number, + flags: PropTypes.number, + }), showAllPath: PropTypes.func.isRequired, - // eslint-disable-next-line react/forbid-prop-types - items: PropTypes.array.isRequired, + items: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + id: PropTypes.string, + questions: PropTypes.number, + discussions: PropTypes.number, + flags: PropTypes.number, + })).isRequired, itemPathFunc: PropTypes.func.isRequired, itemLabelFunc: PropTypes.func.isRequired, itemActiveFunc: PropTypes.func.isRequired, diff --git a/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx index e0eda57d7..40510f07d 100644 --- a/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx +++ b/src/discussions/navigation/breadcrumb-menu/LegacyBreadcrumbMenu.test.jsx @@ -14,8 +14,8 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { getApiBaseUrl, Routes as ROUTES } from '../../../data/constants'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; -import { fetchCourseTopics } from '../../topics/data/thunks'; +import executeThunk from '../../../test-utils'; +import fetchCourseTopics from '../../topics/data/thunks'; import { LegacyBreadcrumbMenu } from '../index'; import '../../topics/data/__factories__'; diff --git a/src/discussions/navigation/index.js b/src/discussions/navigation/index.js index e5e9c078b..56e03bce4 100644 --- a/src/discussions/navigation/index.js +++ b/src/discussions/navigation/index.js @@ -1,3 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export { default as LegacyBreadcrumbMenu } from './breadcrumb-menu/LegacyBreadcrumbMenu'; export { default as NavigationBar } from './navigation-bar/NavigationBar'; diff --git a/src/discussions/navigation/navigation-bar/NavigationBar.jsx b/src/discussions/navigation/navigation-bar/NavigationBar.jsx index 7d0f47cf3..226325e8e 100644 --- a/src/discussions/navigation/navigation-bar/NavigationBar.jsx +++ b/src/discussions/navigation/navigation-bar/NavigationBar.jsx @@ -6,7 +6,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Nav } from '@edx/paragon'; import { Routes } from '../../../data/constants'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import { discussionsPath } from '../../utils'; import messages from './messages'; diff --git a/src/discussions/post-comments/PostCommentsView.jsx b/src/discussions/post-comments/PostCommentsView.jsx index bafde3cdb..1bf7de93f 100644 --- a/src/discussions/post-comments/PostCommentsView.jsx +++ b/src/discussions/post-comments/PostCommentsView.jsx @@ -1,5 +1,5 @@ import React, { - Suspense, useCallback, useContext, useEffect, useState, + Suspense, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -12,8 +12,8 @@ import Spinner from '../../components/Spinner'; import { EndorsementStatus, PostsPages, ThreadType, } from '../../data/constants'; -import { useDispatchWithState } from '../../data/hooks'; -import { DiscussionContext } from '../common/context'; +import useDispatchWithState from '../../data/hooks'; +import DiscussionContext from '../common/context'; import { useIsOnDesktop } from '../data/hooks'; import { EmptyPage } from '../empty-posts'; import { Post } from '../posts'; @@ -22,7 +22,7 @@ import { discussionsPath } from '../utils'; import { ResponseEditor } from './comments/comment'; import { useCommentsCount, usePost } from './data/hooks'; import messages from './messages'; -import { PostCommentsContext } from './postCommentsContext'; +import PostCommentsContext from './postCommentsContext'; const CommentsSort = React.lazy(() => import('./comments/CommentsSort')); const CommentsView = React.lazy(() => import('./comments/CommentsView')); @@ -61,6 +61,12 @@ const PostCommentsView = () => { setAddingResponse(false); }, []); + const postCommentsContextValue = useMemo(() => ({ + isClosed: closed, + postType: type, + postId, + })); + if (!threadId) { if (!isLoading) { return ( @@ -79,13 +85,7 @@ const PostCommentsView = () => { } return ( - // eslint-disable-next-line react/jsx-no-constructed-context-values - + {!isOnDesktop && ( enableInContextSidebar ? ( <> diff --git a/src/discussions/post-comments/PostCommentsView.test.jsx b/src/discussions/post-comments/PostCommentsView.test.jsx index 3bee05c2f..09028821b 100644 --- a/src/discussions/post-comments/PostCommentsView.test.jsx +++ b/src/discussions/post-comments/PostCommentsView.test.jsx @@ -14,18 +14,18 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { getApiBaseUrl } from '../../data/constants'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import { getCohortsApiUrl } from '../cohorts/data/api'; -import { fetchCourseCohorts } from '../cohorts/data/thunks'; -import { DiscussionContext } from '../common/context'; +import fetchCourseCohorts from '../cohorts/data/thunks'; +import DiscussionContext from '../common/context'; import { getCourseConfigApiUrl } from '../data/api'; -import { fetchCourseConfig } from '../data/thunks'; +import fetchCourseConfig from '../data/thunks'; import DiscussionContent from '../discussions-home/DiscussionContent'; import { getThreadsApiUrl } from '../posts/data/api'; import { fetchThread, fetchThreads } from '../posts/data/thunks'; -import { fetchCourseTopics } from '../topics/data/thunks'; +import fetchCourseTopics from '../topics/data/thunks'; import { getDiscussionTourUrl } from '../tours/data/api'; -import { selectTours } from '../tours/data/selectors'; +import selectTours from '../tours/data/selectors'; import { fetchDiscussionTours } from '../tours/data/thunks'; import discussionTourFactory from '../tours/data/tours.factory'; import { getCommentsApiUrl } from './data/api'; diff --git a/src/discussions/post-comments/comments/CommentsView.jsx b/src/discussions/post-comments/comments/CommentsView.jsx index e6850b6d8..0bc895612 100644 --- a/src/discussions/post-comments/comments/CommentsView.jsx +++ b/src/discussions/post-comments/comments/CommentsView.jsx @@ -9,7 +9,7 @@ import { useUserPostingEnabled } from '../../data/hooks'; import { isLastElementOfList } from '../../utils'; import { usePostComments } from '../data/hooks'; import messages from '../messages'; -import { PostCommentsContext } from '../postCommentsContext'; +import PostCommentsContext from '../postCommentsContext'; import { Comment, ResponseEditor } from './comment'; const CommentsView = ({ endorsed }) => { @@ -73,49 +73,46 @@ const CommentsView = ({ endorsed }) => { ), [hasMorePages, isLoading, handleLoadMoreResponses]); return ( - // eslint-disable-next-line react/jsx-no-useless-fragment - <> - {((hasMorePages && isLoading) || !isLoading) && ( - <> - {endorsedCommentsIds.length > 0 && ( - <> - {handleDefinition(messages.endorsedResponseCount, endorsedCommentsIds.length)} - {endorsed === EndorsementStatus.DISCUSSION - ? handleComments(endorsedCommentsIds, true) - : handleComments(endorsedCommentsIds, false)} - - )} - {endorsed !== EndorsementStatus.ENDORSED && ( - <> - {handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)} - {unEndorsedCommentsIds.length === 0 &&
} - {handleComments(unEndorsedCommentsIds, false)} - {(isUserPrivilegedInPostingRestriction && !!unEndorsedCommentsIds.length && !isClosed) && ( -
- {!addingResponse && ( - - )} - -
- )} - - )} - - )} - + ((hasMorePages && isLoading) || !isLoading) && ( + <> + {endorsedCommentsIds.length > 0 && ( + <> + {handleDefinition(messages.endorsedResponseCount, endorsedCommentsIds.length)} + {endorsed === EndorsementStatus.DISCUSSION + ? handleComments(endorsedCommentsIds, true) + : handleComments(endorsedCommentsIds, false)} + + )} + {endorsed !== EndorsementStatus.ENDORSED && ( + <> + {handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)} + {unEndorsedCommentsIds.length === 0 &&
} + {handleComments(unEndorsedCommentsIds, false)} + {(isUserPrivilegedInPostingRestriction && !!unEndorsedCommentsIds.length && !isClosed) && ( +
+ {!addingResponse && ( + + )} + +
+ )} + + )} + + ) ); }; diff --git a/src/discussions/post-comments/comments/comment/Comment.jsx b/src/discussions/post-comments/comments/comment/Comment.jsx index 728dd2ce8..c08c10de1 100644 --- a/src/discussions/post-comments/comments/comment/Comment.jsx +++ b/src/discussions/post-comments/comments/comment/Comment.jsx @@ -12,7 +12,7 @@ import { Button, useToggle } from '@edx/paragon'; import HTMLLoader from '../../../../components/HTMLLoader'; import { ContentActions, EndorsementStatus } from '../../../../data/constants'; import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../../common'; -import { DiscussionContext } from '../../../common/context'; +import DiscussionContext from '../../../common/context'; import HoverCard from '../../../common/HoverCard'; import { ContentTypes } from '../../../data/constants'; import { useUserPostingEnabled } from '../../../data/hooks'; @@ -29,7 +29,7 @@ import { } from '../../data/selectors'; import { editComment, fetchCommentResponses, removeComment } from '../../data/thunks'; import messages from '../../messages'; -import { PostCommentsContext } from '../../postCommentsContext'; +import PostCommentsContext from '../../postCommentsContext'; import CommentEditor from './CommentEditor'; import CommentHeader from './CommentHeader'; import Reply from './Reply'; diff --git a/src/discussions/post-comments/comments/comment/CommentEditor.jsx b/src/discussions/post-comments/comments/comment/CommentEditor.jsx index c0a0ed73b..a2337128d 100644 --- a/src/discussions/post-comments/comments/comment/CommentEditor.jsx +++ b/src/discussions/post-comments/comments/comment/CommentEditor.jsx @@ -12,8 +12,8 @@ import { Button, Form, StatefulButton } from '@edx/paragon'; import { TinyMCEEditor } from '../../../../components'; import FormikErrorFeedback from '../../../../components/FormikErrorFeedback'; import PostPreviewPanel from '../../../../components/PostPreviewPanel'; -import { useDispatchWithState } from '../../../../data/hooks'; -import { DiscussionContext } from '../../../common/context'; +import useDispatchWithState from '../../../../data/hooks'; +import DiscussionContext from '../../../common/context'; import { selectModerationSettings, selectUserHasModerationPrivileges, @@ -60,7 +60,6 @@ const CommentEditor = ({ const initialValues = { comment: rawBody, - // eslint-disable-next-line react/prop-types editReasonCode: lastEdit?.reasonCode || (userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined), }; @@ -183,7 +182,9 @@ CommentEditor.propTypes = { comment: PropTypes.shape({ author: PropTypes.string, id: PropTypes.string, - lastEdit: PropTypes.shape({}), + lastEdit: PropTypes.shape({ + reasonCode: PropTypes.shape({}), + }), parentId: PropTypes.string, rawBody: PropTypes.string, threadId: PropTypes.string.isRequired, diff --git a/src/discussions/post-comments/comments/comment/ResponseEditor.jsx b/src/discussions/post-comments/comments/comment/ResponseEditor.jsx index 33714fed1..636a5593c 100644 --- a/src/discussions/post-comments/comments/comment/ResponseEditor.jsx +++ b/src/discussions/post-comments/comments/comment/ResponseEditor.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { DiscussionContext } from '../../../common/context'; +import DiscussionContext from '../../../common/context'; import CommentEditor from './CommentEditor'; const ResponseEditor = ({ diff --git a/src/discussions/post-comments/comments/comment/index.js b/src/discussions/post-comments/comments/comment/index.js index f26c389d3..d0c399f7d 100644 --- a/src/discussions/post-comments/comments/comment/index.js +++ b/src/discussions/post-comments/comments/comment/index.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ export { default as Comment } from './Comment'; export { default as CommentEditor } from './CommentEditor'; export { default as ResponseEditor } from './ResponseEditor'; diff --git a/src/discussions/post-comments/comments/comment/proptypes.js b/src/discussions/post-comments/comments/comment/proptypes.js deleted file mode 100644 index 68a867504..000000000 --- a/src/discussions/post-comments/comments/comment/proptypes.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import PropTypes from 'prop-types'; - -export const commentShape = PropTypes.shape({ - createdAt: PropTypes.string, - abuseFlagged: PropTypes.bool, - renderedBody: PropTypes.string, - endorsedBy: PropTypes.string, - endorsedAt: PropTypes.string, - endorsed: PropTypes.bool, - author: PropTypes.string, - authorLabel: PropTypes.string, - users: PropTypes.objectOf(PropTypes.shape({ - profile: PropTypes.shape({ - hasImage: PropTypes.bool, - imageUrlFull: PropTypes.string, - imageUrlLarge: PropTypes.string, - imageUrlMedium: PropTypes.string, - imageUrlSmall: PropTypes.string, - }), - })), -}); diff --git a/src/discussions/post-comments/data/api.js b/src/discussions/post-comments/data/api.js index 55499dc9f..bf73f95c9 100644 --- a/src/discussions/post-comments/data/api.js +++ b/src/discussions/post-comments/data/api.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; diff --git a/src/discussions/post-comments/data/api.test.js b/src/discussions/post-comments/data/api.test.js index dc75c2fea..16f6fd186 100644 --- a/src/discussions/post-comments/data/api.test.js +++ b/src/discussions/post-comments/data/api.test.js @@ -5,7 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCommentsApiUrl } from './api'; import { addComment, editComment, fetchCommentResponses, fetchThreadComments, removeComment, diff --git a/src/discussions/post-comments/data/hooks.js b/src/discussions/post-comments/data/hooks.js index 75fe03711..f697210e8 100644 --- a/src/discussions/post-comments/data/hooks.js +++ b/src/discussions/post-comments/data/hooks.js @@ -7,8 +7,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { EndorsementStatus } from '../../../data/constants'; -import { useDispatchWithState } from '../../../data/hooks'; -import { DiscussionContext } from '../../common/context'; +import useDispatchWithState from '../../../data/hooks'; +import DiscussionContext from '../../common/context'; import { selectThread } from '../../posts/data/selectors'; import { markThreadAsRead } from '../../posts/data/thunks'; import { filterPosts } from '../../utils'; diff --git a/src/discussions/post-comments/data/redux.test.js b/src/discussions/post-comments/data/redux.test.js index 6e32eab80..dbad7cf3d 100644 --- a/src/discussions/post-comments/data/redux.test.js +++ b/src/discussions/post-comments/data/redux.test.js @@ -6,7 +6,7 @@ import { initializeMockApp } from '@edx/frontend-platform/testing'; import { EndorsementStatus } from '../../../data/constants'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCommentsApiUrl } from './api'; import { addComment, editComment, fetchCommentResponses, fetchThreadComments, removeComment, diff --git a/src/discussions/post-comments/data/selectors.js b/src/discussions/post-comments/data/selectors.js index 960cb8ac4..fb04b8f37 100644 --- a/src/discussions/post-comments/data/selectors.js +++ b/src/discussions/post-comments/data/selectors.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { createSelector } from '@reduxjs/toolkit'; const selectCommentsById = state => state.comments.commentsById; diff --git a/src/discussions/post-comments/data/slices.js b/src/discussions/post-comments/data/slices.js index bf0eb8391..96ebf1f92 100644 --- a/src/discussions/post-comments/data/slices.js +++ b/src/discussions/post-comments/data/slices.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ import { createSlice } from '@reduxjs/toolkit'; import { EndorsementStatus, RequestStatus } from '../../../data/constants'; @@ -25,155 +24,290 @@ const commentsSlice = createSlice({ sortOrder: true, }, reducers: { - fetchCommentsRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, + fetchCommentsRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), fetchCommentsSuccess: (state, { payload }) => { const { threadId, page, endorsed } = payload; - // force endorsed to be null, true or false - state.status = RequestStatus.SUCCESSFUL; - state.commentsInThreads[threadId] = state.commentsInThreads[threadId] || {}; - state.pagination[threadId] = state.pagination[threadId] || {}; - // Append to existing list of comments list - // only if the new fetch is due to pagination + const newState = { ...state }; + + newState.status = RequestStatus.SUCCESSFUL; + + newState.commentsInThreads = { + ...newState.commentsInThreads, + [threadId]: newState.commentsInThreads[threadId] || {}, + }; + + newState.pagination = { + ...newState.pagination, + [threadId]: newState.pagination[threadId] || {}, + }; + if (page === 1) { - state.commentsInThreads[threadId][endorsed] = (payload.commentsInThreads[threadId] || []); + newState.commentsInThreads = { + ...newState.commentsInThreads, + [threadId]: { + ...newState.commentsInThreads[threadId], + [endorsed]: payload.commentsInThreads[threadId] || [], + }, + }; } else { - state.commentsInThreads[threadId][endorsed] = [ - // Newly posted comments will appear twice when fetching more pages. - // We use a Set here to remove duplicate entries, then spread it - // back into an array. - ...new Set([ - ...(state.commentsInThreads[threadId][endorsed] || []), - ...(payload.commentsInThreads[threadId] || []), - ]), - ]; - } - - state.pagination[threadId][endorsed] = { - currentPage: payload.page, - totalPages: payload.pagination.numPages, - hasMorePages: Boolean(payload.pagination.next), + newState.commentsInThreads = { + ...newState.commentsInThreads, + [threadId]: { + ...newState.commentsInThreads[threadId], + [endorsed]: [ + ...new Set([ + ...(newState.commentsInThreads[threadId][endorsed] || []), + ...(payload.commentsInThreads[threadId] || []), + ]), + ], + }, + }; + } + + newState.pagination = { + ...newState.pagination, + [threadId]: { + ...newState.pagination[threadId], + [endorsed]: { + currentPage: payload.page, + totalPages: payload.pagination.numPages, + hasMorePages: Boolean(payload.pagination.next), + }, + }, }; - state.commentsById = { ...state.commentsById, ...payload.commentsById }; - }, - fetchCommentsFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchCommentsDenied: (state) => { - state.status = RequestStatus.DENIED; - }, - fetchCommentResponsesRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, - fetchCommentResponsesFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchCommentResponsesDenied: (state) => { - state.status = RequestStatus.DENIED; + + newState.commentsById = { ...newState.commentsById, ...payload.commentsById }; + + return newState; }, + fetchCommentsFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchCommentsDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), + fetchCommentResponsesRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), + fetchCommentResponsesFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchCommentResponsesDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), fetchCommentResponsesSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; + const newState = { ...state }; + newState.status = RequestStatus.SUCCESSFUL; + if (payload.page === 1) { - state.commentsInComments[payload.commentId] = payload.commentsInComments[payload.commentId] || []; + newState.commentsInComments = { + ...newState.commentsInComments, + [payload.commentId]: payload.commentsInComments[payload.commentId] || [], + }; } else { - state.commentsInComments[payload.commentId] = [ - ...new Set([ - ...(state.commentsInComments[payload.commentId] || []), - ...(payload.commentsInComments[payload.commentId] || []), - ]), - ]; - } - state.commentsById = { ...state.commentsById, ...payload.commentsById }; - state.responsesPagination[payload.commentId] = { - currentPage: payload.page, - totalPages: payload.pagination.numPages, - hasMorePages: Boolean(payload.pagination.next), + newState.commentsInComments = { + ...newState.commentsInComments, + [payload.commentId]: [ + ...new Set([ + ...(newState.commentsInComments[payload.commentId] || []), + ...(payload.commentsInComments[payload.commentId] || []), + ]), + ], + }; + } + + newState.commentsById = { ...newState.commentsById, ...payload.commentsById }; + newState.responsesPagination = { + ...newState.responsesPagination, + [payload.commentId]: { + currentPage: payload.page, + totalPages: payload.pagination.numPages, + hasMorePages: Boolean(payload.pagination.next), + }, }; + + return newState; }, - postCommentRequest: (state, { payload }) => { - state.postStatus = RequestStatus.IN_PROGRESS; - state.commentDraft = payload; - }, - postCommentDenied: (state) => { - state.postStatus = RequestStatus.DENIED; - }, - postCommentFailed: (state) => { - state.postStatus = RequestStatus.FAILED; - }, + postCommentRequest: (state, { payload }) => ( + { + ...state, + postStatus: RequestStatus.IN_PROGRESS, + commentDraft: payload, + } + ), + postCommentDenied: (state) => ( + { + ...state, + postStatus: RequestStatus.DENIED, + } + ), + postCommentFailed: (state) => ( + { + ...state, + postStatus: RequestStatus.FAILED, + } + ), postCommentSuccess: (state, { payload }) => { - state.postStatus = RequestStatus.SUCCESSFUL; + const newState = { ...state }; + newState.postStatus = RequestStatus.SUCCESSFUL; + if (payload.parentId) { - if (!(payload.parentId in state.commentsInComments)) { - state.commentsInComments[payload.parentId] = []; - } - state.commentsInComments[payload.parentId].push(payload.id); + newState.commentsInComments = { + ...newState.commentsInComments, + [payload.parentId]: [ + ...(newState.commentsInComments[payload.parentId] || []), + payload.id, + ], + }; } else { - // The comment should be added to either the discussion or unendorsed - // sections since a new comment won't be endorsed yet. - ( - state.commentsInThreads[payload.threadId][EndorsementStatus.DISCUSSION] - || state.commentsInThreads[payload.threadId][EndorsementStatus.UNENDORSED] - ).push(payload.id); - } - state.commentsById[payload.id] = payload; - state.commentDraft = null; - }, - updateCommentRequest: (state, { payload }) => { - state.postStatus = RequestStatus.IN_PROGRESS; - state.commentDraft = payload; - }, - updateCommentDenied: (state) => { - state.postStatus = RequestStatus.DENIED; - }, - updateCommentFailed: (state) => { - state.postStatus = RequestStatus.FAILED; - }, - updateCommentSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - state.commentsById[payload.id] = payload; - state.commentDraft = null; + const threadComments = newState.commentsInThreads[payload.threadId] || {}; + const endorsementStatus = threadComments[EndorsementStatus.DISCUSSION] + ? EndorsementStatus.DISCUSSION + : EndorsementStatus.UNENDORSED; + + const updatedThreadComments = { + ...threadComments, + [endorsementStatus]: [ + ...(threadComments[endorsementStatus] || []), + payload.id, + ], + }; + newState.commentsInThreads = { + ...newState.commentsInThreads, + [payload.threadId]: updatedThreadComments, + }; + } + + newState.commentsById = { ...newState.commentsById, [payload.id]: payload }; + newState.commentDraft = null; + + return newState; }, + updateCommentRequest: (state, { payload }) => ( + { + ...state, + postStatus: RequestStatus.IN_PROGRESS, + commentDraft: payload, + } + ), + updateCommentDenied: (state) => ( + { + ...state, + postStatus: RequestStatus.DENIED, + } + ), + updateCommentFailed: (state) => ( + { + ...state, + postStatus: RequestStatus.FAILED, + } + ), + updateCommentSuccess: (state, { payload }) => ( + { + ...state, + commentsById: { + ...state.commentsById, + [payload.id]: payload, + }, + commentDraft: null, + } + ), updateCommentsList: (state, { payload }) => { const { id: commentId, threadId, endorsed } = payload; const commentAddListtype = endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED; const commentRemoveListType = !endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED; - state.commentsInThreads[threadId][commentRemoveListType] = ( - state.commentsInThreads[threadId]?.[commentRemoveListType]?.filter(item => item !== commentId) - ); - state.commentsInThreads[threadId][commentAddListtype] = [ - ...state.commentsInThreads[threadId][commentAddListtype], payload.id, + const updatedThread = { ...state.commentsInThreads[threadId] }; + + updatedThread[commentRemoveListType] = updatedThread[commentRemoveListType] + ?.filter(item => item !== commentId) + ?? []; + updatedThread[commentAddListtype] = [ + ...(updatedThread[commentAddListtype] || []), commentId, ]; + + return { + ...state, + commentsInThreads: { + ...state.commentsInThreads, + [threadId]: updatedThread, + }, + }; }, - deleteCommentRequest: (state) => { - state.postStatus = RequestStatus.IN_PROGRESS; - }, - deleteCommentDenied: (state) => { - state.postStatus = RequestStatus.DENIED; - }, - deleteCommentFailed: (state) => { - state.postStatus = RequestStatus.FAILED; - }, + deleteCommentRequest: (state) => ( + { + ...state, + postStatus: RequestStatus.IN_PROGRESS, + } + ), + deleteCommentDenied: (state) => ( + { + ...state, + postStatus: RequestStatus.DENIED, + } + ), + deleteCommentFailed: (state) => ( + { + ...state, + postStatus: RequestStatus.FAILED, + } + ), deleteCommentSuccess: (state, { payload }) => { const { commentId } = payload; const { threadId, parentId } = state.commentsById[commentId]; - state.postStatus = RequestStatus.SUCCESSFUL; + const newState = { + ...state, + postStatus: RequestStatus.SUCCESSFUL, + commentsInThreads: { ...state.commentsInThreads }, + commentsInComments: { ...state.commentsInComments }, + commentsById: { ...state.commentsById }, + }; + [EndorsementStatus.DISCUSSION, EndorsementStatus.UNENDORSED, EndorsementStatus.ENDORSED].forEach((endorsed) => { - state.commentsInThreads[threadId][endorsed] = ( - state.commentsInThreads[threadId]?.[endorsed]?.filter(item => item !== commentId) - ); + newState.commentsInThreads[threadId] = { + ...newState.commentsInThreads[threadId], + [endorsed]: newState.commentsInThreads[threadId]?.[endorsed]?.filter(item => item !== commentId), + }; }); + if (parentId) { - state.commentsInComments[parentId] = state.commentsInComments[parentId].filter(item => item !== commentId); + newState.commentsInComments[parentId] = newState.commentsInComments[parentId].filter( + item => item !== commentId, + ); } - delete state.commentsById[commentId]; - }, - setCommentSortOrder: (state, { payload }) => { - state.sortOrder = payload; + + delete newState.commentsById[commentId]; + + return newState; }, + setCommentSortOrder: (state, { payload }) => ( + { + ...state, + sortOrder: payload, + } + ), }, }); diff --git a/src/discussions/post-comments/data/thunks.js b/src/discussions/post-comments/data/thunks.js index 3cb22227f..311774a61 100644 --- a/src/discussions/post-comments/data/thunks.js +++ b/src/discussions/post-comments/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { camelCaseObject } from '@edx/frontend-platform'; import { logError } from '@edx/frontend-platform/logging'; diff --git a/src/discussions/post-comments/index.js b/src/discussions/post-comments/index.js deleted file mode 100644 index 6e9b456b9..000000000 --- a/src/discussions/post-comments/index.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -export { default as PostCommentsView } from './PostCommentsView'; diff --git a/src/discussions/post-comments/postCommentsContext.js b/src/discussions/post-comments/postCommentsContext.js index 89be6d700..6e17a1371 100644 --- a/src/discussions/post-comments/postCommentsContext.js +++ b/src/discussions/post-comments/postCommentsContext.js @@ -1,8 +1,9 @@ -/* eslint-disable import/prefer-default-export */ import React from 'react'; -export const PostCommentsContext = React.createContext({ +const PostCommentsContext = React.createContext({ isClosed: false, postType: 'discussion', postId: '', }); + +export default PostCommentsContext; diff --git a/src/discussions/posts/PostsList.jsx b/src/discussions/posts/PostsList.jsx index 6717ca98f..804e5d8e9 100644 --- a/src/discussions/posts/PostsList.jsx +++ b/src/discussions/posts/PostsList.jsx @@ -10,11 +10,11 @@ import { AppContext } from '@edx/frontend-platform/react'; import { Button, Spinner } from '@edx/paragon'; import { RequestStatus } from '../../data/constants'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { selectConfigLoadingStatus, selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors'; import { fetchUserPosts } from '../learners/data/thunks'; import messages from '../messages'; -import { usePostList } from './data/hooks'; +import usePostList from './data/hooks'; import { selectThreadFilters, selectThreadNextPage, selectThreadSorting, threadsLoadingStatus, } from './data/selectors'; diff --git a/src/discussions/posts/PostsView.jsx b/src/discussions/posts/PostsView.jsx index 883d5052e..b2fa7d208 100644 --- a/src/discussions/posts/PostsView.jsx +++ b/src/discussions/posts/PostsView.jsx @@ -8,12 +8,12 @@ import { useDispatch, useSelector } from 'react-redux'; import SearchInfo from '../../components/SearchInfo'; 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 fetchCourseTopicsV3 from '../in-context-topics/data/thunks'; import { selectTopics } from '../topics/data/selectors'; -import { fetchCourseTopics } from '../topics/data/thunks'; +import fetchCourseTopics from '../topics/data/thunks'; import { handleKeyDown } from '../utils'; import { selectAllThreadsIds, selectTopicThreadsIds } from './data/selectors'; import { setSearchQuery } from './data/slices'; diff --git a/src/discussions/posts/PostsView.test.jsx b/src/discussions/posts/PostsView.test.jsx index d420270c7..8e5f01083 100644 --- a/src/discussions/posts/PostsView.test.jsx +++ b/src/discussions/posts/PostsView.test.jsx @@ -15,12 +15,12 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { getApiBaseUrl, Routes as ROUTES, ThreadType } from '../../data/constants'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; +import executeThunk from '../../test-utils'; import { getCohortsApiUrl } from '../cohorts/data/api'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { fetchConfigSuccess } from '../data/slices'; import { getCoursesApiUrl } from '../learners/data/api'; -import { fetchCourseTopics } from '../topics/data/thunks'; +import fetchCourseTopics from '../topics/data/thunks'; import { getThreadsApiUrl } from './data/api'; import { PostsView } from './index'; diff --git a/src/discussions/posts/data/api.js b/src/discussions/posts/data/api.js index 069ddda10..6b530adec 100644 --- a/src/discussions/posts/data/api.js +++ b/src/discussions/posts/data/api.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import snakeCase from 'lodash.snakecase'; import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform'; diff --git a/src/discussions/posts/data/hooks.js b/src/discussions/posts/data/hooks.js index 8837f9a05..ef434baa2 100644 --- a/src/discussions/posts/data/hooks.js +++ b/src/discussions/posts/data/hooks.js @@ -1,11 +1,10 @@ -/* eslint-disable import/prefer-default-export */ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { selectThreadsByIds } from './selectors'; -export const usePostList = (ids) => { +const usePostList = (ids) => { const posts = useSelector(selectThreadsByIds(ids)); const pinnedPostsIds = []; const unpinnedPostsIds = []; @@ -24,3 +23,5 @@ export const usePostList = (ids) => { return sortedIds; }; + +export default usePostList; diff --git a/src/discussions/posts/data/redux.test.js b/src/discussions/posts/data/redux.test.js index 5d463e5da..17393693b 100644 --- a/src/discussions/posts/data/redux.test.js +++ b/src/discussions/posts/data/redux.test.js @@ -5,7 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getThreadsApiUrl } from './api'; import { createNewThread, fetchThread, fetchThreads, removeThread, updateExistingThread, diff --git a/src/discussions/posts/data/selectors.js b/src/discussions/posts/data/selectors.js index 9693dd123..4e9e85433 100644 --- a/src/discussions/posts/data/selectors.js +++ b/src/discussions/posts/data/selectors.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - import { createSelector } from '@reduxjs/toolkit'; const selectThreads = state => state.threads.threadsById; diff --git a/src/discussions/posts/data/slices.js b/src/discussions/posts/data/slices.js index 7b452f31c..2638dfddb 100644 --- a/src/discussions/posts/data/slices.js +++ b/src/discussions/posts/data/slices.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ import { createSlice } from '@reduxjs/toolkit'; import omitBy from 'lodash/omitBy'; @@ -57,185 +56,326 @@ const threadsSlice = createSlice({ sortedBy: ThreadOrdering.BY_LAST_ACTIVITY, }, reducers: { - fetchLearnerThreadsRequest: (state, { payload }) => { - if (state.author !== payload.author) { - state.pages = []; - state.author = payload.author; + fetchLearnerThreadsRequest: (state, { payload }) => ( + { + ...state, + pages: state.author !== payload.author ? [] : state.pages, + author: state.author !== payload.author ? payload.author : state.author, + status: RequestStatus.IN_PROGRESS, } - state.status = RequestStatus.IN_PROGRESS; - }, - clearPostsPages: (state) => { - state.pages = []; - }, - fetchThreadsRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, + ), + clearPostsPages: (state) => ( + { + ...state, + pages: [], + } + ), + fetchThreadsRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), fetchThreadsSuccess: (state, { payload }) => { const { author, page, ids, threadsById, isFilterChanged, threadsInTopic, avatars, pagination, textSearchRewrite, } = payload; - if (state.author !== author) { - state.pages = []; - state.author = author; + const newState = { ...state }; + + if (newState.author !== author) { + newState.pages = []; + newState.author = author; } - if (!state.pages[page - 1]) { - state.pages[page - 1] = ids; + + const updatedPages = newState.pages.slice(); + if (!updatedPages[page - 1]) { + updatedPages[page - 1] = ids; } else { - state.pages[page - 1] = [...new Set([...state.pages[page - 1], ...ids])]; + updatedPages[page - 1] = [...new Set([...updatedPages[page - 1], ...ids])]; } - state.status = RequestStatus.SUCCESSFUL; - state.threadsById = { ...state.threadsById, ...threadsById }; - state.threadsInTopic = (isFilterChanged || page === 1) + newState.pages = updatedPages; + + newState.status = RequestStatus.SUCCESSFUL; + newState.threadsById = { ...newState.threadsById, ...threadsById }; + newState.threadsInTopic = (isFilterChanged || page === 1) ? { ...threadsInTopic } - : mergeThreadsInTopics(state.threadsInTopic, threadsInTopic); - state.avatars = { ...state.avatars, ...avatars }; - state.nextPage = (page < pagination.numPages) ? page + 1 : null; - state.totalPages = pagination.numPages; - state.totalThreads = pagination.count; - state.textSearchRewrite = textSearchRewrite; - }, - fetchThreadsFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchThreadsDenied: (state) => { - state.status = RequestStatus.DENIED; - }, - fetchThreadRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, - fetchThreadSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - state.threadsById = { ...state.threadsById, ...payload.threadsById }; - state.avatars = { ...state.avatars, ...payload.avatars }; - }, - fetchThreadByDirectLinkSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - state.pages[payload.page - 1] = payload.ids; - state.threadsInTopic = { ...payload.threadsInTopic, ...state.threadsInTopic }; - state.threadsById = { ...payload.threadsById, ...state.threadsById }; - state.avatars = { ...state.avatars, ...payload.avatars }; - }, - fetchThreadFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchThreadDenied: (state) => { - state.status = RequestStatus.DENIED; - }, - postThreadRequest: (state, { payload }) => { - state.postStatus = RequestStatus.IN_PROGRESS; - state.threadDraft = payload; - }, - postThreadSuccess: (state, { payload }) => { - state.postStatus = RequestStatus.SUCCESSFUL; - state.threadsById[payload.id] = payload; - state.threadsInTopic[payload.topicId] = [ - ...(state.threadsInTopic[payload.topicId] || []), - payload.id, - ]; - if (!payload.anonymousToPeers) { - // Temporarily add it to the top of the list so it's visible - state.pages[0] = [payload.id].concat(state.pages[0] || []); - } - state.avatars = { ...state.avatars, ...payload.avatars }; - state.redirectToThread = { topicId: payload.topicId, threadId: payload.id }; - state.threadDraft = null; - }, - postThreadFailed: (state) => { - state.postStatus = RequestStatus.FAILED; - }, - postThreadDenied: (state) => { - state.postStatus = RequestStatus.DENIED; - }, - updateThreadRequest: (state, { payload }) => { - state.postStatus = RequestStatus.IN_PROGRESS; - state.threadDraft = payload; - }, - updateThreadAsRead: (state, { payload }) => { - state.threadsById[payload.threadId] = { ...state.threadsById[payload.threadId], read: true }; - }, - updateThreadSuccess: (state, { payload }) => { - state.postStatus = RequestStatus.SUCCESSFUL; - state.threadsById[payload.id] = { - ...state.threadsById[payload.id], - ...payload, - abuseFlaggedCount: state.threadsById[payload.id].abuseFlaggedCount || false, - }; - state.avatars = { ...state.avatars, ...payload.avatars }; - state.threadDraft = null; - }, - updateThreadFailed: (state) => { - state.postStatus = RequestStatus.FAILED; - state.totalThreads = 0; - }, - updateThreadDenied: (state) => { - state.postStatus = RequestStatus.DENIED; - state.totalThreads = 0; - }, - deleteThreadRequest: (state) => { - state.postStatus = RequestStatus.IN_PROGRESS; + : mergeThreadsInTopics(newState.threadsInTopic, threadsInTopic); + newState.avatars = { ...newState.avatars, ...avatars }; + newState.nextPage = (page < pagination.numPages) ? page + 1 : null; + newState.totalPages = pagination.numPages; + newState.totalThreads = pagination.count; + newState.textSearchRewrite = textSearchRewrite; + + return newState; }, + fetchThreadsFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchThreadsDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), + fetchThreadRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), + fetchThreadSuccess: (state, { payload }) => ( + { + ...state, + status: RequestStatus.SUCCESSFUL, + threadsById: { ...state.threadsById, ...payload.threadsById }, + avatars: { ...state.avatars, ...payload.avatars }, + } + ), + fetchThreadByDirectLinkSuccess: (state, { payload }) => ( + { + ...state, + status: RequestStatus.SUCCESSFUL, + pages: [ + ...state.pages.slice(0, payload.page - 1), + payload.ids, + ...state.pages.slice(payload.page), + ], + threadsInTopic: { ...payload.threadsInTopic, ...state.threadsInTopic }, + threadsById: { ...payload.threadsById, ...state.threadsById }, + avatars: { ...state.avatars, ...payload.avatars }, + } + ), + fetchThreadFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchThreadDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), + postThreadRequest: (state, { payload }) => ( + { + ...state, + postStatus: RequestStatus.IN_PROGRESS, + threadsDraft: payload, + } + ), + postThreadSuccess: (state, { payload }) => ( + { + ...state, + postStatus: RequestStatus.SUCCESSFUL, + threadsById: { + ...state.threadsById, + [payload.id]: payload, + }, + threadsInTopic: { + ...state.threadsInTopic, + [payload.topicId]: [ + ...(state.threadsInTopic[payload.topicId] || []), + payload.id, + ], + }, + pages: !payload.anonymousToPeers + ? [ + ...[payload.id].concat(state.pages[0]) || [], + ...state.pages.slice(1), + ] + : [...state.pages], + avatars: { ...state.avatars, ...payload.avatars }, + redirectToThread: { topicId: payload.topicId, threadId: payload.id }, + threadDraft: null, + } + ), + postThreadFailed: (state) => ( + { + ...state, + postStatus: RequestStatus.FAILED, + } + ), + postThreadDenied: (state) => ( + { + ...state, + postStatus: RequestStatus.DENIED, + } + ), + updateThreadRequest: (state, { payload }) => ( + { + ...state, + postStatus: RequestStatus.IN_PROGRESS, + threadsDraft: payload, + } + ), + updateThreadAsRead: (state, { payload }) => ( + { + ...state, + threadsById: { + ...state.threadsById, + [payload.threadId]: { + ...state.threadsById[payload.threadId], + read: true, + }, + }, + } + ), + updateThreadSuccess: (state, { payload }) => ( + { + ...state, + postStatus: RequestStatus.SUCCESSFUL, + threadsById: { + ...state.threadsById, + [payload.id]: { + ...state.threadsById[payload.id], + ...payload, + abuseFlaggedCount: state.threadsById[payload.id].abuseFlaggedCount || false, + }, + }, + avatars: { ...state.avatars, ...payload.avatars }, + threadDraft: null, + } + ), + updateThreadFailed: (state) => ( + { + ...state, + postStatus: RequestStatus.FAILED, + totalThreads: 0, + } + ), + updateThreadDenied: (state) => ( + { + ...state, + postStatus: RequestStatus.DENIED, + totalThreads: 0, + } + ), + deleteThreadRequest: (state) => ( + { + ...state, + postStatus: RequestStatus.IN_PROGRESS, + } + ), deleteThreadSuccess: (state, { payload }) => { const { threadId } = payload; const { topicId } = state.threadsById[threadId]; - state.postStatus = RequestStatus.SUCCESSFUL; - state.threadsInTopic[topicId] = state.threadsInTopic[topicId].filter(item => item !== threadId); - state.pages = state.pages.map(page => page?.filter(item => item !== threadId)); - state.threadsById = omitBy(state.threadsById, (thread) => thread.id === threadId); - }, - deleteThreadFailed: (state) => { - state.postStatus = RequestStatus.FAILED; - }, - deleteThreadDenied: (state) => { - state.postStatus = RequestStatus.DENIED; - }, - setSortedBy: (state, { payload }) => { - state.sortedBy = payload; - state.pages = []; - }, - setStatusFilter: (state, { payload }) => { - state.filters.status = payload; - state.pages = []; - }, - setPostsTypeFilter: (state, { payload }) => { - state.filters.postType = payload; - state.pages = []; - }, - setCohortFilter: (state, { payload }) => { - state.filters.cohort = payload; - state.pages = []; - }, - setSearchQuery: (state, { payload }) => { - state.filters.search = payload; - // Search doesn't work with following - state.filters.status = state.filters.status === PostsStatusFilter.FOLLOWING - ? PostsStatusFilter.ALL - : state.filters.status; - state.pages = []; - }, - showPostEditor: (state) => { - state.postEditorVisible = true; - state.redirectToThread = null; - }, - hidePostEditor: (state) => { - state.postEditorVisible = false; - }, - clearRedirect: (state) => { - state.redirectToThread = null; - }, - clearFilter: (state) => { - state.filters = { - status: PostsStatusFilter.ALL, - postType: ThreadType.ALL, - cohort: '', - search: '', + return { + ...state, + postStatus: RequestStatus.SUCCESSFUL, + threadsInTopic: { + ...state.threadsInTopic, + [topicId]: state.threadsInTopic[topicId].filter(item => item !== threadId), + }, + pages: state.pages.map(page => page?.filter(item => item !== threadId)), + threadsById: omitBy(state.threadsById, (thread) => thread.id === threadId), }; - state.pages = []; - }, - clearSort: (state) => { - state.sortedBy = ThreadOrdering.BY_LAST_ACTIVITY; - state.pages = []; }, + deleteThreadFailed: (state) => ( + { + ...state, + postStatus: RequestStatus.FAILED, + } + ), + deleteThreadDenied: (state) => ( + { + ...state, + postStatus: RequestStatus.DENIED, + } + ), + setSortedBy: (state, { payload }) => ( + { + ...state, + sortedBy: payload, + pages: [], + } + ), + setStatusFilter: (state, { payload }) => ( + { + ...state, + filters: { + ...state.filters, + status: payload, + }, + pages: [], + } + ), + setPostsTypeFilter: (state, { payload }) => ( + { + ...state, + filters: { + ...state.filters, + postType: payload, + }, + pages: [], + } + ), + setCohortFilter: (state, { payload }) => ( + { + ...state, + filters: { + ...state.filters, + cohort: payload, + }, + pages: [], + } + ), + setSearchQuery: (state, { payload }) => ( + { + ...state, + filters: { + ...state.filters, + search: payload, + status: state.filters.status === PostsStatusFilter.FOLLOWING + ? PostsStatusFilter.ALL + : state.filters.status, + }, + pages: [], + } + ), + showPostEditor: (state) => ( + { + ...state, + postEditorVisible: true, + redirectToThread: null, + } + ), + hidePostEditor: (state) => ( + { + ...state, + postEditorVisible: false, + } + ), + clearRedirect: (state) => ( + { + ...state, + redirectToThread: null, + } + ), + clearFilter: (state) => ( + { + ...state, + filters: { + ...state.filters, + status: PostsStatusFilter.ALL, + postType: ThreadType.ALL, + cohort: '', + search: '', + }, + pages: [], + } + ), + clearSort: (state) => ( + { + ...state, + sortedBy: ThreadOrdering.BY_LAST_ACTIVITY, + pages: [], + } + ), }, }); diff --git a/src/discussions/posts/data/thunks.js b/src/discussions/posts/data/thunks.js index 691967ece..7276dd1d3 100644 --- a/src/discussions/posts/data/thunks.js +++ b/src/discussions/posts/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { camelCaseObject } from '@edx/frontend-platform'; import { logError } from '@edx/frontend-platform/logging'; diff --git a/src/discussions/posts/index.js b/src/discussions/posts/index.js index 08a7c4d88..f7d3c75dc 100644 --- a/src/discussions/posts/index.js +++ b/src/discussions/posts/index.js @@ -1,7 +1,4 @@ -/* eslint-disable import/prefer-default-export */ export { showPostEditor } from './data'; export { default as Post } from './post/Post'; export { default as messages } from './post-actions-bar/messages'; -// eslint-disable-next-line import/no-cycle -export { default as PostEditor } from './post-editor/PostEditor'; export { default as PostsView } from './PostsView'; diff --git a/src/discussions/posts/post-actions-bar/PostActionsBar.jsx b/src/discussions/posts/post-actions-bar/PostActionsBar.jsx index adb487948..0a3a81961 100644 --- a/src/discussions/posts/post-actions-bar/PostActionsBar.jsx +++ b/src/discussions/posts/post-actions-bar/PostActionsBar.jsx @@ -11,7 +11,7 @@ import { Close } from '@edx/paragon/icons'; import Search from '../../../components/Search'; import { RequestStatus } from '../../../data/constants'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import { useUserPostingEnabled } from '../../data/hooks'; import { selectConfigLoadingStatus, selectEnableInContext } from '../../data/selectors'; import { TopicSearchBar as IncontextSearch } from '../../in-context-topics/topic-search'; diff --git a/src/discussions/posts/post-editor/PostEditor.jsx b/src/discussions/posts/post-editor/PostEditor.jsx index e626c3581..6f4275833 100644 --- a/src/discussions/posts/post-editor/PostEditor.jsx +++ b/src/discussions/posts/post-editor/PostEditor.jsx @@ -19,10 +19,10 @@ import { Help, Post } from '@edx/paragon/icons'; import { TinyMCEEditor } from '../../../components'; import FormikErrorFeedback from '../../../components/FormikErrorFeedback'; import PostPreviewPanel from '../../../components/PostPreviewPanel'; -import { useDispatchWithState } from '../../../data/hooks'; -import { selectCourseCohorts } from '../../cohorts/data/selectors'; -import { fetchCourseCohorts } from '../../cohorts/data/thunks'; -import { DiscussionContext } from '../../common/context'; +import useDispatchWithState from '../../../data/hooks'; +import selectCourseCohorts from '../../cohorts/data/selectors'; +import fetchCourseCohorts from '../../cohorts/data/thunks'; +import DiscussionContext from '../../common/context'; import { useCurrentDiscussionTopic } from '../../data/hooks'; import { selectAnonymousPostingConfig, @@ -33,8 +33,7 @@ import { selectUserIsGroupTa, selectUserIsStaff, } from '../../data/selectors'; -// eslint-disable-next-line import/no-cycle -import { EmptyPage } from '../../empty-posts'; +import EmptyPage from '../../empty-posts/EmptyPage'; import { selectArchivedTopics, selectCoursewareTopics as inContextCourseware, diff --git a/src/discussions/posts/post-editor/PostEditor.test.jsx b/src/discussions/posts/post-editor/PostEditor.test.jsx index ff75a5bdf..c72678485 100644 --- a/src/discussions/posts/post-editor/PostEditor.test.jsx +++ b/src/discussions/posts/post-editor/PostEditor.test.jsx @@ -16,13 +16,13 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { getApiBaseUrl, Routes as ROUTES } from '../../../data/constants'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCohortsApiUrl } from '../../cohorts/data/api'; -import { DiscussionContext } from '../../common/context'; -import { fetchCourseTopics } from '../../topics/data/thunks'; +import DiscussionContext from '../../common/context'; +import fetchCourseTopics from '../../topics/data/thunks'; import { getThreadsApiUrl } from '../data/api'; import { fetchThread } from '../data/thunks'; -import { PostEditor } from '../index'; +import PostEditor from './PostEditor'; import '../../cohorts/data/__factories__'; import '../../data/__factories__'; diff --git a/src/discussions/posts/post-editor/PostTypeCard.jsx b/src/discussions/posts/post-editor/PostTypeCard.jsx index 4db1968c9..4c6fe96dd 100644 --- a/src/discussions/posts/post-editor/PostTypeCard.jsx +++ b/src/discussions/posts/post-editor/PostTypeCard.jsx @@ -5,7 +5,7 @@ import classNames from 'classnames'; import { Card, Form } from '@edx/paragon'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; const PostTypeCard = ({ value, diff --git a/src/discussions/posts/post-filter-bar/PostFilterBar.jsx b/src/discussions/posts/post-filter-bar/PostFilterBar.jsx index 4cc172d78..af4d71789 100644 --- a/src/discussions/posts/post-filter-bar/PostFilterBar.jsx +++ b/src/discussions/posts/post-filter-bar/PostFilterBar.jsx @@ -19,9 +19,9 @@ import { PostsStatusFilter, RequestStatus, ThreadOrdering, ThreadType, } from '../../../data/constants'; -import { selectCourseCohorts } from '../../cohorts/data/selectors'; -import { fetchCourseCohorts } from '../../cohorts/data/thunks'; -import { DiscussionContext } from '../../common/context'; +import selectCourseCohorts from '../../cohorts/data/selectors'; +import fetchCourseCohorts from '../../cohorts/data/thunks'; +import DiscussionContext from '../../common/context'; import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors'; import { setCohortFilter, setPostsTypeFilter, setSortedBy, setStatusFilter, diff --git a/src/discussions/posts/post/LikeButton.jsx b/src/discussions/posts/post/LikeButton.jsx index afc4b1a60..68c97b986 100644 --- a/src/discussions/posts/post/LikeButton.jsx +++ b/src/discussions/posts/post/LikeButton.jsx @@ -5,8 +5,8 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, IconButton, OverlayTrigger, Tooltip, } from '@edx/paragon'; +import { ThumbUpFilled, ThumbUpOutline } from '@edx/paragon/icons'; -import { ThumbUpFilled, ThumbUpOutline } from '../../../components/icons'; import messages from './messages'; const LikeButton = ({ count, onClick, voted }) => { diff --git a/src/discussions/posts/post/Post.jsx b/src/discussions/posts/post/Post.jsx index 7e5464002..821702332 100644 --- a/src/discussions/posts/post/Post.jsx +++ b/src/discussions/posts/post/Post.jsx @@ -14,7 +14,7 @@ import HTMLLoader from '../../../components/HTMLLoader'; import { ContentActions, getFullUrl } from '../../../data/constants'; import { selectorForUnitSubsection, selectTopicContext } from '../../../data/selectors'; import { AlertBanner, Confirmation } from '../../common'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import HoverCard from '../../common/HoverCard'; import { ContentTypes } from '../../data/constants'; import { selectUserHasModerationPrivileges } from '../../data/selectors'; diff --git a/src/discussions/posts/post/PostFooter.jsx b/src/discussions/posts/post/PostFooter.jsx index 131e60bcd..ceb68a309 100644 --- a/src/discussions/posts/post/PostFooter.jsx +++ b/src/discussions/posts/post/PostFooter.jsx @@ -7,9 +7,10 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon, IconButton, OverlayTrigger, Tooltip, } from '@edx/paragon'; -import { Locked, People } from '@edx/paragon/icons'; +import { + Locked, People, StarFilled, StarOutline, +} from '@edx/paragon/icons'; -import { StarFilled, StarOutline } from '../../../components/icons'; import { updateExistingThread } from '../data/thunks'; import LikeButton from './LikeButton'; import messages from './messages'; diff --git a/src/discussions/posts/post/PostHeader.jsx b/src/discussions/posts/post/PostHeader.jsx index 5708dc51e..7e6c8a123 100644 --- a/src/discussions/posts/post/PostHeader.jsx +++ b/src/discussions/posts/post/PostHeader.jsx @@ -5,8 +5,8 @@ import classNames from 'classnames'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Avatar, Badge, Icon } from '@edx/paragon'; +import { Issue, Question } from '@edx/paragon/icons'; -import { Issue, Question } from '../../../components/icons'; import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants'; import { AuthorLabel } from '../../common'; import { useAlertBannerVisible } from '../../data/hooks'; diff --git a/src/discussions/posts/post/PostLink.jsx b/src/discussions/posts/post/PostLink.jsx index 5035c35fa..95b9a2e92 100644 --- a/src/discussions/posts/post/PostLink.jsx +++ b/src/discussions/posts/post/PostLink.jsx @@ -7,12 +7,11 @@ import { Link, useLocation } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Badge, Icon } from '@edx/paragon'; -import { CheckCircle } from '@edx/paragon/icons'; +import { CheckCircle, PushPin } from '@edx/paragon/icons'; -import { PushPin } from '../../../components/icons'; import { AvatarOutlineAndLabelColors, Routes, ThreadType } from '../../../data/constants'; import AuthorLabel from '../../common/AuthorLabel'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import { discussionsPath, isPostPreviewAvailable } from '../../utils'; import { selectThread } from '../data/selectors'; import messages from './messages'; diff --git a/src/discussions/posts/post/PostLink.test.jsx b/src/discussions/posts/post/PostLink.test.jsx index 5f1501cf1..18d39795d 100644 --- a/src/discussions/posts/post/PostLink.test.jsx +++ b/src/discussions/posts/post/PostLink.test.jsx @@ -10,10 +10,10 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; -import { DiscussionContext } from '../../common/context'; +import executeThunk from '../../../test-utils'; +import DiscussionContext from '../../common/context'; import { getCourseConfigApiUrl } from '../../data/api'; -import { fetchCourseConfig } from '../../data/thunks'; +import fetchCourseConfig from '../../data/thunks'; import { getThreadsApiUrl } from '../data/api'; import { fetchThread } from '../data/thunks'; import PostLink from './PostLink'; diff --git a/src/discussions/posts/post/PostSummaryFooter.jsx b/src/discussions/posts/post/PostSummaryFooter.jsx index 1c288add2..7c4df9bf1 100644 --- a/src/discussions/posts/post/PostSummaryFooter.jsx +++ b/src/discussions/posts/post/PostSummaryFooter.jsx @@ -9,10 +9,10 @@ import { Badge, Icon, OverlayTrigger, Tooltip, } from '@edx/paragon'; import { + People, QuestionAnswer, QuestionAnswerOutline, StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline, } from '@edx/paragon/icons'; -import { People, QuestionAnswer, QuestionAnswerOutline } from '../../../components/icons'; import timeLocale from '../../common/time-locale'; import { selectUserHasModerationPrivileges } from '../../data/selectors'; import messages from './messages'; diff --git a/src/discussions/posts/post/index.js b/src/discussions/posts/post/index.js index d8275ae4d..d41c94fcb 100644 --- a/src/discussions/posts/post/index.js +++ b/src/discussions/posts/post/index.js @@ -1,3 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export { default as Post } from './Post'; export { default as PostLink } from './PostLink'; diff --git a/src/discussions/posts/post/proptypes.js b/src/discussions/posts/post/proptypes.js deleted file mode 100644 index 2731ca5ba..000000000 --- a/src/discussions/posts/post/proptypes.js +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import PropTypes from 'prop-types'; - -export const postShape = PropTypes.shape({ - abuseFlagged: PropTypes.bool, - author: PropTypes.string, - commentCount: PropTypes.number, - courseId: PropTypes.string, - following: PropTypes.bool, - id: PropTypes.string, - pinned: PropTypes.bool, - rawBody: PropTypes.string, - hasEndorsed: PropTypes.bool, - previewBody: PropTypes.string, - read: PropTypes.bool, - title: PropTypes.string, - topicId: PropTypes.string, - type: PropTypes.string, - updatedAt: PropTypes.string, -}); diff --git a/src/discussions/topics/TopicsView.jsx b/src/discussions/topics/TopicsView.jsx index 4de8ebc42..5ea4622d2 100644 --- a/src/discussions/topics/TopicsView.jsx +++ b/src/discussions/topics/TopicsView.jsx @@ -7,13 +7,13 @@ import { useParams } from 'react-router-dom'; import SearchInfo from '../../components/SearchInfo'; import { RequestStatus } from '../../data/constants'; -import { DiscussionContext } from '../common/context'; +import DiscussionContext from '../common/context'; import { selectDiscussionProvider } from '../data/selectors'; import NoResults from '../posts/NoResults'; import { handleKeyDown } from '../utils'; import { selectCategories, selectNonCoursewareTopics, selectTopicFilter } from './data/selectors'; import { setFilter, setTopicsCount } from './data/slices'; -import { fetchCourseTopics } from './data/thunks'; +import fetchCourseTopics from './data/thunks'; import LegacyTopicGroup from './topic-group/LegacyTopicGroup'; import Topic from './topic-group/topic/Topic'; import countFilteredTopics from './utils'; diff --git a/src/discussions/topics/TopicsView.test.jsx b/src/discussions/topics/TopicsView.test.jsx index 842dba95c..27f469468 100644 --- a/src/discussions/topics/TopicsView.test.jsx +++ b/src/discussions/topics/TopicsView.test.jsx @@ -14,10 +14,10 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { getApiBaseUrl } from '../../data/constants'; import { initializeStore } from '../../store'; -import { executeThunk } from '../../test-utils'; -import { DiscussionContext } from '../common/context'; +import executeThunk from '../../test-utils'; +import DiscussionContext from '../common/context'; import { selectCoursewareTopics, selectNonCoursewareTopics } from './data/selectors'; -import { fetchCourseTopics } from './data/thunks'; +import fetchCourseTopics from './data/thunks'; import TopicsView from './TopicsView'; import './data/__factories__'; diff --git a/src/discussions/topics/data/api.js b/src/discussions/topics/data/api.js index 31d043cf4..fda05b438 100644 --- a/src/discussions/topics/data/api.js +++ b/src/discussions/topics/data/api.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getApiBaseUrl } from '../../../data/constants'; diff --git a/src/discussions/topics/data/api.test.js b/src/discussions/topics/data/api.test.js index 72f68e683..16b5dd15c 100644 --- a/src/discussions/topics/data/api.test.js +++ b/src/discussions/topics/data/api.test.js @@ -5,9 +5,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getCourseTopics, getCourseTopicsApiUrl } from './api'; -import { fetchCourseTopics } from './thunks'; +import fetchCourseTopics from './thunks'; import './__factories__'; diff --git a/src/discussions/topics/data/selectors.js b/src/discussions/topics/data/selectors.js index 3093ccde0..b48627369 100644 --- a/src/discussions/topics/data/selectors.js +++ b/src/discussions/topics/data/selectors.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - import { createSelector } from '@reduxjs/toolkit'; export const selectTopicFilter = state => state.topics.filter.trim() diff --git a/src/discussions/topics/data/slices.js b/src/discussions/topics/data/slices.js index e022719ce..4086411b9 100644 --- a/src/discussions/topics/data/slices.js +++ b/src/discussions/topics/data/slices.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ import { createSlice } from '@reduxjs/toolkit'; import { RequestStatus, TopicOrdering } from '../../../data/constants'; @@ -22,31 +21,52 @@ const topicsSlice = createSlice({ }, }, reducers: { - fetchCourseTopicsRequest: (state) => { - state.status = RequestStatus.IN_PROGRESS; - }, - fetchCourseTopicsSuccess: (state, { payload }) => { - state.status = RequestStatus.SUCCESSFUL; - state.topics = payload.topics; - state.nonCoursewareIds = payload.nonCoursewareIds; - state.categoryIds = payload.categoryIds; - state.topicsInCategory = payload.topicsInCategory; - }, - fetchCourseTopicsFailed: (state) => { - state.status = RequestStatus.FAILED; - }, - fetchCourseTopicsDenied: (state) => { - state.status = RequestStatus.DENIED; - }, - setFilter: (state, { payload }) => { - state.filter = payload; - }, - setSortBy: (state, { payload }) => { - state.sortBy = payload; - }, - setTopicsCount: (state, { payload }) => { - state.results.count = payload; - }, + fetchCourseTopicsRequest: (state) => ( + { + ...state, + status: RequestStatus.IN_PROGRESS, + } + ), + fetchCourseTopicsSuccess: (state, { payload }) => ( + { + ...state, + status: RequestStatus.SUCCESSFUL, + topics: payload.topics, + nonCoursewareIds: payload.nonCoursewareIds, + categoryIds: payload.categoryIds, + topicsInCategory: payload.topicsInCategory, + } + ), + fetchCourseTopicsFailed: (state) => ( + { + ...state, + status: RequestStatus.FAILED, + } + ), + fetchCourseTopicsDenied: (state) => ( + { + ...state, + status: RequestStatus.DENIED, + } + ), + setFilter: (state, { payload }) => ( + { + ...state, + filter: payload, + } + ), + setSortBy: (state, { payload }) => ( + { + ...state, + sortBy: payload, + } + ), + setTopicsCount: (state, { payload }) => ( + { + ...state, + results: { ...state.results, count: payload }, + } + ), }, }); diff --git a/src/discussions/topics/data/thunks.js b/src/discussions/topics/data/thunks.js index cbce1c8c9..091f20c6b 100644 --- a/src/discussions/topics/data/thunks.js +++ b/src/discussions/topics/data/thunks.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { camelCaseObject } from '@edx/frontend-platform'; import { logError } from '@edx/frontend-platform/logging'; @@ -28,7 +27,7 @@ function normaliseTopics(data) { }; } -export function fetchCourseTopics(courseId) { +export default function fetchCourseTopics(courseId) { return async (dispatch) => { try { dispatch(fetchCourseTopicsRequest({ courseId })); diff --git a/src/discussions/topics/index.js b/src/discussions/topics/index.js deleted file mode 100644 index a6c2e11f2..000000000 --- a/src/discussions/topics/index.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -export { default as TopicsView } from './TopicsView'; diff --git a/src/discussions/topics/topic-group/TopicGroupBase.jsx b/src/discussions/topics/topic-group/TopicGroupBase.jsx index 08861e49f..cf6cab52b 100644 --- a/src/discussions/topics/topic-group/TopicGroupBase.jsx +++ b/src/discussions/topics/topic-group/TopicGroupBase.jsx @@ -7,7 +7,7 @@ import { Link, useLocation } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Routes } from '../../../data/constants'; -import { DiscussionContext } from '../../common/context'; +import DiscussionContext from '../../common/context'; import { discussionsPath } from '../../utils'; import { selectTopicFilter, selectTopicsById } from '../data/selectors'; import messages from '../messages'; @@ -44,8 +44,7 @@ const TopicGroupBase = ({ const renderFilteredTopics = useMemo(() => { if (!hasFilteredSubtopics) { - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>; + return null; } return ( diff --git a/src/discussions/topics/topic-group/topic/Topic.jsx b/src/discussions/topics/topic-group/topic/Topic.jsx index 838998e9d..d3c41f1e9 100644 --- a/src/discussions/topics/topic-group/topic/Topic.jsx +++ b/src/discussions/topics/topic-group/topic/Topic.jsx @@ -1,5 +1,3 @@ -/* eslint-disable react/prop-types */ -/* eslint-disable no-unused-vars, react/forbid-prop-types */ import React, { useCallback, useContext } from 'react'; import PropTypes from 'prop-types'; @@ -12,7 +10,7 @@ import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon'; import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons'; import { Routes } from '../../../../data/constants'; -import { DiscussionContext } from '../../../common/context'; +import DiscussionContext from '../../../common/context'; import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../../data/selectors'; import { discussionsPath } from '../../../utils'; import { selectTopic } from '../../data/selectors'; diff --git a/src/discussions/tours/data/redux.test.js b/src/discussions/tours/data/redux.test.js index eb22fadae..3f011cae8 100644 --- a/src/discussions/tours/data/redux.test.js +++ b/src/discussions/tours/data/redux.test.js @@ -5,9 +5,9 @@ import { initializeMockApp } from '@edx/frontend-platform/testing'; import { RequestStatus } from '../../../data/constants'; import { initializeStore } from '../../../store'; -import { executeThunk } from '../../../test-utils'; +import executeThunk from '../../../test-utils'; import { getDiscussionTourUrl } from './api'; -import { selectTours } from './selectors'; +import selectTours from './selectors'; import { discussionsTourRequest, discussionsToursRequestError, diff --git a/src/discussions/tours/data/selectors.js b/src/discussions/tours/data/selectors.js index 2a65909f3..e20b3acf2 100644 --- a/src/discussions/tours/data/selectors.js +++ b/src/discussions/tours/data/selectors.js @@ -1,2 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export -export const selectTours = (state) => state.tours.tours; +const selectTours = (state) => state.tours.tours; + +export default selectTours; diff --git a/src/discussions/tours/data/slices.js b/src/discussions/tours/data/slices.js index 17e28e944..fecd90fe7 100644 --- a/src/discussions/tours/data/slices.js +++ b/src/discussions/tours/data/slices.js @@ -1,5 +1,3 @@ -/* eslint-disable no-param-reassign,import/prefer-default-export */ - import { createSlice } from '@reduxjs/toolkit'; import { RequestStatus } from '../../../data/constants'; @@ -12,30 +10,49 @@ const userDiscussionsToursSlice = createSlice({ error: null, }, reducers: { - discussionsTourRequest: (state) => { - state.loading = RequestStatus.IN_PROGRESS; - state.error = null; - }, - fetchUserDiscussionsToursSuccess: (state, action) => { - state.tours = action.payload; - state.loading = RequestStatus.SUCCESSFUL; - state.error = null; - }, - discussionsToursRequestError: (state, action) => { - state.loading = RequestStatus.FAILED; - state.error = action.payload; - }, + discussionsTourRequest: (state) => ( + { + ...state, + loading: RequestStatus.IN_PROGRESS, + error: null, + } + ), + fetchUserDiscussionsToursSuccess: (state, action) => ( + { + ...state, + tours: action.payload, + loading: RequestStatus.SUCCESSFUL, + error: null, + } + ), + discussionsToursRequestError: (state, action) => ( + { + ...state, + loading: RequestStatus.FAILED, + error: action.payload, + } + ), updateUserDiscussionsTourSuccess: (state, action) => { const tourIndex = state.tours.findIndex(tour => tour.id === action.payload.id); - state.tours[tourIndex] = action.payload; - state.loading = RequestStatus.SUCCESSFUL; - state.error = null; + return { + ...state, + tours: state.tours.map( + (tour, index) => (index === tourIndex ? action.payload : tour), + ), + loading: RequestStatus.SUCCESSFUL, + error: null, + }; }, updateUserDiscussionsTourByName: (state, action) => { const tourIndex = state.tours.findIndex(tour => tour.tourName === action.payload.tourName); - state.tours[tourIndex] = { ...state.tours[tourIndex], ...action.payload }; - state.loading = RequestStatus.SUCCESSFUL; - state.error = null; + return { + ...state, + tours: state.tours.map( + (tour, index) => (index === tourIndex ? { ...state.tours[tourIndex], ...action.payload } : tour), + ), + loading: RequestStatus.SUCCESSFUL, + error: null, + }; }, }, }); diff --git a/src/discussions/utils.js b/src/discussions/utils.js index 64e0eb8cb..77fcd6b47 100644 --- a/src/discussions/utils.js +++ b/src/discussions/utils.js @@ -9,15 +9,15 @@ import { import { getConfig } from '@edx/frontend-platform'; import { - CheckCircle, CheckCircleOutline, Delete, Edit, Lock, LockOpen, Pin, Report, Verified, VerifiedOutline, + CheckCircle, CheckCircleOutline, Delete, Edit, InsertLink, + Lock, LockOpen, Pin, Report, Verified, VerifiedOutline, } from '@edx/paragon/icons'; -import { InsertLink } from '../components/icons'; import { ContentActions, Routes, ThreadType, } from '../data/constants'; import { ContentSelectors } from './data/constants'; -import { PostCommentsContext } from './post-comments/postCommentsContext'; +import PostCommentsContext from './post-comments/postCommentsContext'; import messages from './messages'; /** diff --git a/src/setupTest.js b/src/setupTest.jsx similarity index 96% rename from src/setupTest.js rename to src/setupTest.jsx index 6e65f16ef..261398e07 100755 --- a/src/setupTest.js +++ b/src/setupTest.jsx @@ -24,7 +24,6 @@ const MockEditor = ({ onBlur, onEditorChange, }) => ( - // eslint-disable-next-line react/jsx-filename-extension