From 4d51cf8855966ca6f640ef43192c776ec358e8cc Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Mon, 29 Sep 2025 18:29:43 -0400 Subject: [PATCH] revert: feat: added bulk delete user posts feature for privileged users (#818) This reverts commit 909d133acc4ef68aca2d2161e8fc1d0d3e2dc671. See https://github.com/openedx/edx-platform/issues/37402 for details We leave the original PR's changes to Confirmation.jsx in place because they are OK and are now tangled in with recent changes and more difficult to revert. --- src/data/constants.js | 2 - src/discussions/data/selectors.js | 2 - src/discussions/data/slices.js | 1 - .../learners/LearnerActionsDropdown.jsx | 107 ------------ .../learners/LearnerActionsDropdown.test.jsx | 153 ------------------ src/discussions/learners/LearnerPostsView.jsx | 91 ++--------- .../learners/LearnerPostsView.test.jsx | 128 +-------------- src/discussions/learners/data/api.js | 21 --- src/discussions/learners/data/api.test.jsx | 23 +-- src/discussions/learners/data/constants.js | 4 - src/discussions/learners/data/selectors.js | 2 - src/discussions/learners/data/slices.js | 26 --- src/discussions/learners/data/thunks.js | 26 +-- src/discussions/learners/messages.js | 39 ----- src/discussions/learners/test-utils.js | 28 +--- src/discussions/learners/utils.js | 42 ----- src/index.scss | 2 +- 17 files changed, 23 insertions(+), 674 deletions(-) delete mode 100644 src/discussions/learners/LearnerActionsDropdown.jsx delete mode 100644 src/discussions/learners/LearnerActionsDropdown.test.jsx delete mode 100644 src/discussions/learners/data/constants.js delete mode 100644 src/discussions/learners/utils.js diff --git a/src/data/constants.js b/src/data/constants.js index 269212d8..50b2078c 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -58,8 +58,6 @@ export const ContentActions = { CHANGE_TOPIC: 'topic_id', CHANGE_TYPE: 'type', VOTE: 'voted', - DELETE_COURSE_POSTS: 'delete-course-posts', - DELETE_ORG_POSTS: 'delete-org-posts', }; /** diff --git a/src/discussions/data/selectors.js b/src/discussions/data/selectors.js index d9f102a7..a9d8b86a 100644 --- a/src/discussions/data/selectors.js +++ b/src/discussions/data/selectors.js @@ -11,8 +11,6 @@ export const selectAnonymousPostingConfig = state => ({ export const selectUserHasModerationPrivileges = state => state.config.hasModerationPrivileges; -export const selectUserHasBulkDeletePrivileges = state => state.config.hasBulkDeletePrivileges; - export const selectUserIsStaff = state => state.config.isUserAdmin; export const selectUserIsGroupTa = state => state.config.isGroupTa; diff --git a/src/discussions/data/slices.js b/src/discussions/data/slices.js index b6bc2e7f..3ac1dd14 100644 --- a/src/discussions/data/slices.js +++ b/src/discussions/data/slices.js @@ -11,7 +11,6 @@ const configSlice = createSlice({ userRoles: [], groupAtSubsection: false, hasModerationPrivileges: false, - hasBulkDeletePrivileges: false, isGroupTa: false, isCourseAdmin: false, isCourseStaff: false, diff --git a/src/discussions/learners/LearnerActionsDropdown.jsx b/src/discussions/learners/LearnerActionsDropdown.jsx deleted file mode 100644 index 9571ceef..00000000 --- a/src/discussions/learners/LearnerActionsDropdown.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { - useCallback, useRef, useState, -} from 'react'; -import PropTypes from 'prop-types'; - -import { - Button, Dropdown, Icon, IconButton, ModalPopup, useToggle, -} from '@openedx/paragon'; -import { MoreHoriz } from '@openedx/paragon/icons'; - -import { useIntl } from '@edx/frontend-platform/i18n'; - -import { useLearnerActions } from './utils'; - -const LearnerActionsDropdown = ({ - actionHandlers, - dropDownIconSize, - userHasBulkDeletePrivileges, -}) => { - const buttonRef = useRef(); - const intl = useIntl(); - const [isOpen, open, close] = useToggle(false); - const [target, setTarget] = useState(null); - const actions = useLearnerActions(userHasBulkDeletePrivileges); - - const handleActions = useCallback((action) => { - const actionFunction = actionHandlers[action]; - if (actionFunction) { - actionFunction(); - } - }, [actionHandlers]); - - const onClickButton = useCallback((event) => { - event.preventDefault(); - setTarget(buttonRef.current); - open(); - }, [open]); - - const onCloseModal = useCallback(() => { - close(); - setTarget(null); - }, [close]); - - return ( - <> - -
- -
- {actions.map(action => ( - - { - close(); - handleActions(action.action); - }} - className="d-flex justify-content-start actions-dropdown-item" - data-testId={action.id} - > - - - {action.label.defaultMessage} - - - - ))} -
-
-
- - ); -}; - -LearnerActionsDropdown.propTypes = { - actionHandlers: PropTypes.objectOf(PropTypes.func).isRequired, - dropDownIconSize: PropTypes.bool, - userHasBulkDeletePrivileges: PropTypes.bool, -}; - -LearnerActionsDropdown.defaultProps = { - dropDownIconSize: false, - userHasBulkDeletePrivileges: false, -}; - -export default LearnerActionsDropdown; diff --git a/src/discussions/learners/LearnerActionsDropdown.test.jsx b/src/discussions/learners/LearnerActionsDropdown.test.jsx deleted file mode 100644 index 65b7ab0f..00000000 --- a/src/discussions/learners/LearnerActionsDropdown.test.jsx +++ /dev/null @@ -1,153 +0,0 @@ -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 { Factory } from 'rosie'; - -import { initializeMockApp } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { AppProvider } from '@edx/frontend-platform/react'; - -import { ContentActions } from '../../data/constants'; -import { initializeStore } from '../../store'; -import executeThunk from '../../test-utils'; -import { getCourseConfigApiUrl } from '../data/api'; -import fetchCourseConfig from '../data/thunks'; -import LearnerActionsDropdown from './LearnerActionsDropdown'; - -let store; -let axiosMock; -const courseId = 'course-v1:edX+TestX+Test_Course'; -const username = 'abc123'; - -const renderComponent = ({ - contentType = 'LEARNER', - userHasBulkDeletePrivileges = false, - actionHandlers = {}, -} = {}) => { - render( - - - - - , - ); -}; - -const findOpenActionsDropdownButton = async () => ( - screen.findByRole('button', { name: 'Actions menu' }) -); - -describe('LearnerActionsDropdown', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username, - administrator: false, - roles: [], - }, - }); - store = initializeStore(); - Factory.resetAll(); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - - axiosMock.onGet(`${getCourseConfigApiUrl()}${courseId}/`) - .reply(200, { isPostingEnabled: true }); - - await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); - }); - - it('can open dropdown if enabled', async () => { - renderComponent({ userHasBulkDeletePrivileges: true }); - - const openButton = await findOpenActionsDropdownButton(); - await act(async () => { - fireEvent.click(openButton); - }); - - await waitFor(() => expect(screen.queryByTestId('learner-actions-dropdown-modal-popup')).toBeInTheDocument()); - }); - - it('shows delete action for privileged users', async () => { - const mockHandler = jest.fn(); - renderComponent({ - userHasBulkDeletePrivileges: true, - actionHandlers: { deleteCoursePosts: mockHandler, deleteOrgPosts: mockHandler }, - }); - - const openButton = await findOpenActionsDropdownButton(); - await act(async () => { - fireEvent.click(openButton); - }); - - await waitFor(() => { - const deleteCourseItem = screen.queryByTestId('delete-course-posts'); - const deleteOrgItem = screen.queryByTestId('delete-org-posts'); - expect(deleteCourseItem).toBeInTheDocument(); - expect(deleteOrgItem).toBeInTheDocument(); - }); - }); - - it('triggers deleteCoursePosts handler when delete-course-posts is clicked', async () => { - const mockDeleteCourseHandler = jest.fn(); - const mockDeleteOrgHandler = jest.fn(); - renderComponent({ - userHasBulkDeletePrivileges: true, - actionHandlers: { - [ContentActions.DELETE_COURSE_POSTS]: mockDeleteCourseHandler, - [ContentActions.DELETE_ORG_POSTS]: mockDeleteOrgHandler, - }, - }); - - const openButton = await findOpenActionsDropdownButton(); - await act(async () => { - fireEvent.click(openButton); - }); - - await waitFor(() => expect(screen.queryByTestId('learner-actions-dropdown-modal-popup')).toBeInTheDocument()); - - const deleteCourseItem = await screen.findByTestId('delete-course-posts'); - await act(async () => { - fireEvent.click(deleteCourseItem); - }); - - await waitFor(() => expect(screen.queryByTestId('learner-actions-dropdown-modal-popup')).not.toBeInTheDocument()); - expect(mockDeleteCourseHandler).toHaveBeenCalled(); - expect(mockDeleteOrgHandler).not.toHaveBeenCalled(); - }); - - it('triggers deleteOrgPosts handler when delete-org-posts is clicked', async () => { - const mockDeleteCourseHandler = jest.fn(); - const mockDeleteOrgHandler = jest.fn(); - renderComponent({ - userHasBulkDeletePrivileges: true, - actionHandlers: { - [ContentActions.DELETE_COURSE_POSTS]: mockDeleteCourseHandler, - [ContentActions.DELETE_ORG_POSTS]: mockDeleteOrgHandler, - }, - }); - - const openButton = await findOpenActionsDropdownButton(); - await act(async () => { - fireEvent.click(openButton); - }); - - await waitFor(() => expect(screen.queryByTestId('learner-actions-dropdown-modal-popup')).toBeInTheDocument()); - - const deleteOrgItem = await screen.findByTestId('delete-org-posts'); - await act(async () => { - fireEvent.click(deleteOrgItem); - }); - - await waitFor(() => expect(screen.queryByTestId('learner-actions-dropdown-modal-popup')).not.toBeInTheDocument()); - expect(mockDeleteOrgHandler).toHaveBeenCalled(); - expect(mockDeleteCourseHandler).not.toHaveBeenCalled(); - }); -}); diff --git a/src/discussions/learners/LearnerPostsView.jsx b/src/discussions/learners/LearnerPostsView.jsx index 846b254b..9122b457 100644 --- a/src/discussions/learners/LearnerPostsView.jsx +++ b/src/discussions/learners/LearnerPostsView.jsx @@ -1,9 +1,9 @@ import React, { - useCallback, useContext, useEffect, useMemo, useState, + useCallback, useContext, useEffect, useMemo, } from 'react'; import { - Button, Icon, IconButton, Spinner, useToggle, + Button, Icon, IconButton, Spinner, } from '@openedx/paragon'; import { ArrowBack } from '@openedx/paragon/icons'; import capitalize from 'lodash/capitalize'; @@ -13,18 +13,11 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { - ContentActions, RequestStatus, Routes, } from '../../data/constants'; -import useDispatchWithState from '../../data/hooks'; -import { Confirmation } from '../common'; import DiscussionContext from '../common/context'; -import { - selectUserHasBulkDeletePrivileges, - selectUserHasModerationPrivileges, - selectUserIsStaff, -} from '../data/selectors'; +import { selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors'; import usePostList from '../posts/data/hooks'; import { selectAllThreadsIds, @@ -35,11 +28,8 @@ import { clearPostsPages } from '../posts/data/slices'; import NoResults from '../posts/NoResults'; import { PostLink } from '../posts/post'; import { discussionsPath } from '../utils'; -import { BulkDeleteType } from './data/constants'; -import { learnersLoadingStatus, selectBulkDeleteStats } from './data/selectors'; -import { deleteUserPosts, fetchUserPosts } from './data/thunks'; +import { fetchUserPosts } from './data/thunks'; import LearnerPostFilterBar from './learner-post-filter-bar/LearnerPostFilterBar'; -import LearnerActionsDropdown from './LearnerActionsDropdown'; import messages from './messages'; const LearnerPostsView = () => { @@ -48,20 +38,14 @@ const LearnerPostsView = () => { const navigate = useNavigate(); const dispatch = useDispatch(); - const [bulkDeleting, dispatchDelete] = useDispatchWithState(); const postsIds = useSelector(selectAllThreadsIds); const loadingStatus = useSelector(threadsLoadingStatus()); - const learnerLoadingStatus = useSelector(learnersLoadingStatus()); const postFilter = useSelector(state => state.learners.postFilter); const { courseId, learnerUsername: username } = useContext(DiscussionContext); const nextPage = useSelector(selectThreadNextPage()); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userIsStaff = useSelector(selectUserIsStaff); - const userHasBulkDeletePrivileges = useSelector(selectUserHasBulkDeletePrivileges); - const bulkDeleteStats = useSelector(selectBulkDeleteStats()); const sortedPostsIds = usePostList(postsIds); - const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false); - const [isDeletingCourseOrOrg, setIsDeletingCourseOrOrg] = useState(BulkDeleteType.COURSE); const loadMorePosts = useCallback((pageNum = undefined) => { const params = { @@ -75,23 +59,6 @@ const LearnerPostsView = () => { dispatch(fetchUserPosts(courseId, params)); }, [courseId, postFilter, username, userHasModerationPrivileges, userIsStaff]); - const handleShowDeleteConfirmation = useCallback(async (courseOrOrg) => { - setIsDeletingCourseOrOrg(courseOrOrg); - showDeleteConfirmation(); - await dispatch(deleteUserPosts(courseId, username, courseOrOrg, false)); - }, [courseId, username, showDeleteConfirmation]); - - const handleDeletePosts = useCallback(async (courseOrOrg) => { - await dispatchDelete(deleteUserPosts(courseId, username, courseOrOrg, true)); - navigate({ ...discussionsPath(Routes.LEARNERS.PATH, { courseId })(location) }); - hideDeleteConfirmation(); - }, [courseId, username, hideDeleteConfirmation]); - - const actionHandlers = useMemo(() => ({ - [ContentActions.DELETE_COURSE_POSTS]: () => handleShowDeleteConfirmation(BulkDeleteType.COURSE), - [ContentActions.DELETE_ORG_POSTS]: () => handleShowDeleteConfirmation(BulkDeleteType.ORG), - }), [handleShowDeleteConfirmation]); - const postInstances = useMemo(() => ( sortedPostsIds?.map((postId, idx) => ( { return (
-
-
- navigate({ ...discussionsPath(Routes.LEARNERS.PATH, { courseId })(location) })} - alt={intl.formatMessage(messages.back)} - /> -
-
+
+ navigate({ ...discussionsPath(Routes.LEARNERS.PATH, { courseId })(location) })} + alt={intl.formatMessage(messages.back)} + /> +
{intl.formatMessage(messages.activityForLearner, { username: capitalize(username) })}
- {userHasBulkDeletePrivileges ? ( -
- -
- ) - : (
)} +
@@ -154,22 +109,6 @@ const LearnerPostsView = () => { ) )}
- handleDeletePosts(isDeletingCourseOrOrg)} - confirmButtonText={intl.formatMessage(messages.deletePostsConfirm)} - confirmButtonVariant="danger" - isDataLoading={!(learnerLoadingStatus === RequestStatus.SUCCESSFUL)} - isConfirmButtonPending={bulkDeleting} - pendingConfirmButtonText={intl.formatMessage(messages.deletePostConfirmPending)} - />
); }; diff --git a/src/discussions/learners/LearnerPostsView.test.jsx b/src/discussions/learners/LearnerPostsView.test.jsx index dcca0656..5977d0f5 100644 --- a/src/discussions/learners/LearnerPostsView.test.jsx +++ b/src/discussions/learners/LearnerPostsView.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { - fireEvent, render, screen, waitFor, within, + fireEvent, render, screen, waitFor, } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import { act } from 'react-dom/test-utils'; @@ -20,7 +20,7 @@ import executeThunk from '../../test-utils'; import { getCohortsApiUrl } from '../cohorts/data/api'; import fetchCourseCohorts from '../cohorts/data/thunks'; import DiscussionContext from '../common/context'; -import { deletePostsApiUrl, learnerPostsApiUrl } from './data/api'; +import { learnerPostsApiUrl } from './data/api'; import { fetchUserPosts } from './data/thunks'; import LearnerPostsView from './LearnerPostsView'; import { setUpPrivilages } from './test-utils'; @@ -220,128 +220,4 @@ describe('Learner Posts View', () => { expect(loadMoreButton).not.toBeInTheDocument(); expect(container.querySelectorAll('.discussion-post')).toHaveLength(4); }); - - test('should display dropdown menu button for bulk delete user posts for privileged users', async () => { - await setUpPrivilages(axiosMock, store, true, true); - await renderComponent(); - expect(within(container).queryByRole('button', { name: /actions menu/i })).toBeInTheDocument(); - }); - - test('should NOT display dropdown menu button for bulk delete user posts for other users', async () => { - await setUpPrivilages(axiosMock, store, true, false); - await renderComponent(); - expect(within(container).queryByRole('button', { name: /actions menu/i })).not.toBeInTheDocument(); - }); - - test('should display confirmation dialog when delete course posts is clicked', async () => { - await setUpPrivilages(axiosMock, store, true, true); - axiosMock.onPost(deletePostsApiUrl(courseId, username, 'course', false)) - .reply(202, { thread_count: 2, comment_count: 3 }); - await renderComponent(); - - const actionsButton = await screen.findByRole('button', { name: /actions menu/i }); - await act(async () => { - fireEvent.click(actionsButton); - }); - - const deleteCourseItem = await screen.findByTestId('delete-course-posts'); - await act(async () => { - fireEvent.click(deleteCourseItem); - }); - - await waitFor(() => { - const dialog = screen.getByText('Are you sure you want to delete this user\'s discussion contributions?'); - expect(dialog).toBeInTheDocument(); - expect(screen.getByText('You are about to delete 5 discussion contributions by this user in this course. This includes all discussion threads, responses, and comments authored by them.')).toBeInTheDocument(); - expect(screen.getByText('This action cannot be undone.')).toBeInTheDocument(); - expect(screen.getByText('Cancel')).toBeInTheDocument(); - expect(screen.getByText('Delete')).toBeInTheDocument(); - }); - }); - - test('should complete delete course posts flow and redirect', async () => { - await setUpPrivilages(axiosMock, store, true, true); - axiosMock.onPost(deletePostsApiUrl(courseId, username, 'course', false)) - .reply(202, { thread_count: 2, comment_count: 3 }); - axiosMock.onPost(deletePostsApiUrl(courseId, username, 'course', true)) - .reply(202, { thread_count: 0, comment_count: 0 }); - await renderComponent(); - - const actionsButton = await screen.findByRole('button', { name: /actions menu/i }); - await act(async () => { - fireEvent.click(actionsButton); - }); - - const deleteCourseItem = await screen.findByTestId('delete-course-posts'); - await act(async () => { - fireEvent.click(deleteCourseItem); - }); - - await waitFor(() => { - expect(screen.getByText('Are you sure you want to delete this user\'s discussion contributions?')).toBeInTheDocument(); - }); - - const confirmButton = await screen.findByText('Delete'); - await act(async () => { - fireEvent.click(confirmButton); - }); - - await waitFor(() => { - expect(lastLocation.pathname.endsWith('/learners')).toBeTruthy(); - expect(screen.queryByText('Are you sure you want to delete this user\'s discussion contributions?')).not.toBeInTheDocument(); - }); - }); - - test('should close confirmation dialog when cancel is clicked', async () => { - await setUpPrivilages(axiosMock, store, true, true); - axiosMock.onPost(deletePostsApiUrl(courseId, username, 'course', false)) - .reply(202, { thread_count: 2, comment_count: 3 }); - await renderComponent(); - - const actionsButton = await screen.findByRole('button', { name: /actions menu/i }); - await act(async () => { - fireEvent.click(actionsButton); - }); - - const deleteCourseItem = await screen.findByTestId('delete-course-posts'); - await act(async () => { - fireEvent.click(deleteCourseItem); - }); - - await waitFor(() => { - expect(screen.getByText('Are you sure you want to delete this user\'s discussion contributions?')).toBeInTheDocument(); - }); - - const cancelButton = await screen.findByText('Cancel'); - await act(async () => { - fireEvent.click(cancelButton); - }); - - await waitFor(() => { - expect(screen.queryByText('Are you sure you want to delete this user\'s discussion contributions?')).not.toBeInTheDocument(); - }); - }); - - test('should display confirmation dialog for org posts deletion', async () => { - await setUpPrivilages(axiosMock, store, true, true); - axiosMock.onPost(deletePostsApiUrl(courseId, username, 'org', false)) - .reply(202, { thread_count: 5, comment_count: 10 }); - await renderComponent(); - - const actionsButton = await screen.findByRole('button', { name: /actions menu/i }); - await act(async () => { - fireEvent.click(actionsButton); - }); - - const deleteOrgItem = await screen.findByTestId('delete-org-posts'); - await act(async () => { - fireEvent.click(deleteOrgItem); - }); - - await waitFor(() => { - expect(screen.getByText('Are you sure you want to delete this user\'s discussion contributions?')).toBeInTheDocument(); - expect(screen.getByText('You are about to delete 15 discussion contributions by this user across the organization. This includes all discussion threads, responses, and comments authored by them.')).toBeInTheDocument(); - expect(screen.getByText('This action cannot be undone.')).toBeInTheDocument(); - }); - }); }); diff --git a/src/discussions/learners/data/api.js b/src/discussions/learners/data/api.js index 05121079..53a4d9c8 100644 --- a/src/discussions/learners/data/api.js +++ b/src/discussions/learners/data/api.js @@ -11,7 +11,6 @@ export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussio export const getUserProfileApiUrl = () => `${getConfig().LMS_BASE_URL}/api/user/v1/accounts`; export const learnerPostsApiUrl = (courseId) => `${getCoursesApiUrl()}${courseId}/learner/`; export const learnersApiUrl = (courseId) => `${getCoursesApiUrl()}${courseId}/activity_stats/`; -export const deletePostsApiUrl = (courseId, username, courseOrOrg, execute) => `${getConfig().LMS_BASE_URL}/api/discussion/v1/bulk_delete_user_posts/${courseId}?username=${username}&course_or_org=${courseOrOrg}&execute=${execute}`; /** * Fetches all the learners in the given course. @@ -83,23 +82,3 @@ export async function getUserPosts(courseId, { .get(learnerPostsApiUrl(courseId), { params }); return data; } - -/** - * Deletes posts by a specific user in a course or organization - * @param {string} courseId Course ID of the course - * @param {string} username Username of the user whose posts are to be deleted - * @param {string} courseOrOrg Can be 'course' or 'org' to specify deletion scope - * @param {boolean} execute If true, deletes posts; if false, returns count of threads and comments - * @returns API Response object in the format - * { - * thread_count: number, - * comment_count: number - * } - */ -export async function deleteUserPostsApi(courseId, username, courseOrOrg, execute) { - const { data } = await getAuthenticatedHttpClient().post( - deletePostsApiUrl(courseId, username, courseOrOrg, execute), - null, - ); - return data; -} diff --git a/src/discussions/learners/data/api.test.jsx b/src/discussions/learners/data/api.test.jsx index 444debaf..e03e883f 100644 --- a/src/discussions/learners/data/api.test.jsx +++ b/src/discussions/learners/data/api.test.jsx @@ -4,7 +4,7 @@ import { Factory } from 'rosie'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; -import { setupDeleteUserPostsMockResponse, setupLearnerMockResponse, setupPostsMockResponse } from '../test-utils'; +import { setupLearnerMockResponse, setupPostsMockResponse } from '../test-utils'; import './__factories__'; @@ -80,25 +80,4 @@ describe('Learner api test cases', () => { expect(threads.status).toEqual('denied'); }); - - it.each([ - { courseOrOrg: 'course', execute: false, response: { comment_count: 3, thread_count: 2 } }, - { courseOrOrg: 'course', execute: true, response: { comment_count: 0, thread_count: 0 } }, - { courseOrOrg: 'org', execute: false, response: { comment_count: 3, thread_count: 2 } }, - { courseOrOrg: 'org', execute: true, response: { comment_count: 0, thread_count: 0 } }, - ])( - 'Successfully fetches user post stats and bulk deletes user posts based on execute', - async ({ courseOrOrg, execute, response }) => { - const learners = await setupDeleteUserPostsMockResponse({ courseOrOrg, execute, response }); - - expect(learners.status).toEqual('successful'); - expect(Object.values(learners.bulkDeleteStats)).toEqual(Object.values(response)); - }, - ); - - it('Failed to bulk delete user posts', async () => { - const learners = await setupPostsMockResponse({ statusCode: 400 }); - - expect(learners.status).toEqual('failed'); - }); }); diff --git a/src/discussions/learners/data/constants.js b/src/discussions/learners/data/constants.js deleted file mode 100644 index c957e256..00000000 --- a/src/discussions/learners/data/constants.js +++ /dev/null @@ -1,4 +0,0 @@ -export const BulkDeleteType = { - COURSE: 'course', - ORG: 'org', -}; diff --git a/src/discussions/learners/data/selectors.js b/src/discussions/learners/data/selectors.js index 10763445..bf3ea98e 100644 --- a/src/discussions/learners/data/selectors.js +++ b/src/discussions/learners/data/selectors.js @@ -16,5 +16,3 @@ export const selectLearnerNextPage = () => state => state.learners.nextPage; export const selectLearnerAvatar = author => state => ( state.learners.learnerProfiles[author]?.profileImage?.imageUrlLarge ); - -export const selectBulkDeleteStats = () => state => state.learners.bulkDeleteStats; diff --git a/src/discussions/learners/data/slices.js b/src/discussions/learners/data/slices.js index 534fe850..cc94aaee 100644 --- a/src/discussions/learners/data/slices.js +++ b/src/discussions/learners/data/slices.js @@ -25,10 +25,6 @@ const learnersSlice = createSlice({ cohort: '', }, usernameSearch: null, - bulkDeleteStats: { - commentCount: 0, - threadCount: 0, - }, }, reducers: { fetchLearnersSuccess: (state, { payload }) => ( @@ -88,25 +84,6 @@ const learnersSlice = createSlice({ postFilter: payload, } ), - deleteUserPostsRequest: (state) => ( - { - ...state, - status: RequestStatus.IN_PROGRESS, - } - ), - deleteUserPostsSuccess: (state, { payload }) => ( - { - ...state, - status: RequestStatus.SUCCESSFUL, - bulkDeleteStats: payload, - } - ), - deleteUserPostsFailed: (state) => ( - { - ...state, - status: RequestStatus.FAILED, - } - ), }, }); @@ -118,9 +95,6 @@ export const { setSortedBy, setUsernameSearch, setPostFilter, - deleteUserPostsRequest, - deleteUserPostsSuccess, - deleteUserPostsFailed, } = learnersSlice.actions; export const learnersReducer = learnersSlice.reducer; diff --git a/src/discussions/learners/data/thunks.js b/src/discussions/learners/data/thunks.js index afc554b6..93603c9c 100644 --- a/src/discussions/learners/data/thunks.js +++ b/src/discussions/learners/data/thunks.js @@ -12,16 +12,8 @@ import { } from '../../posts/data/slices'; import { normaliseThreads } from '../../posts/data/thunks'; import { getHttpErrorStatus } from '../../utils'; +import { getLearners, getUserPosts, getUserProfiles } from './api'; import { - deleteUserPostsApi, - getLearners, - getUserPosts, - getUserProfiles, -} from './api'; -import { - deleteUserPostsFailed, - deleteUserPostsRequest, - deleteUserPostsSuccess, fetchLearnersDenied, fetchLearnersFailed, fetchLearnersRequest, @@ -129,19 +121,3 @@ export function fetchUserPosts(courseId, { } }; } - -export const deleteUserPosts = ( - courseId, - username, - courseOrOrg, - execute, -) => async (dispatch) => { - try { - dispatch(deleteUserPostsRequest({ courseId, username })); - const response = await deleteUserPostsApi(courseId, username, courseOrOrg, execute); - dispatch(deleteUserPostsSuccess(camelCaseObject(response))); - } catch (error) { - dispatch(deleteUserPostsFailed()); - logError(error); - } -}; diff --git a/src/discussions/learners/messages.js b/src/discussions/learners/messages.js index 38403e56..816730af 100644 --- a/src/discussions/learners/messages.js +++ b/src/discussions/learners/messages.js @@ -62,45 +62,6 @@ const messages = defineMessages({ defaultMessage: 'Posts', description: 'Tooltip text for all posts icon', }, - deleteCoursePosts: { - id: 'discussions.learner.actions.deleteCoursePosts', - defaultMessage: 'Delete user posts within this course', - description: 'Action to delete user posts within a specific course', - }, - deleteOrgPosts: { - id: 'discussions.learner.actions.deleteOrgPosts', - defaultMessage: 'Delete user posts within this organization', - description: 'Action to delete user posts within the organization', - }, - deletePostsTitle: { - id: 'discussions.learner.deletePosts.title', - defaultMessage: 'Are you sure you want to delete this user\'s discussion contributions?', - description: 'Title for delete course posts confirmation dialog', - }, - deletePostsDescription: { - id: 'discussions.learner.deletePosts.description', - defaultMessage: `{bulkType, select, - course {You are about to delete {count, plural, one {# discussion contribution} other {# discussion contributions}} by this user in this course. This includes all discussion threads, responses, and comments authored by them.} - org {You are about to delete {count, plural, one {# discussion contribution} other {# discussion contributions}} by this user across the organization. This includes all discussion threads, responses, and comments authored by them.} - other {You are about to delete {count, plural, one {# discussion contribution} other {# discussion contributions}} by this user. This includes all discussion threads, responses, and comments authored by them.} - }`, - description: 'Description for delete posts confirmation dialog', - }, - deletePostsConfirm: { - id: 'discussions.learner.deletePosts.confirm', - defaultMessage: 'Delete', - description: 'Confirm button text for delete posts', - }, - deletePostConfirmPending: { - id: 'discussions.learner.deletePosts.confirm.pending', - defaultMessage: 'Deleting', - description: 'Pending state of confirm button text for delete posts', - }, - deletePostsBoldDescription: { - id: 'discussions.learner.deletePosts.boldDescription', - defaultMessage: 'This action cannot be undone.', - description: 'Bold disclaimer description for delete confirmation dialog', - }, }); export default messages; diff --git a/src/discussions/learners/test-utils.js b/src/discussions/learners/test-utils.js index 253c4585..a14ba17e 100644 --- a/src/discussions/learners/test-utils.js +++ b/src/discussions/learners/test-utils.js @@ -7,13 +7,8 @@ import { initializeStore } from '../../store'; import executeThunk from '../../test-utils'; import { getDiscussionsConfigUrl } from '../data/api'; import fetchCourseConfig from '../data/thunks'; -import { - deletePostsApiUrl, - getUserProfileApiUrl, - learnerPostsApiUrl, - learnersApiUrl, -} from './data/api'; -import { deleteUserPosts, fetchLearners, fetchUserPosts } from './data/thunks'; +import { getUserProfileApiUrl, learnerPostsApiUrl, learnersApiUrl } from './data/api'; +import { fetchLearners, fetchUserPosts } from './data/thunks'; const courseId = 'course-v1:edX+DemoX+Demo_Course'; @@ -59,26 +54,9 @@ export async function setupPostsMockResponse({ return store.getState().threads; } -export async function setupDeleteUserPostsMockResponse({ - username = 'abc123', - courseOrOrg, - statusCode = 202, - execute, - response, -} = {}) { - const store = initializeStore(); - const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - - axiosMock.onPost(deletePostsApiUrl(courseId, username, courseOrOrg, execute)).reply(statusCode, response); - - await executeThunk(deleteUserPosts(courseId, username, courseOrOrg, execute), store.dispatch, store.getState); - return store.getState().learners; -} - -export async function setUpPrivilages(axiosMock, store, hasModerationPrivileges, hasBulkDeletePrivileges) { +export async function setUpPrivilages(axiosMock, store, hasModerationPrivileges) { axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { hasModerationPrivileges, - hasBulkDeletePrivileges, }); await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState); diff --git a/src/discussions/learners/utils.js b/src/discussions/learners/utils.js deleted file mode 100644 index 7f4cf4e7..00000000 --- a/src/discussions/learners/utils.js +++ /dev/null @@ -1,42 +0,0 @@ -import { useMemo } from 'react'; - -import { Delete } from '@openedx/paragon/icons'; - -import { useIntl } from '@edx/frontend-platform/i18n'; - -import { ContentActions } from '../../data/constants'; -import messages from './messages'; - -export const LEARNER_ACTIONS_LIST = [ - { - id: 'delete-course-posts', - action: ContentActions.DELETE_COURSE_POSTS, - icon: Delete, - label: messages.deleteCoursePosts, - }, - { - id: 'delete-org-posts', - action: ContentActions.DELETE_ORG_POSTS, - icon: Delete, - label: messages.deleteOrgPosts, - }, -]; - -export function useLearnerActions(userHasBulkDeletePrivileges = false) { - const intl = useIntl(); - - const actions = useMemo(() => { - if (!userHasBulkDeletePrivileges) { - return []; - } - return LEARNER_ACTIONS_LIST.map(action => ({ - ...action, - label: { - id: action.label.id, - defaultMessage: intl.formatMessage(action.label), - }, - })); - }, [userHasBulkDeletePrivileges, intl]); - - return actions; -} diff --git a/src/index.scss b/src/index.scss index 3b0bd2f0..2981b535 100755 --- a/src/index.scss +++ b/src/index.scss @@ -553,7 +553,7 @@ code { .actions-dropdown-item { padding: 12px 16px; height: 48px !important; - min-width: 195px !important + width: 195px !important } .font-xl {