From 59187d2217f3a67a268521164006434053ff522f Mon Sep 17 00:00:00 2001
From: Awais Ansari <79941147+awais-ansari@users.noreply.github.com>
Date: Mon, 20 Jun 2022 16:14:13 +0500
Subject: [PATCH] feat: implement learners area new UI (#197)
* feat: implement learners area new UI
* fix: learners list UI
* fix: initial learner sort based on role
---
src/data/constants.js | 22 +--
src/discussions/comments/data/api.js | 26 ---
src/discussions/data/thunks.js | 7 +
.../discussions-home/DiscussionContent.jsx | 8 -
.../discussions-home/DiscussionSidebar.jsx | 3 +-
.../discussions-home/DiscussionsHome.jsx | 8 +-
src/discussions/empty-posts/EmptyLearners.jsx | 25 +++
src/discussions/empty-posts/index.js | 1 +
.../learners/LearnerPageHeader.jsx | 70 -------
src/discussions/learners/LearnerPostsView.jsx | 107 +++++++++++
.../learners/LearnersContentView.jsx | 53 ------
.../learners/LearnersContentView.test.jsx | 178 ------------------
src/discussions/learners/LearnersView.jsx | 11 +-
src/discussions/learners/data/api.js | 28 ++-
src/discussions/learners/data/selectors.js | 28 ---
src/discussions/learners/data/slices.js | 51 -----
src/discussions/learners/data/thunks.js | 80 +++-----
src/discussions/learners/index.js | 2 +-
.../learners/learner/CommentsTabContent.jsx | 51 -----
.../learners/learner/LearnerAvatar.jsx | 9 +-
.../learners/learner/LearnerCard.jsx | 47 +++--
.../learners/learner/LearnerFilterBar.jsx | 116 ++++++++++++
.../learners/learner/LearnerFooter.jsx | 74 ++++----
.../learners/learner/LearnerFooter.test.jsx | 2 +-
.../learners/learner/PostsTabContent.jsx | 56 ------
src/discussions/learners/learner/index.js | 1 +
src/discussions/learners/learner/messages.js | 23 ---
src/discussions/learners/messages.js | 54 ++++--
src/discussions/posts/PostsView.jsx | 22 ++-
src/discussions/posts/data/api.js | 18 --
src/discussions/posts/data/slices.js | 8 +
src/discussions/posts/data/thunks.js | 2 +-
src/discussions/posts/post/PostLink.jsx | 2 +
src/index.scss | 4 +
34 files changed, 460 insertions(+), 737 deletions(-)
create mode 100644 src/discussions/empty-posts/EmptyLearners.jsx
delete mode 100644 src/discussions/learners/LearnerPageHeader.jsx
create mode 100644 src/discussions/learners/LearnerPostsView.jsx
delete mode 100644 src/discussions/learners/LearnersContentView.jsx
delete mode 100644 src/discussions/learners/LearnersContentView.test.jsx
delete mode 100644 src/discussions/learners/learner/CommentsTabContent.jsx
create mode 100644 src/discussions/learners/learner/LearnerFilterBar.jsx
delete mode 100644 src/discussions/learners/learner/PostsTabContent.jsx
delete mode 100644 src/discussions/learners/learner/messages.js
diff --git a/src/data/constants.js b/src/data/constants.js
index 3869b2b6..87cce239 100644
--- a/src/data/constants.js
+++ b/src/data/constants.js
@@ -134,17 +134,6 @@ export const LearnersOrdering = {
BY_LAST_ACTIVITY: 'activity',
};
-/**
- * Enum for Learner content tabs
- * @readonly
- * @enum {string}
- */
-export const LearnerTabs = {
- POSTS: 'posts',
- COMMENTS: 'comments',
- RESPONSES: 'responses',
-};
-
/**
* Enum for discussion provider types supported by the MFE.
* @type {{OPEN_EDX: string, LEGACY: string}}
@@ -162,12 +151,7 @@ export const Routes = {
},
LEARNERS: {
PATH: `${BASE_PATH}/learners`,
- LEARNER: `${BASE_PATH}/learners/:learnerUsername`,
- TABS: {
- posts: `${BASE_PATH}/learners/:learnerUsername/${LearnerTabs.POSTS}`,
- responses: `${BASE_PATH}/learners/:learnerUsername/${LearnerTabs.RESPONSES}`,
- comments: `${BASE_PATH}/learners/:learnerUsername/${LearnerTabs.COMMENTS}`,
- },
+ POSTS: `${BASE_PATH}/learners/:learnerUsername/posts(/:postId)?`,
},
POSTS: {
PATH: `${BASE_PATH}/topics/:topicId`,
@@ -191,6 +175,7 @@ export const Routes = {
`${BASE_PATH}/topics/:topicId/posts/:postId`,
`${BASE_PATH}/posts/:postId`,
`${BASE_PATH}/my-posts/:postId`,
+ `${BASE_PATH}/learners/:learnerUsername/posts/:postId`,
],
PAGE: `${BASE_PATH}/:page`,
PAGES: {
@@ -198,6 +183,7 @@ export const Routes = {
topics: `${BASE_PATH}/topics/:topicId/posts/:postId`,
posts: `${BASE_PATH}/posts/:postId`,
'my-posts': `${BASE_PATH}/my-posts/:postId`,
+ learners: `${BASE_PATH}/learners/:learnerUsername/posts/:postId`,
},
},
TOPICS: {
@@ -218,5 +204,5 @@ export const ALL_ROUTES = []
.concat(Routes.COMMENTS.PATH)
.concat(Routes.TOPICS.PATH)
.concat([Routes.POSTS.ALL_POSTS, Routes.POSTS.MY_POSTS])
- .concat([Routes.LEARNERS.LEARNER, Routes.LEARNERS.PATH])
+ .concat([Routes.LEARNERS.POSTS, Routes.LEARNERS.PATH])
.concat([Routes.DISCUSSIONS.PATH]);
diff --git a/src/discussions/comments/data/api.js b/src/discussions/comments/data/api.js
index 6adac30e..c8e37d7e 100644
--- a/src/discussions/comments/data/api.js
+++ b/src/discussions/comments/data/api.js
@@ -117,29 +117,3 @@ export async function deleteComment(commentId) {
await getAuthenticatedHttpClient()
.delete(url);
}
-
-/**
- * Get the comments by a specific user in a course's discussions
- *
- * comments = responses + comments in the UI
- *
- * @param {string} courseId Course ID for the course
- * @param {string} username Username of the user
- * @returns API response in the format
- * {
- * results: [array of comments],
- * pagination: {count, num_pages, next, previous}
- * }
-
- */
-export async function getUserComments(courseId, username, { page }) {
- const { data } = await getAuthenticatedHttpClient()
- .get(commentsApiUrl, {
- params: {
- course_id: courseId,
- username,
- page,
- },
- });
- return data;
-}
diff --git a/src/discussions/data/thunks.js b/src/discussions/data/thunks.js
index c6e9aa92..e0ff9e69 100644
--- a/src/discussions/data/thunks.js
+++ b/src/discussions/data/thunks.js
@@ -2,6 +2,8 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
+import { LearnersOrdering } from '../../data/constants';
+import { setSortedBy } from '../learners/data';
import { getHttpErrorStatus } from '../utils';
import { getDiscussionsConfig, getDiscussionsSettings } from './api';
import {
@@ -16,13 +18,18 @@ import {
export function fetchCourseConfig(courseId) {
return async (dispatch) => {
try {
+ let learnerSort = LearnersOrdering.BY_LAST_ACTIVITY;
dispatch(fetchConfigRequest());
+
const config = await getDiscussionsConfig(courseId);
if (config.is_user_admin || config.user_is_privileged) {
const settings = await getDiscussionsSettings(courseId);
Object.assign(config, { settings });
+ learnerSort = LearnersOrdering.BY_FLAG;
}
+
dispatch(fetchConfigSuccess(camelCaseObject(config)));
+ dispatch(setSortedBy(learnerSort));
} catch (error) {
if (getHttpErrorStatus(error) === 403) {
dispatch(fetchConfigDenied());
diff --git a/src/discussions/discussions-home/DiscussionContent.jsx b/src/discussions/discussions-home/DiscussionContent.jsx
index 66b43201..d3ffb490 100644
--- a/src/discussions/discussions-home/DiscussionContent.jsx
+++ b/src/discussions/discussions-home/DiscussionContent.jsx
@@ -6,8 +6,6 @@ import { Route, Switch } from 'react-router';
import { Routes } from '../../data/constants';
import { CommentsView } from '../comments';
import { useContainerSizeForParent } from '../data/hooks';
-import { LearnersContentView } from '../learners';
-import LearnerPageHeader from '../learners/LearnerPageHeader';
import { PostEditor } from '../posts';
export default function DiscussionContent() {
@@ -17,9 +15,6 @@ export default function DiscussionContent() {
return (
-
-
-
{postEditorVisible ? (
@@ -33,9 +28,6 @@ export default function DiscussionContent() {
-
-
-
)}
diff --git a/src/discussions/discussions-home/DiscussionSidebar.jsx b/src/discussions/discussions-home/DiscussionSidebar.jsx
index 40e4bcd0..9888c324 100644
--- a/src/discussions/discussions-home/DiscussionSidebar.jsx
+++ b/src/discussions/discussions-home/DiscussionSidebar.jsx
@@ -7,7 +7,7 @@ import {
} from 'react-router';
import { Routes } from '../../data/constants';
-import { LearnersView } from '../learners';
+import { LearnerPostsView, LearnersView } from '../learners';
import { PostsView } from '../posts';
import { TopicsView } from '../topics';
@@ -29,6 +29,7 @@ export default function DiscussionSidebar({ displaySidebar }) {
component={PostsView}
/>
+
}
/>
}
/>
+
)}
diff --git a/src/discussions/empty-posts/EmptyLearners.jsx b/src/discussions/empty-posts/EmptyLearners.jsx
new file mode 100644
index 00000000..dd217a0c
--- /dev/null
+++ b/src/discussions/empty-posts/EmptyLearners.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+import { useIsOnDesktop } from '../data/hooks';
+import messages from '../messages';
+import EmptyPage from './EmptyPage';
+
+function EmptyLearners({ intl }) {
+ const isOnDesktop = useIsOnDesktop();
+
+ if (!isOnDesktop) {
+ return null;
+ }
+
+ return (
+
+ );
+}
+
+EmptyLearners.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(EmptyLearners);
diff --git a/src/discussions/empty-posts/index.js b/src/discussions/empty-posts/index.js
index 3b9263a5..ff3ecac2 100644
--- a/src/discussions/empty-posts/index.js
+++ b/src/discussions/empty-posts/index.js
@@ -1,3 +1,4 @@
+export { default as EmptyLearners } from './EmptyLearners';
export { default as EmptyPage } from './EmptyPage';
export { default as EmptyPosts } from './EmptyPosts';
export { default as EmptyTopics } from './EmptyTopics';
diff --git a/src/discussions/learners/LearnerPageHeader.jsx b/src/discussions/learners/LearnerPageHeader.jsx
deleted file mode 100644
index 7582e8d8..00000000
--- a/src/discussions/learners/LearnerPageHeader.jsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React, { useContext } from 'react';
-
-import classNames from 'classnames';
-import { useSelector } from 'react-redux';
-import { generatePath, NavLink } from 'react-router-dom';
-
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Avatar, ButtonGroup, Icon } from '@edx/paragon';
-import { Report } from '@edx/paragon/icons';
-
-import { Routes } from '../../data/constants';
-import { DiscussionContext } from '../common/context';
-import { selectLearner, selectLearnerAvatar, selectLearnerProfile } from './data/selectors';
-import messages from './messages';
-
-function LearnerPageHeader({ intl }) {
- const { courseId, learnerUsername } = useContext(DiscussionContext);
- const params = { courseId, learnerUsername };
- const learner = useSelector(selectLearner(learnerUsername));
- const profile = useSelector(selectLearnerProfile(learnerUsername));
- const avatar = useSelector(selectLearnerAvatar(learnerUsername));
-
- const activeTabClass = (active) => classNames('btn', { 'btn-primary': active, 'btn-outline-primary': !active });
-
- return (
-
-
-
-
- {profile.username}
-
-
-
-
-
- {intl.formatMessage(messages.postsTab)} {learner.threads}
- {
- learner.activeFlags ? (
-
-
-
- ) : null
- }
-
-
- {intl.formatMessage(messages.responsesTab)} {learner.responses}
-
-
- {intl.formatMessage(messages.commentsTab)} {learner.replies}
-
-
-
-
- );
-}
-
-LearnerPageHeader.propTypes = {
- intl: intlShape.isRequired,
-};
-
-export default injectIntl(LearnerPageHeader);
diff --git a/src/discussions/learners/LearnerPostsView.jsx b/src/discussions/learners/LearnerPostsView.jsx
new file mode 100644
index 00000000..449b884b
--- /dev/null
+++ b/src/discussions/learners/LearnerPostsView.jsx
@@ -0,0 +1,107 @@
+import React, { useContext, useEffect } from 'react';
+
+import capitalize from 'lodash/capitalize';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory, useLocation } from 'react-router-dom';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Icon, IconButton, Spinner } from '@edx/paragon';
+import { ArrowBack } from '@edx/paragon/icons';
+
+import ScrollThreshold from '../../components/ScrollThreshold';
+import { RequestStatus, Routes } from '../../data/constants';
+import { DiscussionContext } from '../common/context';
+import {
+ selectAllThreads,
+ selectThreadNextPage,
+ threadsLoadingStatus,
+} from '../posts/data/selectors';
+import NoResults from '../posts/NoResults';
+import { PostLink } from '../posts/post';
+import { discussionsPath } from '../utils';
+import { selectLearnerProfile } from './data/selectors';
+import { fetchUserPosts } from './data/thunks';
+import messages from './messages';
+
+function LearnerPostsView({ intl }) {
+ const location = useLocation();
+ const history = useHistory();
+ const dispatch = useDispatch();
+
+ const posts = useSelector(selectAllThreads);
+ const loadingStatus = useSelector(threadsLoadingStatus());
+ const { courseId, learnerUsername: username } = useContext(DiscussionContext);
+ const nextPage = useSelector(selectThreadNextPage());
+ const { id: userId } = useSelector(selectLearnerProfile(username));
+
+ useEffect(() => {
+ dispatch(fetchUserPosts(courseId, username, userId));
+ }, [courseId, username]);
+
+ const loadMorePosts = () => (
+ dispatch(fetchUserPosts(courseId, username, userId, {
+ page: nextPage,
+ }))
+ );
+
+ const checkIsSelected = (id) => window.location.pathname.includes(id);
+
+ let lastPinnedIdx = null;
+ const postInstances = posts?.map((post, idx) => {
+ if (post.pinned && lastPinnedIdx !== false) {
+ lastPinnedIdx = idx;
+ } else if (lastPinnedIdx != null && lastPinnedIdx !== false) {
+ lastPinnedIdx = false;
+ // Add a spacing after the group of pinned posts
+ return (
+
+
+
+
+ );
+ }
+ return ();
+ });
+
+ return (
+
+
+
history.push(discussionsPath(Routes.LEARNERS.PATH, { courseId })(location))}
+ alt={intl.formatMessage(messages.back)}
+ />
+
+ {intl.formatMessage(messages.activityForLearner, { username: capitalize(username) })}
+
+
+
+
+
+ {postInstances}
+ {posts?.length === 0 &&
}
+ {loadingStatus === RequestStatus.IN_PROGRESS ? (
+
+
+
+ ) : (
+ nextPage && (
+
{
+ loadMorePosts();
+ }}
+ />
+ )
+ )}
+
+
+ );
+}
+
+LearnerPostsView.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(LearnerPostsView);
diff --git a/src/discussions/learners/LearnersContentView.jsx b/src/discussions/learners/LearnersContentView.jsx
deleted file mode 100644
index db650d77..00000000
--- a/src/discussions/learners/LearnersContentView.jsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React, { useContext } from 'react';
-
-import { useSelector } from 'react-redux';
-import {
- generatePath, Redirect, Route, Switch,
-} from 'react-router-dom';
-
-import { Spinner } from '@edx/paragon';
-
-import { LearnerTabs, RequestStatus, Routes } from '../../data/constants';
-import { DiscussionContext } from '../common/context';
-import { learnersLoadingStatus } from './data/selectors';
-import CommentsTabContent from './learner/CommentsTabContent';
-import PostsTabContent from './learner/PostsTabContent';
-
-function LearnersContentView() {
- const { courseId, learnerUsername } = useContext(DiscussionContext);
- const params = { courseId, learnerUsername };
- const apiStatus = useSelector(learnersLoadingStatus());
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- apiStatus === RequestStatus.IN_PROGRESS && (
-
-
-
- )
- }
-
- );
-}
-
-LearnersContentView.propTypes = {
-};
-
-export default LearnersContentView;
diff --git a/src/discussions/learners/LearnersContentView.test.jsx b/src/discussions/learners/LearnersContentView.test.jsx
deleted file mode 100644
index 432c7fb0..00000000
--- a/src/discussions/learners/LearnersContentView.test.jsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import React from 'react';
-
-import {
- fireEvent, render, screen, waitFor,
-} from '@testing-library/react';
-import MockAdapter from 'axios-mock-adapter';
-import { act } from 'react-dom/test-utils';
-import { IntlProvider } from 'react-intl';
-import { MemoryRouter, Route } from 'react-router';
-import { Factory } from 'rosie';
-
-import { initializeMockApp } from '@edx/frontend-platform';
-import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
-import { AppProvider } from '@edx/frontend-platform/react';
-
-import { LearnerTabs } from '../../data/constants';
-import { initializeStore } from '../../store';
-import { executeThunk } from '../../test-utils';
-import { commentsApiUrl } from '../comments/data/api';
-import { DiscussionContext } from '../common/context';
-import DiscussionContent from '../discussions-home/DiscussionContent';
-import { threadsApiUrl } from '../posts/data/api';
-import { coursesApiUrl, userProfileApiUrl } from './data/api';
-import { fetchLearners } from './data/thunks';
-
-import '../comments/data/__factories__';
-import '../posts/data/__factories__';
-import './data/__factories__';
-
-let store;
-let axiosMock;
-const courseId = 'course-v1:edX+TestX+Test_Course';
-const testUsername = 'leaner-1';
-
-function renderComponent(username = testUsername) {
- return render(
-
-
-
-
-
-
-
-
-
-
- ,
- );
-}
-
-describe('LearnersContentView', () => {
- const learnerCount = 1;
-
- beforeEach(async () => {
- initializeMockApp({
- authenticatedUser: {
- userId: 3,
- username: 'abc123',
- administrator: true,
- roles: [],
- },
- });
-
- axiosMock = new MockAdapter(getAuthenticatedHttpClient());
- store = initializeStore({});
- Factory.resetAll();
-
- axiosMock.onGet(`${coursesApiUrl}${courseId}/activity_stats/`)
- .reply(
- 200,
- Factory.build('learnersResult', {}, {
- count: learnerCount,
- pageSize: 5,
- }),
- );
-
- axiosMock.onGet(`${userProfileApiUrl}?username=${testUsername}`)
- .reply(
- 200,
- Factory.build('learnersProfile', {}, {
- username: [testUsername],
- }).profiles,
- );
- await executeThunk(fetchLearners(courseId), store.dispatch, store.getState);
-
- axiosMock.onGet(threadsApiUrl)
- .reply(200, Factory.build('threadsResult', {}, {
- topicId: undefined,
- count: 6,
- pageSize: 5,
- }));
-
- axiosMock.onGet(commentsApiUrl)
- .reply(200, Factory.build('commentsResult', {}, {
- count: 9,
- pageSize: 8,
- }));
- });
-
- test('it loads the posts view by default', async () => {
- await act(async () => {
- await renderComponent();
- });
- expect(screen.queryAllByTestId('post')).toHaveLength(5);
- expect(screen.queryAllByText('This is Thread', { exact: false })).toHaveLength(5);
- });
-
- test('it renders all the comments WITHOUT parent id in responses tab', async () => {
- await act(async () => {
- await renderComponent();
- });
- await act(async () => {
- fireEvent.click(screen.getByText('Responses', { exact: false }));
- });
-
- expect(screen.queryAllByText('comment number', { exact: false })).toHaveLength(8);
- });
-
- test('it renders all the comments with parent id in comments tab', async () => {
- axiosMock.onGet(commentsApiUrl)
- .reply(200, Factory.build('commentsResult', {}, {
- count: 4,
- parentId: 'test_parent_id',
- }));
- await act(async () => {
- await renderComponent();
- });
- await act(async () => {
- fireEvent.click(screen.getByRole('link', { name: /Comments \d+/i }));
- });
- expect(screen.queryAllByText('comment number', { exact: false })).toHaveLength(4);
- });
-
- test('it can switch back to the posts tab', async () => {
- await act(async () => {
- await renderComponent();
- });
- await act(async () => {
- fireEvent.click(screen.getByRole('link', { name: /Responses \d+/i }));
- });
- expect(screen.queryAllByText('comment number', { exact: false })).toHaveLength(8);
-
- await act(async () => {
- fireEvent.click(screen.getByRole('link', { name: /Posts \d+/i }));
- });
- await waitFor(() => expect(screen.queryAllByTestId('post')).toHaveLength(5));
- });
-
- describe('Posts Tab Button', () => {
- it('does not show Report Icon when the learner has NO active flags', async () => {
- await act(async () => {
- await renderComponent('leaner-2');
- });
- const button = screen.getByRole('link', { name: /Posts/i });
- expect(button.innerHTML).not.toContain('svg');
- });
-
- it('shows the Report Icon when the learner has active Flags', async () => {
- axiosMock.onGet(`${coursesApiUrl}${courseId}/activity_stats/`)
- .reply(() => [200, Factory.build('learnersResult', {}, {
- count: 1,
- pageSize: 5,
- activeFlags: 1,
- })]);
- axiosMock.onGet(`${userProfileApiUrl}?username=leaner-2`)
- .reply(() => [200, Factory.build('learnersProfile', {}, {
- username: ['leaner-2'],
- }).profiles]);
- await executeThunk(fetchLearners(courseId), store.dispatch, store.getState);
-
- await act(async () => {
- await renderComponent('leaner-2');
- });
- const button = screen.getByRole('link', { name: /Posts/i });
- expect(button.innerHTML).toContain('svg');
- });
- });
-});
diff --git a/src/discussions/learners/LearnersView.jsx b/src/discussions/learners/LearnersView.jsx
index 4a954c9b..f2fab0d0 100644
--- a/src/discussions/learners/LearnersView.jsx
+++ b/src/discussions/learners/LearnersView.jsx
@@ -17,12 +17,10 @@ import {
selectLearnerSorting,
} from './data/selectors';
import { fetchLearners } from './data/thunks';
-import { LearnerCard } from './learner';
+import { LearnerCard, LearnerFilterBar } from './learner';
function LearnersView() {
- const {
- courseId,
- } = useParams();
+ const { courseId } = useParams();
const location = useLocation();
const dispatch = useDispatch();
const orderBy = useSelector(selectLearnerSorting());
@@ -31,6 +29,7 @@ function LearnersView() {
const courseConfigLoadingStatus = useSelector(selectconfigLoadingStatus);
const learnersTabEnabled = useSelector(selectLearnersTabEnabled);
const learners = useSelector(selectAllLearners);
+
useEffect(() => {
if (learnersTabEnabled) {
dispatch(fetchLearners(courseId, { orderBy }));
@@ -45,9 +44,11 @@ function LearnersView() {
}));
}
};
+
return (
-
+
+
{courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && !learnersTabEnabled && (
}
*/
-export async function getLearners(
- courseId, { page, orderBy },
-) {
- const params = { page, orderBy };
+export async function getLearners(courseId, params) {
const url = `${coursesApiUrl}${courseId}/activity_stats/`;
const { data } = await getAuthenticatedHttpClient().get(url, { params });
return data;
@@ -36,3 +32,23 @@ export async function getUserProfiles(usernames) {
const { data } = await getAuthenticatedHttpClient().get(url);
return data;
}
+
+/**
+ * Get the posts by a specific user in a course's discussions
+ *
+ * @param {string} courseId Course ID of the course
+ * @param {string} username Username of the user
+ * @param {number} page
+ * @returns API Response object in the format
+ * {
+ * results: [array of posts],
+ * pagination: {count, num_pages, next, previous}
+ * }
+ */
+export async function getUserPosts(courseId, userId, { page }) {
+ const learnerPostsApiUrl = `${coursesApiUrl}${courseId}/learner/`;
+
+ const { data } = await getAuthenticatedHttpClient()
+ .get(learnerPostsApiUrl, { params: { user_id: userId, page } });
+ return data;
+}
diff --git a/src/discussions/learners/data/selectors.js b/src/discussions/learners/data/selectors.js
index 6f629c76..15926553 100644
--- a/src/discussions/learners/data/selectors.js
+++ b/src/discussions/learners/data/selectors.js
@@ -2,8 +2,6 @@
import { createSelector } from '@reduxjs/toolkit';
-import { LearnerTabs } from '../../../data/constants';
-
export const selectAllLearners = createSelector(
state => state.learners.pages,
pages => pages.flat(),
@@ -13,18 +11,8 @@ export const learnersLoadingStatus = () => state => state.learners.status;
export const selectLearnerSorting = () => state => state.learners.sortedBy;
-export const selectLearnerFilters = () => state => state.learners.filters;
-
export const selectLearnerNextPage = () => state => state.learners.nextPage;
-export const selectLearnerCommentsNextPage = (learner) => state => (
- state.learners.commentPaginationByUser?.[learner]?.nextPage
-);
-
-export const selectLearnerPostsNextPage = (learner) => state => (
- state.learners.postPaginationByUser?.[learner]?.nextPage
-);
-
export const selectLearnerAvatar = author => state => (
state.learners.learnerProfiles[author]?.profileImage?.imageUrlSmall
);
@@ -39,19 +27,3 @@ export const selectLearner = (username) => createSelector(
);
export const selectLearnerProfile = (username) => state => state.learners.learnerProfiles[username] || {};
-
-export const selectUserPosts = username => state => (state.learners.postsByUser[username] || []).flat();
-
-/**
- * Get the comments of a post.
- * @param {string} username Username of the learner to get the comments of
- * @param {LearnerTabs} commentType Type of comments to get
- * @returns {Array} Array of comments
- */
-export const selectUserComments = (username, commentType) => state => (
- commentType === LearnerTabs.COMMENTS
- ? (state.learners.commentsByUser[username] || []).flat().filter(c => c.parentId)
- : (state.learners.commentsByUser[username] || []).flat().filter(c => !c.parentId)
-);
-
-export const flaggedCommentCount = (username) => state => state.learners.flaggedCommentsByUser[username] || 0;
diff --git a/src/discussions/learners/data/slices.js b/src/discussions/learners/data/slices.js
index 6ab501f8..69e9dae8 100644
--- a/src/discussions/learners/data/slices.js
+++ b/src/discussions/learners/data/slices.js
@@ -10,25 +10,12 @@ const learnersSlice = createSlice({
name: 'learner',
initialState: {
status: RequestStatus.IN_PROGRESS,
- avatars: {},
learnerProfiles: {},
pages: [],
nextPage: null,
totalPages: null,
totalLearners: null,
sortedBy: LearnersOrdering.BY_LAST_ACTIVITY,
- commentPaginationByUser: {
-
- },
- commentsByUser: {
- // Map username to comments
- },
- postPaginationByUser: {
-
- },
- postsByUser: {
- // Map username to posts
- },
},
reducers: {
fetchLearnersSuccess: (state, { payload }) => {
@@ -54,37 +41,6 @@ const learnersSlice = createSlice({
setSortedBy: (state, { payload }) => {
state.sortedBy = payload;
},
- fetchUserCommentsRequest: (state) => {
- state.status = RequestStatus.IN_PROGRESS;
- },
- fetchUserCommentsSuccess: (state, { payload }) => {
- state.commentsByUser[payload.username] ??= [];
- state.commentsByUser[payload.username][payload.page - 1] = payload.comments;
- state.commentPaginationByUser[payload.username] = {
- nextPage: (payload.page < payload.pagination.numPages) ? payload.page + 1 : null,
- totalPages: payload.pagination.numPages,
- };
- state.status = RequestStatus.SUCCESSFUL;
- },
- fetchUserCommentsDenied: (state) => {
- state.status = RequestStatus.DENIED;
- },
- fetchUserPostsRequest: (state) => {
- state.status = RequestStatus.IN_PROGRESS;
- },
- fetchUserPostsSuccess: (state, { payload }) => {
- state.postsByUser[payload.username] ??= [];
- state.postsByUser[payload.username][payload.page - 1] = payload.posts;
- state.postPaginationByUser[payload.username] = {
- nextPage: (payload.page < payload.pagination.numPages) ? payload.page + 1 : null,
- totalPages: payload.pagination.numPages,
- };
- state.status = RequestStatus.SUCCESS;
- },
- fetchUserPostsDenied: (state) => {
- state.status = RequestStatus.DENIED;
- },
-
},
});
@@ -94,13 +50,6 @@ export const {
fetchLearnersSuccess,
fetchLearnersDenied,
setSortedBy,
- fetchUserCommentsRequest,
- fetchUserCommentsDenied,
- fetchUserCommentsSuccess,
- fetchUserPostsRequest,
- fetchUserPostsDenied,
- fetchUserPostsSuccess,
-
} = learnersSlice.actions;
export const learnersReducer = learnersSlice.reducer;
diff --git a/src/discussions/learners/data/thunks.js b/src/discussions/learners/data/thunks.js
index 26666a63..33718eca 100644
--- a/src/discussions/learners/data/thunks.js
+++ b/src/discussions/learners/data/thunks.js
@@ -1,24 +1,21 @@
/* eslint-disable import/prefer-default-export */
-import { camelCaseObject } from '@edx/frontend-platform';
+import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
-import { getUserComments } from '../../comments/data/api';
-import { getUserPosts } from '../../posts/data/api';
-import { getHttpErrorStatus } from '../../utils';
import {
- getLearners, getUserProfiles,
-} from './api';
+ fetchLearnerThreadsRequest,
+ fetchThreadsDenied,
+ fetchThreadsFailed,
+ fetchThreadsSuccess,
+} from '../../posts/data/slices';
+import { normaliseThreads } from '../../posts/data/thunks';
+import { getHttpErrorStatus } from '../../utils';
+import { getLearners, getUserPosts, getUserProfiles } from './api';
import {
fetchLearnersDenied,
fetchLearnersFailed,
fetchLearnersRequest,
fetchLearnersSuccess,
- fetchUserCommentsDenied,
- fetchUserCommentsRequest,
- fetchUserCommentsSuccess,
- fetchUserPostsDenied,
- fetchUserPostsRequest,
- fetchUserPostsSuccess,
} from './slices';
/**
@@ -33,13 +30,11 @@ export function fetchLearners(courseId, {
page = 1,
} = {}) {
return async (dispatch) => {
- const options = {
- orderBy,
- page,
- };
try {
+ const params = snakeCaseObject({ orderBy, page });
+
dispatch(fetchLearnersRequest({ courseId }));
- const learnerStats = await getLearners(courseId, options);
+ const learnerStats = await getLearners(courseId, params);
const learnerProfilesData = await getUserProfiles(learnerStats.results.map((l) => l.username));
const learnerProfiles = {};
learnerProfilesData.forEach(
@@ -59,58 +54,31 @@ export function fetchLearners(courseId, {
};
}
-/**
- * Fetch the comments of a user for the specified course and update the
- * redux state
- *
- * @param {string} courseId Course ID of the course eg., course-v1:X+Y+Z
- * @param {string} username Username of the learner
- * @param {number} page
- * @returns a promise that will update the state with the learner's comments
- */
-export function fetchUserComments(courseId, username, { page = 1 } = {}) {
- return async (dispatch) => {
- try {
- dispatch(fetchUserCommentsRequest());
- const data = await getUserComments(courseId, username, { page });
- dispatch(fetchUserCommentsSuccess(camelCaseObject({
- page,
- username,
- comments: data.results,
- pagination: data.pagination,
- })));
- } catch (error) {
- if (getHttpErrorStatus(error) === 403) {
- dispatch(fetchUserCommentsDenied());
- }
- }
- };
-}
-
/**
* Fetch the posts of a user for the specified course and update the
* redux state
*
* @param {string} courseId Course ID of the course eg., course-v1:X+Y+Z
- * @param {string} username Username of the learner
+ * @param {string} userId userId of the learner
* @param page
* @returns a promise that will update the state with the learner's posts
*/
-export function fetchUserPosts(courseId, username, { page = 1 } = {}) {
+export function fetchUserPosts(courseId, username, userId, { page = 1 } = {}) {
return async (dispatch) => {
try {
- dispatch(fetchUserPostsRequest());
- const data = await getUserPosts(courseId, username, { page });
- dispatch(fetchUserPostsSuccess(camelCaseObject({
- page,
- username,
- posts: data.results,
- pagination: data.pagination,
- })));
+ dispatch(fetchLearnerThreadsRequest({ courseId, author: username }));
+
+ const data = await getUserPosts(courseId, userId, { page });
+ const normalisedData = normaliseThreads(camelCaseObject(data));
+
+ dispatch(fetchThreadsSuccess({ ...normalisedData, page, author: username }));
} catch (error) {
if (getHttpErrorStatus(error) === 403) {
- dispatch(fetchUserPostsDenied());
+ dispatch(fetchThreadsDenied());
+ } else {
+ dispatch(fetchThreadsFailed());
}
+ logError(error);
}
};
}
diff --git a/src/discussions/learners/index.js b/src/discussions/learners/index.js
index 3dc1fdb5..e4cf49c4 100644
--- a/src/discussions/learners/index.js
+++ b/src/discussions/learners/index.js
@@ -1,3 +1,3 @@
/* eslint-disable import/prefer-default-export */
-export { default as LearnersContentView } from './LearnersContentView';
+export { default as LearnerPostsView } from './LearnerPostsView';
export { default as LearnersView } from './LearnersView';
diff --git a/src/discussions/learners/learner/CommentsTabContent.jsx b/src/discussions/learners/learner/CommentsTabContent.jsx
deleted file mode 100644
index 26cbd47b..00000000
--- a/src/discussions/learners/learner/CommentsTabContent.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { useContext, useEffect } from 'react';
-import PropType from 'prop-types';
-
-import { useSelector } from 'react-redux';
-
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Button } from '@edx/paragon';
-
-import { useDispatchWithState } from '../../../data/hooks';
-import Comment from '../../comments/comment/Comment';
-import messages from '../../comments/messages';
-import { DiscussionContext } from '../../common/context';
-import { selectLearnerCommentsNextPage, selectUserComments } from '../data/selectors';
-import { fetchUserComments } from '../data/thunks';
-
-function CommentsTabContent({ tab, intl }) {
- const [loading, dispatch] = useDispatchWithState();
- const { courseId, learnerUsername: username } = useContext(DiscussionContext);
- const comments = useSelector(selectUserComments(username, tab));
- const nextPage = useSelector(selectLearnerCommentsNextPage(username));
-
- useEffect(() => {
- dispatch(fetchUserComments(courseId, username));
- }, [courseId, username]);
-
- const handleLoadMoreComments = () => dispatch(fetchUserComments(courseId, username, { page: nextPage }));
- return (
-
- {comments.map(
- (comment) => ,
- )}
- {nextPage && !loading && (
-
- )}
-
- );
-}
-
-CommentsTabContent.propTypes = {
- intl: intlShape.isRequired,
- tab: PropType.string.isRequired,
-};
-
-export default injectIntl(CommentsTabContent);
diff --git a/src/discussions/learners/learner/LearnerAvatar.jsx b/src/discussions/learners/learner/LearnerAvatar.jsx
index d3f966f7..d63a4d8a 100644
--- a/src/discussions/learners/learner/LearnerAvatar.jsx
+++ b/src/discussions/learners/learner/LearnerAvatar.jsx
@@ -10,12 +10,15 @@ import { learnerShape } from './proptypes';
function LearnerAvatar({ learner }) {
const learnerAvatar = useSelector(selectLearnerAvatar(learner.username));
return (
-
+
);
diff --git a/src/discussions/learners/learner/LearnerCard.jsx b/src/discussions/learners/learner/LearnerCard.jsx
index 6d4710c3..56efe514 100644
--- a/src/discussions/learners/learner/LearnerCard.jsx
+++ b/src/discussions/learners/learner/LearnerCard.jsx
@@ -11,9 +11,9 @@ import { Routes } from '../../../data/constants';
import { DiscussionContext } from '../../common/context';
import { discussionsPath } from '../../utils';
import { selectLearnerLastLogin } from '../data/selectors';
+import messages from '../messages';
import LearnerAvatar from './LearnerAvatar';
import LearnerFooter from './LearnerFooter';
-import messages from './messages';
import { learnerShape } from './proptypes';
function LearnerCard({
@@ -21,43 +21,52 @@ function LearnerCard({
intl,
courseId,
}) {
- const {
- inContext,
- learnerUsername,
- } = useContext(DiscussionContext);
- const linkUrl = discussionsPath(Routes.LEARNERS.LEARNER, {
+ const { inContext, learnerUsername } = useContext(DiscussionContext);
+ const learnerLastLogin = useSelector(selectLearnerLastLogin(learner.username));
+ const lastActiveTime = timeago.format(new Date(learnerLastLogin), intl.locale);
+ const linkUrl = discussionsPath(Routes.LEARNERS.POSTS, {
0: inContext ? 'in-context' : undefined,
learnerUsername: learner.username,
courseId,
});
- const learnerLastLogin = useSelector(selectLearnerLastLogin(learner.username));
- const lastActiveTime = timeago.format(new Date(learnerLastLogin), intl.locale);
+
return (
-
-
-
-
-
- {learner.username}
-
+
+
+
+
+ {learner.username}
- {learnerLastLogin
- &&
{intl.formatMessage(messages.lastActive, { lastActiveTime })}}
+ {learnerLastLogin && (
+
+
+ {intl.formatMessage(messages.lastActive, { lastActiveTime })}
+
+
+ )}
+
-
diff --git a/src/discussions/learners/learner/LearnerFilterBar.jsx b/src/discussions/learners/learner/LearnerFilterBar.jsx
new file mode 100644
index 00000000..f0ca59f8
--- /dev/null
+++ b/src/discussions/learners/learner/LearnerFilterBar.jsx
@@ -0,0 +1,116 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import classNames from 'classnames';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Collapsible, Form, Icon } from '@edx/paragon';
+import { Check, Tune } from '@edx/paragon/icons';
+
+import { LearnersOrdering } from '../../../data/constants';
+import { selectUserIsPrivileged } from '../../data/selectors';
+import { setSortedBy } from '../data';
+import { selectLearnerSorting } from '../data/selectors';
+import messages from '../messages';
+
+const ActionItem = ({
+ id,
+ label,
+ value,
+ selected,
+}) => (
+
+);
+
+ActionItem.propTypes = {
+ id: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ selected: PropTypes.string.isRequired,
+};
+
+function LearnerFilterBar({
+ intl,
+}) {
+ const dispatch = useDispatch();
+ const userIsPrivileged = useSelector(selectUserIsPrivileged);
+ const currentSorting = useSelector(selectLearnerSorting());
+ const [isOpen, setOpen] = useState(false);
+
+ const handleSortFilterChange = (event) => {
+ const { name, value } = event.currentTarget;
+
+ if (name === 'sort') {
+ dispatch(setSortedBy(value));
+ }
+ setOpen(false);
+ };
+
+ return (
+
setOpen(!isOpen)}
+ className="collapsible-card-lg border-right-0"
+ >
+
+
+ {intl.formatMessage(messages.sortFilterStatus, {
+ sort: currentSorting,
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+LearnerFilterBar.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(LearnerFilterBar);
diff --git a/src/discussions/learners/learner/LearnerFooter.jsx b/src/discussions/learners/learner/LearnerFooter.jsx
index eaf5deb9..612f42ae 100644
--- a/src/discussions/learners/learner/LearnerFooter.jsx
+++ b/src/discussions/learners/learner/LearnerFooter.jsx
@@ -1,12 +1,11 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import {
- Icon, OverlayTrigger, Tooltip,
-} from '@edx/paragon';
-import { Edit, QuestionAnswer, Report } from '@edx/paragon/icons';
+import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
+import { Edit, Report } from '@edx/paragon/icons';
-import messages from './messages';
+import { QuestionAnswerOutline } from '../../../components/icons';
+import messages from '../messages';
import { learnerShape } from './proptypes';
function LearnerFooter({
@@ -16,42 +15,39 @@ function LearnerFooter({
const { inactiveFlags } = learner;
const { activeFlags } = learner;
return (
-
-
-
+
+
+
{learner.threads}
-
-
-
+
+
+
{learner.replies + learner.responses}
-
- {Boolean(activeFlags || inactiveFlags)
- && (
-
-
-
- {intl.formatMessage(messages.reported, { reported: activeFlags })}
-
- {Boolean(inactiveFlags)
- && (
-
- {intl.formatMessage(messages.previouslyReported, { previouslyReported: inactiveFlags })}
-
- )}
-
-
- )}
- >
-
-
-
- {activeFlags} {Boolean(inactiveFlags) && `/ ${inactiveFlags}`}
-
-
-
- )}
+
+ {Boolean(activeFlags || inactiveFlags) && (
+
+
+
+ {intl.formatMessage(messages.reported, { reported: activeFlags })}
+
+ {Boolean(inactiveFlags)
+ && (
+
+ {intl.formatMessage(messages.previouslyReported, { previouslyReported: inactiveFlags })}
+
+ )}
+
+
+ )}
+ >
+
+
+ {activeFlags} {Boolean(inactiveFlags) && `/ ${inactiveFlags}`}
+
+
+ )}
);
}
diff --git a/src/discussions/learners/learner/LearnerFooter.test.jsx b/src/discussions/learners/learner/LearnerFooter.test.jsx
index f991bfc9..1d1ca51c 100644
--- a/src/discussions/learners/learner/LearnerFooter.test.jsx
+++ b/src/discussions/learners/learner/LearnerFooter.test.jsx
@@ -7,8 +7,8 @@ import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../../store';
+import messages from '../messages';
import LearnerFooter from './LearnerFooter';
-import messages from './messages';
let store;
diff --git a/src/discussions/learners/learner/PostsTabContent.jsx b/src/discussions/learners/learner/PostsTabContent.jsx
deleted file mode 100644
index a378a2ea..00000000
--- a/src/discussions/learners/learner/PostsTabContent.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { useContext, useEffect } from 'react';
-
-import { useSelector } from 'react-redux';
-
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Button } from '@edx/paragon';
-
-import { useDispatchWithState } from '../../../data/hooks';
-import { DiscussionContext } from '../../common/context';
-import { Post } from '../../posts';
-import { selectLearnerPostsNextPage, selectUserPosts } from '../data/selectors';
-import { fetchUserPosts } from '../data/thunks';
-import messages from './messages';
-
-function PostsTabContent({ intl }) {
- const [loading, dispatch] = useDispatchWithState();
- const { courseId, learnerUsername: username } = useContext(DiscussionContext);
- const posts = useSelector(selectUserPosts(username));
- const nextPage = useSelector(selectLearnerPostsNextPage(username));
-
- useEffect(() => {
- dispatch(fetchUserPosts(courseId, username));
- }, [courseId, username]);
- // console.log({ posts });
- const handleLoadMorePosts = () => dispatch(fetchUserPosts(courseId, username, { page: nextPage }));
-
- return (
-
- {posts.map((post) => (
-
- ))}
- {nextPage && !loading && (
-
- )}
-
- );
-}
-
-PostsTabContent.propTypes = {
- intl: intlShape.isRequired,
-};
-
-export default injectIntl(PostsTabContent);
diff --git a/src/discussions/learners/learner/index.js b/src/discussions/learners/learner/index.js
index 20ce0187..89774d75 100644
--- a/src/discussions/learners/learner/index.js
+++ b/src/discussions/learners/learner/index.js
@@ -1,3 +1,4 @@
/* 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/messages.js b/src/discussions/learners/learner/messages.js
deleted file mode 100644
index 2636bd9b..00000000
--- a/src/discussions/learners/learner/messages.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { defineMessages } from '@edx/frontend-platform/i18n';
-
-const messages = defineMessages({
- reported: {
- id: 'discussions.learner.reported',
- defaultMessage: '{reported} reported',
- },
- previouslyReported: {
- id: 'discussions.learner.previouslyReported',
- defaultMessage: '{previouslyReported} previously reported',
- },
- lastActive: {
- id: 'discussions.learner.lastLogin',
- defaultMessage: 'Last active {lastActiveTime}',
- },
- loadMorePosts: {
- id: 'discussions.learner.loadMostPosts',
- defaultMessage: 'Load more posts',
- description: 'Text on button for loading more posts by a user',
- },
-});
-
-export default messages;
diff --git a/src/discussions/learners/messages.js b/src/discussions/learners/messages.js
index 3fa5a8c6..0f4fae3c 100644
--- a/src/discussions/learners/messages.js
+++ b/src/discussions/learners/messages.js
@@ -1,20 +1,50 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
- postsTab: {
- id: 'discussions.learner.tab.posts',
- defaultMessage: 'Posts',
- description: "Label for the learner's posts tab",
+ reported: {
+ id: 'discussions.learner.reported',
+ defaultMessage: '{reported} reported',
},
- responsesTab: {
- id: 'discussions.learner.tab.responses',
- defaultMessage: 'Responses',
- description: "Label for the learner's responses tab",
+ previouslyReported: {
+ id: 'discussions.learner.previouslyReported',
+ defaultMessage: '{previouslyReported} previously reported',
},
- commentsTab: {
- id: 'discussions.learner.tab.comments',
- defaultMessage: 'Comments',
- description: "Label for the learner's comments tab",
+ lastActive: {
+ id: 'discussions.learner.lastLogin',
+ defaultMessage: 'Last active {lastActiveTime}',
+ },
+ loadMorePosts: {
+ id: 'discussions.learner.loadMostPosts',
+ defaultMessage: 'Load more posts',
+ description: 'Text on button for loading more posts by a user',
+ },
+ back: {
+ id: 'discussions.learner.back',
+ defaultMessage: 'Back',
+ description: 'Text on button for back to learners list',
+ },
+ activityForLearner: {
+ id: 'discussions.learner.activityForLearner',
+ defaultMessage: 'Activity for {username}',
+ description: 'Text for learners post header',
+ },
+ mostActivity: {
+ id: 'discussions.learner.mostActivity',
+ defaultMessage: 'Most activity',
+ description: 'Text for learners sorting by most activity',
+ },
+ reportedActivity: {
+ id: 'discussions.learner.reportedActivity',
+ defaultMessage: 'Reported activity',
+ description: 'Text for learners sorting by reported activity',
+ },
+ sortFilterStatus: {
+ id: 'discussions.learner.sortFilterStatus',
+ defaultMessage: `All learners by {sort, select,
+ flagged {reported activity}
+ activity {most activity}
+ }`,
+ description: 'Text for current selected learners filter',
},
});
diff --git a/src/discussions/posts/PostsView.jsx b/src/discussions/posts/PostsView.jsx
index c8b984e6..9fbe7cf1 100644
--- a/src/discussions/posts/PostsView.jsx
+++ b/src/discussions/posts/PostsView.jsx
@@ -38,14 +38,17 @@ function PostsList({ posts, topics }) {
const showOwnPosts = page === 'my-posts';
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userIsStaff = useSelector(selectUserIsStaff);
- const loadThreads = (topicIds, pageNum = undefined) => dispatch(fetchThreads(courseId, {
- topicIds,
- orderBy,
- filters,
- page: pageNum,
- author: showOwnPosts ? authenticatedUser.username : null,
- countFlagged: userIsPrivileged || userIsStaff,
- }));
+
+ const loadThreads = (topicIds, pageNum = undefined) => (
+ dispatch(fetchThreads(courseId, {
+ topicIds,
+ orderBy,
+ filters,
+ page: pageNum,
+ author: showOwnPosts ? authenticatedUser.username : null,
+ countFlagged: userIsPrivileged || userIsStaff,
+ }))
+ );
useEffect(() => {
if (topics !== undefined) {
@@ -71,10 +74,11 @@ function PostsList({ posts, topics }) {
}
return ();
});
+
return (
<>
{postInstances}
- {posts && posts.length === 0 && }
+ {posts?.length === 0 && }
{loadingStatus === RequestStatus.IN_PROGRESS ? (
diff --git a/src/discussions/posts/data/api.js b/src/discussions/posts/data/api.js
index be4a5d5b..530edb1d 100644
--- a/src/discussions/posts/data/api.js
+++ b/src/discussions/posts/data/api.js
@@ -200,21 +200,3 @@ export async function uploadFile(blob, filename, courseId, threadKey) {
}
return data;
}
-
-/**
- * Get the posts by a specific user in a course's discussions
- *
- * @param {string} courseId Course ID of the course
- * @param {string} username Username of the user
- * @param {number} page
- * @returns API Response object in the format
- * {
- * results: [array of posts],
- * pagination: {count, num_pages, next, previous}
- * }
- */
-export async function getUserPosts(courseId, username, { page }) {
- const { data } = await getAuthenticatedHttpClient()
- .get(threadsApiUrl, { params: { course_id: courseId, author: username, page } });
- return data;
-}
diff --git a/src/discussions/posts/data/slices.js b/src/discussions/posts/data/slices.js
index a3248fb9..12a82ef4 100644
--- a/src/discussions/posts/data/slices.js
+++ b/src/discussions/posts/data/slices.js
@@ -39,6 +39,13 @@ const threadsSlice = createSlice({
sortedBy: ThreadOrdering.BY_LAST_ACTIVITY,
},
reducers: {
+ fetchLearnerThreadsRequest: (state, { payload }) => {
+ if (state.author !== payload.author) {
+ state.pages = [];
+ state.author = payload.author;
+ }
+ state.status = RequestStatus.IN_PROGRESS;
+ },
fetchThreadsRequest: (state) => {
state.status = RequestStatus.IN_PROGRESS;
},
@@ -177,6 +184,7 @@ export const {
deleteThreadFailed,
deleteThreadRequest,
deleteThreadSuccess,
+ fetchLearnerThreadsRequest,
fetchThreadDenied,
fetchThreadFailed,
fetchThreadRequest,
diff --git a/src/discussions/posts/data/thunks.js b/src/discussions/posts/data/thunks.js
index ba8412eb..b9883d26 100644
--- a/src/discussions/posts/data/thunks.js
+++ b/src/discussions/posts/data/thunks.js
@@ -46,7 +46,7 @@ import {
* @param data
* @returns {{pagination, threadsById: {}, threadsInTopic: {}, avatars: {}}}
*/
-function normaliseThreads(data) {
+export function normaliseThreads(data) {
const normalized = {};
let threads;
if ('results' in data) {
diff --git a/src/discussions/posts/post/PostLink.jsx b/src/discussions/posts/post/PostLink.jsx
index ec8440f8..64b3e5e4 100644
--- a/src/discussions/posts/post/PostLink.jsx
+++ b/src/discussions/posts/post/PostLink.jsx
@@ -27,6 +27,7 @@ function PostLink({
postId,
inContext,
category,
+ learnerUsername,
} = useContext(DiscussionContext);
const linkUrl = discussionsPath(Routes.COMMENTS.PAGES[page], {
0: inContext ? 'in-context' : undefined,
@@ -34,6 +35,7 @@ function PostLink({
topicId: post.topicId,
postId: post.id,
category,
+ learnerUsername,
});
const showAnsweredBadge = post.hasEndorsed && post.type === ThreadType.QUESTION;
const authorLabelColor = AvatarBorderAndLabelColors[post.authorLabel];
diff --git a/src/index.scss b/src/index.scss
index ad40eb5e..26b3f961 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -60,3 +60,7 @@ $fa-font-path: "~font-awesome/fonts";
.discussion-post:hover {
background-color: unset !important;
}
+
+.learner > a:hover {
+ background-color: #F2F0EF;
+}