From 3f0a8c109f1a092d6dc9bd0331d42323a653b245 Mon Sep 17 00:00:00 2001 From: Chintan Joshi Date: Wed, 7 Jan 2026 20:51:21 +0530 Subject: [PATCH] Revert "feat: added soft delete functionality (#6)" This reverts commit 9dc3367eec5c6da31761ea7766fb4b1a35509181. --- src/assets/undelete.svg | 3 - src/components/FilterBar.jsx | 45 +----- src/data/constants.js | 6 - src/discussions/common/ActionsDropdown.jsx | 5 +- src/discussions/common/HoverCard.jsx | 16 +-- src/discussions/data/constants.js | 5 - src/discussions/data/selectors.js | 2 +- .../learners/LearnerActionsDropdown.jsx | 114 ++++++---------- .../learners/LearnerActionsDropdown.test.jsx | 23 +--- src/discussions/learners/LearnerPostsView.jsx | 67 ++------- .../learners/LearnerPostsView.test.jsx | 24 ---- src/discussions/learners/data/api.js | 57 -------- src/discussions/learners/data/redux.test.jsx | 7 +- src/discussions/learners/data/slices.js | 31 +---- src/discussions/learners/data/thunks.js | 128 ++++++------------ .../LearnerPostFilterBar.jsx | 45 +++--- .../LearnerPostFilterBar.test.jsx | 17 +-- .../learners/learner/LearnerCard.jsx | 5 - .../learners/learner/LearnerFilterBar.jsx | 11 +- .../learners/learner/LearnerFooter.jsx | 34 +---- src/discussions/learners/learner/proptypes.js | 4 - src/discussions/learners/messages.js | 60 -------- src/discussions/learners/utils.js | 59 -------- src/discussions/messages.js | 50 ------- .../post-comments/PostCommentsView.test.jsx | 1 - .../comments/comment/Comment.jsx | 85 ++---------- .../comments/comment/CommentHeader.jsx | 2 +- .../post-comments/comments/comment/Reply.jsx | 66 +-------- .../data/__factories__/comments.factory.js | 1 - src/discussions/post-comments/data/api.js | 20 --- src/discussions/post-comments/data/hooks.js | 17 +-- src/discussions/post-comments/data/thunks.js | 26 +--- src/discussions/post-comments/messages.js | 34 ----- src/discussions/posts/NoResults.jsx | 2 +- src/discussions/posts/PostsView.test.jsx | 4 +- .../data/__factories__/threads.factory.js | 1 - src/discussions/posts/data/api.js | 18 --- src/discussions/posts/data/selectors.js | 2 - src/discussions/posts/data/slices.js | 19 +-- src/discussions/posts/data/thunks.js | 27 +--- src/discussions/posts/index.js | 1 - .../posts/post-filter-bar/messages.js | 12 -- src/discussions/posts/post/Post.jsx | 62 +-------- src/discussions/posts/post/PostLink.jsx | 66 +-------- src/discussions/posts/post/messages.js | 32 ----- src/discussions/utils.js | 26 +--- src/index.scss | 85 +----------- 47 files changed, 181 insertions(+), 1246 deletions(-) delete mode 100644 src/assets/undelete.svg diff --git a/src/assets/undelete.svg b/src/assets/undelete.svg deleted file mode 100644 index fa787312e..000000000 --- a/src/assets/undelete.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/components/FilterBar.jsx b/src/components/FilterBar.jsx index b7c50f6ee..da5b9427c 100644 --- a/src/components/FilterBar.jsx +++ b/src/components/FilterBar.jsx @@ -75,16 +75,6 @@ const FilterBar = ({ label: intl.formatMessage(messages.filterUnresponded), value: PostsStatusFilter.UNRESPONDED, }, - { - id: 'status-active', - label: intl.formatMessage(messages.filterActive), - value: PostsStatusFilter.ACTIVE, - }, - { - id: 'status-deleted', - label: intl.formatMessage(messages.filterDeleted), - value: PostsStatusFilter.DELETED, - }, { id: 'sort-activity', label: intl.formatMessage(messages.lastActivityAt), @@ -134,7 +124,7 @@ const FilterBar = ({
- {filters.filter(f => !f.hasSeparator).map((value) => ( + {filters.map((value) => ( ))}
- {filters.some(f => f.hasSeparator) && ( - <> -
-
- {filters.filter(f => f.hasSeparator).map((value) => ( - - {value.filters.map(filterName => { - const element = allFilters.find(obj => obj.id === filterName); - if (element) { - return ( - - ); - } - return false; - })} - - ))} -
- - )} {showCohortsFilter && ( <>
@@ -241,7 +199,6 @@ FilterBar.propTypes = { selectedFilters: PropTypes.shape({ postType: ThreadType, status: PostsStatusFilter, - contentStatus: PostsStatusFilter, orderBy: ThreadOrdering, cohort: PropTypes.string, }).isRequired, diff --git a/src/data/constants.js b/src/data/constants.js index 4919694c3..269212d89 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -51,7 +51,6 @@ export const ContentActions = { COPY_LINK: 'copy_link', REPORT: 'abuse_flagged', DELETE: 'delete', - RESTORE: 'restore', FOLLOWING: 'following', CHANGE_GROUP: 'group_id', MARK_READ: 'read', @@ -61,8 +60,6 @@ export const ContentActions = { VOTE: 'voted', DELETE_COURSE_POSTS: 'delete-course-posts', DELETE_ORG_POSTS: 'delete-org-posts', - RESTORE_COURSE_POSTS: 'restore-course-posts', - RESTORE_ORG_POSTS: 'restore-org-posts', }; /** @@ -112,8 +109,6 @@ export const PostsStatusFilter = { REPORTED: 'statusReported', UNANSWERED: 'statusUnanswered', UNRESPONDED: 'statusUnresponded', - ACTIVE: 'statusActive', - DELETED: 'statusDeleted', }; /** @@ -137,7 +132,6 @@ export const LearnersOrdering = { BY_FLAG: 'flagged', BY_LAST_ACTIVITY: 'activity', BY_RECENCY: 'recency', - BY_DELETED: 'deleted', }; /** diff --git a/src/discussions/common/ActionsDropdown.jsx b/src/discussions/common/ActionsDropdown.jsx index b88148ea7..359125081 100644 --- a/src/discussions/common/ActionsDropdown.jsx +++ b/src/discussions/common/ActionsDropdown.jsx @@ -78,13 +78,10 @@ const ActionsDropdown = ({ size="inline" onClick={() => { close(); - if (!action.disabled) { - handleActions(action.action); - } + handleActions(action.action); }} className="d-flex justify-content-start actions-dropdown-item" data-testId={action.id} - disabled={action.disabled} > { const intl = useIntl(); const { enableInContextSidebar } = useContext(DiscussionContext); @@ -51,9 +50,9 @@ const HoverCard = ({ 'px-2.5 py-2 border-0 font-style text-gray-700', { 'w-100': enableInContextSidebar }, )} - onClick={handleResponseCommentButton} - disabled={isClosed || isDeleted} - style={{ lineHeight: '20px', ...(isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}) }} + onClick={() => handleResponseCommentButton()} + disabled={isClosed} + style={{ lineHeight: '20px' }} > {addResponseCommentButtonMessage} @@ -79,8 +78,6 @@ const HoverCard = ({ className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'} size="sm" alt="Endorse" - disabled={isDeleted} - style={isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}} />
@@ -98,9 +95,8 @@ const HoverCard = ({ iconAs={Icon} size="sm" alt="Like" - disabled={!userHasLikePermission || isDeleted} + disabled={!userHasLikePermission} iconClassNames="like-icon-dimensions" - style={isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}} onClick={(e) => { e.preventDefault(); onLike(); @@ -123,8 +119,6 @@ const HoverCard = ({ size="sm" alt="Follow" iconClassNames="follow-icon-dimensions" - disabled={isDeleted} - style={isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}} onClick={(e) => { e.preventDefault(); onFollow(); @@ -171,14 +165,12 @@ HoverCard.propTypes = { )), onFollow: PropTypes.func, following: PropTypes.bool, - isDeleted: PropTypes.bool, }; HoverCard.defaultProps = { onFollow: () => null, endorseIcons: null, following: undefined, - isDeleted: false, }; export default React.memo(HoverCard); diff --git a/src/discussions/data/constants.js b/src/discussions/data/constants.js index e57eeda42..d8f434f36 100644 --- a/src/discussions/data/constants.js +++ b/src/discussions/data/constants.js @@ -10,8 +10,3 @@ export const ContentTypes = { POST: 'POST', COMMENT: 'COMMENT', }; - -export const THREAD_FILTER_TYPES = { - ACTIVE: 'active', - DELETED: 'deleted', -}; diff --git a/src/discussions/data/selectors.js b/src/discussions/data/selectors.js index 1ae6a50ad..d9f102a71 100644 --- a/src/discussions/data/selectors.js +++ b/src/discussions/data/selectors.js @@ -67,7 +67,7 @@ export function selectAreThreadsFiltered(state) { } return !( - (filters.status === PostsStatusFilter.ALL || filters.status === PostsStatusFilter.ACTIVE) + filters.status === PostsStatusFilter.ALL && filters.postType === ThreadType.ALL ); } diff --git a/src/discussions/learners/LearnerActionsDropdown.jsx b/src/discussions/learners/LearnerActionsDropdown.jsx index 65ee08fe6..9571ceefa 100644 --- a/src/discussions/learners/LearnerActionsDropdown.jsx +++ b/src/discussions/learners/LearnerActionsDropdown.jsx @@ -1,17 +1,16 @@ import React, { - useCallback, useEffect, useRef, useState, + useCallback, useRef, useState, } from 'react'; -import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { Button, Dropdown, Icon, IconButton, ModalPopup, useToggle, } from '@openedx/paragon'; -import { ChevronRight, MoreHoriz } from '@openedx/paragon/icons'; +import { MoreHoriz } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { useLearnerActionsMenu } from './utils'; +import { useLearnerActions } from './utils'; const LearnerActionsDropdown = ({ actionHandlers, @@ -22,16 +21,14 @@ const LearnerActionsDropdown = ({ const intl = useIntl(); const [isOpen, open, close] = useToggle(false); const [target, setTarget] = useState(null); - const [activeSubmenu, setActiveSubmenu] = useState(null); - const menuItems = useLearnerActionsMenu(intl, userHasBulkDeletePrivileges); + const actions = useLearnerActions(userHasBulkDeletePrivileges); const handleActions = useCallback((action) => { const actionFunction = actionHandlers[action]; if (actionFunction) { actionFunction(); - close(); } - }, [actionHandlers, close]); + }, [actionHandlers]); const onClickButton = useCallback((event) => { event.preventDefault(); @@ -42,15 +39,8 @@ const LearnerActionsDropdown = ({ const onCloseModal = useCallback(() => { close(); setTarget(null); - setActiveSubmenu(null); }, [close]); - // Cleanup portal on unmount to prevent memory leaks and orphaned DOM nodes - useEffect(() => () => { - setTarget(null); - setActiveSubmenu(null); - }, []); - return ( <>
- {isOpen && ReactDOM.createPortal( - +
-
- {menuItems.map(item => ( -
setActiveSubmenu(item.id)} - onMouseLeave={() => setActiveSubmenu(null)} - style={{ zIndex: 2 }} + {actions.map(action => ( + + { + close(); + handleActions(action.action); + }} + className="d-flex justify-content-start actions-dropdown-item" + data-testId={action.id} > - -
- - {item.label} - -
- -
- {activeSubmenu === item.id && ( -
- {item.submenu.map(subItem => ( - handleActions(subItem.action)} - className="d-flex justify-content-start actions-dropdown-item" - data-testid={subItem.id} - > - - {subItem.label} - - - ))} -
- )} -
- ))} -
- , - document.body, - )} + + + {action.label.defaultMessage} + + + + ))} +
+
); diff --git a/src/discussions/learners/LearnerActionsDropdown.test.jsx b/src/discussions/learners/LearnerActionsDropdown.test.jsx index 466276fb7..65b7ab0f9 100644 --- a/src/discussions/learners/LearnerActionsDropdown.test.jsx +++ b/src/discussions/learners/LearnerActionsDropdown.test.jsx @@ -79,10 +79,7 @@ describe('LearnerActionsDropdown', () => { const mockHandler = jest.fn(); renderComponent({ userHasBulkDeletePrivileges: true, - actionHandlers: { - [ContentActions.DELETE_COURSE_POSTS]: mockHandler, - [ContentActions.DELETE_ORG_POSTS]: mockHandler, - }, + actionHandlers: { deleteCoursePosts: mockHandler, deleteOrgPosts: mockHandler }, }); const openButton = await findOpenActionsDropdownButton(); @@ -90,12 +87,6 @@ describe('LearnerActionsDropdown', () => { fireEvent.click(openButton); }); - // Hover over the delete-activity menu item to show submenu - const deleteActivityItem = await screen.findByTestId('delete-activity'); - await act(async () => { - fireEvent.mouseEnter(deleteActivityItem); - }); - await waitFor(() => { const deleteCourseItem = screen.queryByTestId('delete-course-posts'); const deleteOrgItem = screen.queryByTestId('delete-org-posts'); @@ -122,12 +113,6 @@ describe('LearnerActionsDropdown', () => { await waitFor(() => expect(screen.queryByTestId('learner-actions-dropdown-modal-popup')).toBeInTheDocument()); - // Hover over the delete-activity menu item to show submenu - const deleteActivityItem = await screen.findByTestId('delete-activity'); - await act(async () => { - fireEvent.mouseEnter(deleteActivityItem); - }); - const deleteCourseItem = await screen.findByTestId('delete-course-posts'); await act(async () => { fireEvent.click(deleteCourseItem); @@ -156,12 +141,6 @@ describe('LearnerActionsDropdown', () => { await waitFor(() => expect(screen.queryByTestId('learner-actions-dropdown-modal-popup')).toBeInTheDocument()); - // Hover over the delete-activity menu item to show submenu - const deleteActivityItem = await screen.findByTestId('delete-activity'); - await act(async () => { - fireEvent.mouseEnter(deleteActivityItem); - }); - const deleteOrgItem = await screen.findByTestId('delete-org-posts'); await act(async () => { fireEvent.click(deleteOrgItem); diff --git a/src/discussions/learners/LearnerPostsView.jsx b/src/discussions/learners/LearnerPostsView.jsx index 5cd815d1a..846b254b3 100644 --- a/src/discussions/learners/LearnerPostsView.jsx +++ b/src/discussions/learners/LearnerPostsView.jsx @@ -32,13 +32,12 @@ import { threadsLoadingStatus, } from '../posts/data/selectors'; import { clearPostsPages } from '../posts/data/slices'; -import { fetchThread } from '../posts/data/thunks'; 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, undeleteUserPosts } from './data/thunks'; +import { deleteUserPosts, fetchUserPosts } from './data/thunks'; import LearnerPostFilterBar from './learner-post-filter-bar/LearnerPostFilterBar'; import LearnerActionsDropdown from './LearnerActionsDropdown'; import messages from './messages'; @@ -54,7 +53,7 @@ const LearnerPostsView = () => { const loadingStatus = useSelector(threadsLoadingStatus()); const learnerLoadingStatus = useSelector(learnersLoadingStatus()); const postFilter = useSelector(state => state.learners.postFilter); - const { courseId, learnerUsername: username, postId } = useContext(DiscussionContext); + const { courseId, learnerUsername: username } = useContext(DiscussionContext); const nextPage = useSelector(selectThreadNextPage()); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userIsStaff = useSelector(selectUserIsStaff); @@ -62,10 +61,7 @@ const LearnerPostsView = () => { const bulkDeleteStats = useSelector(selectBulkDeleteStats()); const sortedPostsIds = usePostList(postsIds); const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false); - const [isRestoring, showRestoreConfirmation, hideRestoreConfirmation] = useToggle(false); const [isDeletingCourseOrOrg, setIsDeletingCourseOrOrg] = useState(BulkDeleteType.COURSE); - const [isRestoringCourseOrOrg, setIsRestoringCourseOrOrg] = useState(BulkDeleteType.COURSE); - const [isLoadingRestoreData, setIsLoadingRestoreData] = useState(false); const loadMorePosts = useCallback((pageNum = undefined) => { const params = { @@ -83,57 +79,25 @@ const LearnerPostsView = () => { setIsDeletingCourseOrOrg(courseOrOrg); showDeleteConfirmation(); await dispatch(deleteUserPosts(courseId, username, courseOrOrg, false)); - }, [courseId, username, showDeleteConfirmation, dispatch]); + }, [courseId, username, showDeleteConfirmation]); const handleDeletePosts = useCallback(async (courseOrOrg) => { await dispatchDelete(deleteUserPosts(courseId, username, courseOrOrg, true)); - dispatch(clearPostsPages()); - loadMorePosts(); + navigate({ ...discussionsPath(Routes.LEARNERS.PATH, { courseId })(location) }); hideDeleteConfirmation(); - // If viewing a post, refresh it to show deleted state - if (postId) { - await dispatch(fetchThread(postId, courseId)); - } else { - // Navigate back to learners list after deletion - navigate({ ...discussionsPath(Routes.LEARNERS.PATH, { courseId })(location) }); - } - }, [courseId, username, hideDeleteConfirmation, dispatchDelete, navigate, location, postId, dispatch, loadMorePosts]); - - const handleShowRestoreConfirmation = useCallback(async (courseOrOrg) => { - setIsRestoringCourseOrOrg(courseOrOrg); - setIsLoadingRestoreData(true); - showRestoreConfirmation(); - await dispatch(undeleteUserPosts(courseId, username, courseOrOrg, false)); - setIsLoadingRestoreData(false); - }, [courseId, username, showRestoreConfirmation, dispatch]); - - const handleRestorePosts = useCallback(async (courseOrOrg) => { - await dispatch(undeleteUserPosts(courseId, username, courseOrOrg, true)); - dispatch(clearPostsPages()); - loadMorePosts(); - hideRestoreConfirmation(); - // If viewing a post, refresh it to show restored state - if (postId) { - await dispatch(fetchThread(postId, courseId)); - } else { - // Navigate back to learners list after restoration - navigate({ ...discussionsPath(Routes.LEARNERS.PATH, { courseId })(location) }); - } - }, [courseId, username, hideRestoreConfirmation, dispatch, loadMorePosts, postId, navigate, location]); + }, [courseId, username, hideDeleteConfirmation]); const actionHandlers = useMemo(() => ({ [ContentActions.DELETE_COURSE_POSTS]: () => handleShowDeleteConfirmation(BulkDeleteType.COURSE), [ContentActions.DELETE_ORG_POSTS]: () => handleShowDeleteConfirmation(BulkDeleteType.ORG), - [ContentActions.RESTORE_COURSE_POSTS]: () => handleShowRestoreConfirmation(BulkDeleteType.COURSE), - [ContentActions.RESTORE_ORG_POSTS]: () => handleShowRestoreConfirmation(BulkDeleteType.ORG), - }), [handleShowDeleteConfirmation, handleShowRestoreConfirmation]); + }), [handleShowDeleteConfirmation]); const postInstances = useMemo(() => ( - sortedPostsIds?.map((threadId, idx) => ( + sortedPostsIds?.map((postId, idx) => ( )) @@ -206,19 +170,6 @@ const LearnerPostsView = () => { isConfirmButtonPending={bulkDeleting} pendingConfirmButtonText={intl.formatMessage(messages.deletePostConfirmPending)} /> - handleRestorePosts(isRestoringCourseOrOrg)} - confirmButtonText={intl.formatMessage(messages.restorePostsConfirm)} - confirmButtonVariant="primary" - isDataLoading={isLoadingRestoreData} - />
); }; diff --git a/src/discussions/learners/LearnerPostsView.test.jsx b/src/discussions/learners/LearnerPostsView.test.jsx index 188d27f81..dcca0656d 100644 --- a/src/discussions/learners/LearnerPostsView.test.jsx +++ b/src/discussions/learners/LearnerPostsView.test.jsx @@ -244,12 +244,6 @@ describe('Learner Posts View', () => { fireEvent.click(actionsButton); }); - // Hover over the delete-activity menu item to show submenu - const deleteActivityItem = await screen.findByTestId('delete-activity'); - await act(async () => { - fireEvent.mouseEnter(deleteActivityItem); - }); - const deleteCourseItem = await screen.findByTestId('delete-course-posts'); await act(async () => { fireEvent.click(deleteCourseItem); @@ -278,12 +272,6 @@ describe('Learner Posts View', () => { fireEvent.click(actionsButton); }); - // Hover over the delete-activity menu item to show submenu - const deleteActivityItem = await screen.findByTestId('delete-activity'); - await act(async () => { - fireEvent.mouseEnter(deleteActivityItem); - }); - const deleteCourseItem = await screen.findByTestId('delete-course-posts'); await act(async () => { fireEvent.click(deleteCourseItem); @@ -315,12 +303,6 @@ describe('Learner Posts View', () => { fireEvent.click(actionsButton); }); - // Hover over the delete-activity menu item to show submenu - const deleteActivityItem = await screen.findByTestId('delete-activity'); - await act(async () => { - fireEvent.mouseEnter(deleteActivityItem); - }); - const deleteCourseItem = await screen.findByTestId('delete-course-posts'); await act(async () => { fireEvent.click(deleteCourseItem); @@ -351,12 +333,6 @@ describe('Learner Posts View', () => { fireEvent.click(actionsButton); }); - // Hover over the delete-activity menu item to show submenu - const deleteActivityItem = await screen.findByTestId('delete-activity'); - await act(async () => { - fireEvent.mouseEnter(deleteActivityItem); - }); - const deleteOrgItem = await screen.findByTestId('delete-org-posts'); await act(async () => { fireEvent.click(deleteOrgItem); diff --git a/src/discussions/learners/data/api.js b/src/discussions/learners/data/api.js index 215868fcd..05121079e 100644 --- a/src/discussions/learners/data/api.js +++ b/src/discussions/learners/data/api.js @@ -11,9 +11,7 @@ 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 deletedContentApiUrl = (courseId) => `${getConfig().LMS_BASE_URL}/api/discussion/v1/deleted_content/${courseId}`; 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}`; -export const restorePostsApiUrl = (courseId, username, courseOrOrg, execute) => `${getConfig().LMS_BASE_URL}/api/discussion/v1/bulk_restore_user_posts/${courseId}?username=${username}&course_or_org=${courseOrOrg}&execute=${execute}`; /** * Fetches all the learners in the given course. @@ -51,7 +49,6 @@ export async function getUserProfiles(usernames) { * @param {ThreadViewStatus} view Set to "unread" on "unanswered" to filter to only those statuses. * @param {boolean} countFlagged If true, abuseFlaggedCount will be available. * @param {number} cohort - * @param {boolean} showDeleted If true, only deleted posts will be returned. * @returns API Response object in the format * { * results: [array of posts], @@ -68,7 +65,6 @@ export async function getUserPosts(courseId, { threadType, countFlagged, cohort, - showDeleted, } = {}) { const params = snakeCaseObject({ page, @@ -81,7 +77,6 @@ export async function getUserPosts(courseId, { username: author, countFlagged, groupId: cohort, - showDeleted, }); const { data } = await getAuthenticatedHttpClient() @@ -108,55 +103,3 @@ export async function deleteUserPostsApi(courseId, username, courseOrOrg, execut ); return data; } - -/** - * Restores deleted 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 restored - * @param {string} courseOrOrg Can be 'course' or 'org' to specify restoration scope - * @param {boolean} execute If true, restores 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 restoreUserPostsApi(courseId, username, courseOrOrg, execute) { - const { data } = await getAuthenticatedHttpClient().post( - restorePostsApiUrl(courseId, username, courseOrOrg, execute), - null, - ); - return data; -} - -/** - * Get deleted content for a course - * - * @param {string} courseId Course ID of the course - * @param {string} author Optional - filter by author username - * @param {number} page Page number for pagination - * @param {number} pageSize Number of items per page - * @param {string} contentType Optional - filter by 'thread' or 'comment' - * @returns API Response object in the format - * { - * results: [array of deleted posts], - * pagination: {count, num_pages, next, previous} - * } - */ -export async function getDeletedContent(courseId, { - author, - page, - pageSize, - contentType, -} = {}) { - const params = snakeCaseObject({ - authorId: author, // The backend expects author_id - page, - perPage: pageSize, - contentType, - }); - - const { data } = await getAuthenticatedHttpClient() - .get(deletedContentApiUrl(courseId), { params }); - return data; -} diff --git a/src/discussions/learners/data/redux.test.jsx b/src/discussions/learners/data/redux.test.jsx index fee4b314d..6156188fb 100644 --- a/src/discussions/learners/data/redux.test.jsx +++ b/src/discussions/learners/data/redux.test.jsx @@ -52,7 +52,6 @@ describe('Learner redux test cases', () => { expect(learners.usernameSearch).toBeNull(); expect(learners.postFilter.postType).toEqual('all'); expect(learners.postFilter.status).toEqual('statusAll'); - expect(learners.postFilter.contentStatus).toEqual('statusActive'); expect(learners.postFilter.orderBy).toEqual('lastActivityAt'); expect(learners.postFilter.cohort).toEqual(''); }); @@ -98,10 +97,14 @@ describe('Learner redux test cases', () => { test('Successfully updated the post-filter data in redux', async () => { const learners = await setupLearnerMockResponse(); + const filter = { + ...learners.postFilter, + postType: 'discussion', + }; expect(learners.postFilter.postType).toEqual('all'); - await store.dispatch(setPostFilter({ postType: 'discussion' })); + await store.dispatch(setPostFilter(filter)); const updatedLearners = store.getState().learners; expect(updatedLearners.postFilter.postType).toEqual('discussion'); diff --git a/src/discussions/learners/data/slices.js b/src/discussions/learners/data/slices.js index c80407f0f..534fe850e 100644 --- a/src/discussions/learners/data/slices.js +++ b/src/discussions/learners/data/slices.js @@ -20,8 +20,7 @@ const learnersSlice = createSlice({ sortedBy: LearnersOrdering.BY_LAST_ACTIVITY, postFilter: { postType: ThreadType.ALL, - status: PostsStatusFilter.ALL, // secondary status (Unread, etc.) - contentStatus: PostsStatusFilter.ACTIVE, // main content status (Active/Deleted) + status: PostsStatusFilter.ALL, orderBy: ThreadOrdering.BY_LAST_ACTIVITY, cohort: '', }, @@ -86,10 +85,7 @@ const learnersSlice = createSlice({ { ...state, pages: [], - postFilter: { - ...state.postFilter, - ...payload, - }, + postFilter: payload, } ), deleteUserPostsRequest: (state) => ( @@ -111,26 +107,6 @@ const learnersSlice = createSlice({ status: RequestStatus.FAILED, } ), - undeleteUserPostsRequest: (state) => ( - { - ...state, - status: RequestStatus.IN_PROGRESS, - } - ), - undeleteUserPostsSuccess: (state, { payload }) => ( - { - ...state, - status: RequestStatus.SUCCESSFUL, - bulkDeleteStats: payload, - bulkUndeleteStats: payload, - } - ), - undeleteUserPostsFailed: (state) => ( - { - ...state, - status: RequestStatus.FAILED, - } - ), }, }); @@ -145,9 +121,6 @@ export const { deleteUserPostsRequest, deleteUserPostsSuccess, deleteUserPostsFailed, - undeleteUserPostsRequest, - undeleteUserPostsSuccess, - undeleteUserPostsFailed, } = learnersSlice.actions; export const learnersReducer = learnersSlice.reducer; diff --git a/src/discussions/learners/data/thunks.js b/src/discussions/learners/data/thunks.js index 0a4e041a9..afc554b63 100644 --- a/src/discussions/learners/data/thunks.js +++ b/src/discussions/learners/data/thunks.js @@ -14,11 +14,9 @@ import { normaliseThreads } from '../../posts/data/thunks'; import { getHttpErrorStatus } from '../../utils'; import { deleteUserPostsApi, - getDeletedContent, getLearners, getUserPosts, getUserProfiles, - restoreUserPostsApi, } from './api'; import { deleteUserPostsFailed, @@ -28,9 +26,6 @@ import { fetchLearnersFailed, fetchLearnersRequest, fetchLearnersSuccess, - undeleteUserPostsFailed, - undeleteUserPostsRequest, - undeleteUserPostsSuccess, } from './slices'; /** @@ -89,65 +84,38 @@ export function fetchUserPosts(courseId, { author = null, countFlagged, } = {}) { + const options = { + orderBy, + page, + author, + countFlagged, + }; + if (filters.status === PostsStatusFilter.UNREAD) { + options.status = 'unread'; + } + if (filters.status === PostsStatusFilter.UNANSWERED) { + options.status = 'unanswered'; + } + if (filters.status === PostsStatusFilter.REPORTED) { + options.status = 'flagged'; + } + if (filters.status === PostsStatusFilter.UNRESPONDED) { + options.status = 'unresponded'; + } + if (filters.postType !== ThreadType.ALL) { + options.threadType = filters.postType; + } + if (filters.search) { + options.textSearch = filters.search; + } + if (filters.cohort) { + options.cohort = filters.cohort; + } return async (dispatch) => { try { dispatch(fetchLearnerThreadsRequest({ courseId, author })); - let data; - - // Use dedicated deleted content endpoint when viewing deleted posts - if (filters.contentStatus === PostsStatusFilter.DELETED) { - try { - data = await getDeletedContent(courseId, { - author, - page, - pageSize: 10, - }); - } catch (importError) { - logError('Failed to fetch deleted content:', importError); - throw importError; - } - } else { - // Use regular learner posts endpoint for active content - const options = { - orderBy, - page, - author, - countFlagged, - }; - - // Only show active content (not deleted) - if (filters.contentStatus === PostsStatusFilter.ACTIVE) { - options.showDeleted = false; - } - - // Map of status filters to their API values - const statusMap = { - [PostsStatusFilter.UNREAD]: 'unread', - [PostsStatusFilter.UNANSWERED]: 'unanswered', - [PostsStatusFilter.REPORTED]: 'flagged', - [PostsStatusFilter.UNRESPONDED]: 'unresponded', - }; - - // Apply status filter if it exists in the map - if (statusMap[filters.status]) { - options.status = statusMap[filters.status]; - } - - // Apply additional filters - if (filters.postType !== ThreadType.ALL) { - options.threadType = filters.postType; - } - if (filters.search) { - options.textSearch = filters.search; - } - if (filters.cohort) { - options.cohort = filters.cohort; - } - - data = await getUserPosts(courseId, options); - } - + const data = await getUserPosts(courseId, options); const normalisedData = normaliseThreads(camelCaseObject(data)); dispatch(fetchThreadsSuccess({ ...normalisedData, page, author })); @@ -162,28 +130,18 @@ export function fetchUserPosts(courseId, { }; } -export function deleteUserPosts(courseId, username, courseOrOrg, execute) { - return 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); - } - }; -} - -export function undeleteUserPosts(courseId, username, courseOrOrg, execute) { - return async (dispatch) => { - try { - dispatch(undeleteUserPostsRequest({ courseId, username })); - const response = await restoreUserPostsApi(courseId, username, courseOrOrg, execute); - dispatch(undeleteUserPostsSuccess(camelCaseObject(response))); - } catch (error) { - dispatch(undeleteUserPostsFailed()); - logError(error); - } - }; -} +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/learner-post-filter-bar/LearnerPostFilterBar.jsx b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.jsx index c3f698903..fafdc8799 100644 --- a/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.jsx +++ b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.jsx @@ -7,9 +7,10 @@ import { useParams } from 'react-router-dom'; 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 { selectUserHasModerationPrivileges, selectUserIsGroupTa, selectUserIsStaff } from '../../data/selectors'; +import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors'; import { setPostFilter } from '../data/slices'; const LearnerPostFilterBar = () => { @@ -17,7 +18,6 @@ const LearnerPostFilterBar = () => { const { courseId } = useParams(); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userIsGroupTa = useSelector(selectUserIsGroupTa); - const userIsStaff = useSelector(selectUserIsStaff); const cohorts = useSelector(selectCourseCohorts); const postFilter = useSelector(state => state.learners.postFilter); @@ -36,17 +36,7 @@ const LearnerPostFilterBar = () => { }, ]; - // Add content status filter only for staff, moderators, and TAs - if (userHasModerationPrivileges || userIsGroupTa || userIsStaff) { - filtersToShow.push({ - name: 'contentStatus', // main content status - filters: ['status-active', 'status-deleted'], - hasSeparator: true, - }); - } - if (userHasModerationPrivileges || userIsGroupTa) { - // Add reported filter only for group TA and moderators filtersToShow[1].filters.splice(2, 0, 'status-reported'); } @@ -61,27 +51,40 @@ const LearnerPostFilterBar = () => { }; if (name === 'postType') { if (postFilter.postType !== value) { - dispatch(setPostFilter({ postType: value })); + dispatch(setPostFilter({ + ...postFilter, + postType: value, + })); filterContentEventProperties.threadTypeFilter = value; } } else if (name === 'status') { if (postFilter.status !== value) { - dispatch(setPostFilter({ status: value })); + const postType = (value === PostsStatusFilter.UNANSWERED && ThreadType.QUESTION) + || (value === PostsStatusFilter.UNRESPONDED && ThreadType.DISCUSSION) + || postFilter.postType; + + dispatch(setPostFilter({ + ...postFilter, + postType, + status: value, + })); + filterContentEventProperties.statusFilter = value; } - } else if (name === 'contentStatus') { - if (postFilter.contentStatus !== value) { - dispatch(setPostFilter({ contentStatus: value })); - filterContentEventProperties.contentStatusFilter = value; - } } else if (name === 'orderBy') { if (postFilter.orderBy !== value) { - dispatch(setPostFilter({ orderBy: value })); + dispatch(setPostFilter({ + ...postFilter, + orderBy: value, + })); filterContentEventProperties.sortFilter = value; } } else if (name === 'cohort') { if (postFilter.cohort !== value) { - dispatch(setPostFilter({ cohort: value })); + dispatch(setPostFilter({ + ...postFilter, + cohort: value, + })); filterContentEventProperties.cohortFilter = value; } } 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 235cba254..4c62f2753 100644 --- a/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx +++ b/src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx @@ -68,7 +68,7 @@ describe('LearnerPostFilterBar', () => { fireEvent.click(queryAllByRole('button')[0]); }); await waitFor(() => { - expect(queryAllByRole('radiogroup')).toHaveLength(5); + expect(queryAllByRole('radiogroup')).toHaveLength(4); }); }); @@ -78,24 +78,17 @@ describe('LearnerPostFilterBar', () => { fireEvent.click(queryAllByRole('button')[0]); }); await waitFor(() => { - const radiogroups = queryAllByRole('radiogroup'); - // Radiogroup 0: postType filter - default is 'all' expect( - radiogroups[0].querySelector('input[value="all"]'), + queryAllByRole('radiogroup')[0].querySelector('input[value="all"]'), ).toBeChecked(); - // Radiogroup 1: status filter (any/unread/reported/unanswered/unresponded) - // - not checked since default is statusActive - // Radiogroup 2: orderBy filter - default is 'lastActivityAt' expect( - radiogroups[2].querySelector('input[value="lastActivityAt"]'), + queryAllByRole('radiogroup')[1].querySelector('input[value="statusAll"]'), ).toBeChecked(); - // Radiogroup 3: active/deleted status filter - default is 'statusActive' expect( - radiogroups[3].querySelector('input[value="statusActive"]'), + queryAllByRole('radiogroup')[2].querySelector('input[value="lastActivityAt"]'), ).toBeChecked(); - // Radiogroup 4: cohort filter - default is empty string expect( - radiogroups[4].querySelector('input[value=""]'), + queryAllByRole('radiogroup')[3].querySelector('input[value=""]'), ).toBeChecked(); }); }); diff --git a/src/discussions/learners/learner/LearnerCard.jsx b/src/discussions/learners/learner/LearnerCard.jsx index 7100921b8..8554b3855 100644 --- a/src/discussions/learners/learner/LearnerCard.jsx +++ b/src/discussions/learners/learner/LearnerCard.jsx @@ -13,7 +13,6 @@ import learnerShape from './proptypes'; const LearnerCard = ({ learner }) => { const { username, threads, inactiveFlags, activeFlags, responses, replies, - deletedCount, deletedThreads, deletedResponses, deletedReplies, } = learner; const { enableInContextSidebar, learnerUsername, courseId } = useContext(DiscussionContext); const linkUrl = discussionsPath(Routes.LEARNERS.POSTS, { @@ -52,10 +51,6 @@ const LearnerCard = ({ learner }) => { responses={responses} replies={replies} username={username} - deletedCount={deletedCount} - deletedThreads={deletedThreads} - deletedResponses={deletedResponses} - deletedReplies={deletedReplies} /> )} diff --git a/src/discussions/learners/learner/LearnerFilterBar.jsx b/src/discussions/learners/learner/LearnerFilterBar.jsx index fd372ac93..32f91218f 100644 --- a/src/discussions/learners/learner/LearnerFilterBar.jsx +++ b/src/discussions/learners/learner/LearnerFilterBar.jsx @@ -10,7 +10,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { useIntl } from '@edx/frontend-platform/i18n'; import { LearnersOrdering } from '../../../data/constants'; -import { selectUserHasModerationPrivileges, selectUserIsGroupTa, selectUserIsStaff } from '../../data/selectors'; +import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors'; import { setSortedBy } from '../data'; import { selectLearnerSorting } from '../data/selectors'; import messages from '../messages'; @@ -52,7 +52,6 @@ const LearnerFilterBar = () => { const dispatch = useDispatch(); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userIsGroupTa = useSelector(selectUserIsGroupTa); - const userIsStaff = useSelector(selectUserIsStaff); const currentSorting = useSelector(selectLearnerSorting()); const [isOpen, setOpen] = useState(false); @@ -119,14 +118,6 @@ const LearnerFilterBar = () => { value={LearnersOrdering.BY_RECENCY} selected={currentSorting} /> - {(userHasModerationPrivileges || userIsGroupTa || userIsStaff) && ( - - )} diff --git a/src/discussions/learners/learner/LearnerFooter.jsx b/src/discussions/learners/learner/LearnerFooter.jsx index b21d2db56..ce4adc9e0 100644 --- a/src/discussions/learners/learner/LearnerFooter.jsx +++ b/src/discussions/learners/learner/LearnerFooter.jsx @@ -3,28 +3,22 @@ import PropTypes from 'prop-types'; import { Icon, OverlayTrigger, Tooltip } from '@openedx/paragon'; import { - DeleteOutline, Edit, QuestionAnswerOutline, Report, ReportGmailerrorred, + Edit, QuestionAnswerOutline, Report, ReportGmailerrorred, } from '@openedx/paragon/icons'; import { useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { selectUserHasModerationPrivileges, selectUserIsGroupTa, selectUserIsStaff } from '../../data/selectors'; +import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors'; import messages from '../messages'; const LearnerFooter = ({ inactiveFlags, activeFlags, threads, responses, replies, username, - deletedThreads, deletedResponses, deletedReplies, }) => { const intl = useIntl(); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userIsGroupTa = useSelector(selectUserIsGroupTa); - const userIsStaff = useSelector(selectUserIsStaff); const canSeeLearnerReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa); - const canSeeDeletedStats = userHasModerationPrivileges || userIsGroupTa || userIsStaff; - - // Calculate deleted count (sum of all deleted content) - const totalDeletedCount = (deletedThreads || 0) + (deletedResponses || 0) + (deletedReplies || 0); return (
@@ -60,24 +54,6 @@ const LearnerFooter = ({ {threads}
- {Boolean(canSeeDeletedStats) && ( - -
- {intl.formatMessage(messages.deletedActivity)} -
- - )} - > -
- - {totalDeletedCount} -
-
- )} {Boolean(canSeeLearnerReportedStats) && ( { - if (!userHasBulkDeletePrivileges) { - return []; - } - return [ - { - id: 'delete-activity', - icon: Delete, - label: intl.formatMessage(messages.deleteActivity), - submenu: [ - { - id: 'delete-course-posts', - action: ContentActions.DELETE_COURSE_POSTS, - label: intl.formatMessage(messages.deleteCoursePosts), - }, - { - id: 'delete-org-posts', - action: ContentActions.DELETE_ORG_POSTS, - label: intl.formatMessage(messages.deleteOrgPosts), - }, - ], - }, - { - id: 'restore-activity', - icon: Undelete, - label: intl.formatMessage(messages.restoreActivity), - submenu: [ - { - id: 'restore-course-posts', - action: ContentActions.RESTORE_COURSE_POSTS, - label: intl.formatMessage(messages.restoreCoursePosts), - }, - { - id: 'restore-org-posts', - action: ContentActions.RESTORE_ORG_POSTS, - label: intl.formatMessage(messages.restoreOrgPosts), - }, - ], - }, - ]; - }, [userHasBulkDeletePrivileges, intl]); - - return menuItems; -} diff --git a/src/discussions/messages.js b/src/discussions/messages.js index 7d9ba86ca..3589aaf0d 100644 --- a/src/discussions/messages.js +++ b/src/discussions/messages.js @@ -31,11 +31,6 @@ const messages = defineMessages({ defaultMessage: 'Delete', description: 'Action to delete a post or comment', }, - restoreAction: { - id: 'discussions.actions.restore', - defaultMessage: 'Restore', - description: 'Action to restore a deleted post or comment', - }, confirmationConfirm: { id: 'discussions.confirmation.button.confirm', defaultMessage: 'Confirm', @@ -248,51 +243,6 @@ const messages = defineMessages({ defaultMessage: 'Faculty and staff will never invite you to join external groups or ask for personal or financial information in the discussions. Stay safe, and if you see suspicious activity, please report it.', description: 'Warning message about spam and impersonation in discussion forums', }, - activeThreads: { - id: 'discussions.filter.activeThreads', - defaultMessage: 'Active Threads', - description: 'Label for active threads filter button', - }, - deletedThreads: { - id: 'discussions.filter.deletedThreads', - defaultMessage: 'Deleted Threads', - description: 'Label for deleted threads filter button', - }, - deletedBadge: { - id: 'discussions.thread.deletedBadge', - defaultMessage: 'Deleted', - description: 'Badge shown on deleted threads', - }, - selectedCount: { - id: 'discussions.bulk.selectedCount', - defaultMessage: '{count} selected', - description: 'Count of selected threads for bulk actions', - }, - deleteSelected: { - id: 'discussions.bulk.deleteSelected', - defaultMessage: 'Delete Selected', - description: 'Button text for bulk delete action', - }, - restoreSelected: { - id: 'discussions.bulk.restoreSelected', - defaultMessage: 'Restore Selected', - description: 'Button text for bulk restore action', - }, - deleting: { - id: 'discussions.bulk.deleting', - defaultMessage: 'Deleting...', - description: 'Loading text when bulk deleting threads', - }, - restoring: { - id: 'discussions.bulk.restoring', - defaultMessage: 'Restoring...', - description: 'Loading text when bulk restoring threads', - }, - loadingThreads: { - id: 'discussions.threads.loading', - defaultMessage: 'Loading threads...', - description: 'Loading text when fetching threads', - }, autoSpamFlaggedMessage: { id: 'discussions.autoSpamFlaggedMessage', defaultMessage: 'Content automatically reported as possible spam pending staff review.', diff --git a/src/discussions/post-comments/PostCommentsView.test.jsx b/src/discussions/post-comments/PostCommentsView.test.jsx index 59d6272e7..2de528732 100644 --- a/src/discussions/post-comments/PostCommentsView.test.jsx +++ b/src/discussions/post-comments/PostCommentsView.test.jsx @@ -80,7 +80,6 @@ async function mockAxiosReturnPagedCommentsResponses() { page_size: undefined, requested_fields: 'profile_image', reverse_order: true, - show_deleted: false, }; [1, 2].forEach(async (page) => { diff --git a/src/discussions/post-comments/comments/comment/Comment.jsx b/src/discussions/post-comments/comments/comment/Comment.jsx index 7330d626b..c2d96be80 100644 --- a/src/discussions/post-comments/comments/comment/Comment.jsx +++ b/src/discussions/post-comments/comments/comment/Comment.jsx @@ -8,19 +8,18 @@ import React, { import PropTypes from 'prop-types'; import { Button, useToggle } from '@openedx/paragon'; -import { DeleteOutline } from '@openedx/paragon/icons'; import classNames from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { logError } from '@edx/frontend-platform/logging'; import HTMLLoader from '../../../../components/HTMLLoader'; +import { ContentActions, EndorsementStatus } from '../../../../data/constants'; import { - AvatarOutlineAndLabelColors, ContentActions, EndorsementStatus, PostsStatusFilter, -} from '../../../../data/constants'; -import { - AlertBanner, AuthorLabel, AutoSpamAlertBanner, Confirmation, EndorsedAlertBanner, + AlertBanner, + AutoSpamAlertBanner, + Confirmation, + EndorsedAlertBanner, } from '../../../common'; import DiscussionContext from '../../../common/context'; import HoverCard from '../../../common/HoverCard'; @@ -28,7 +27,6 @@ import withPostingRestrictions from '../../../common/withPostingRestrictions'; import { ContentTypes } from '../../../data/constants'; import { useUserPostingEnabled } from '../../../data/hooks'; import { selectContentCreationRateLimited, selectShouldShowEmailConfirmation } from '../../../data/selectors'; -import { selectThread } from '../../../posts/data/selectors'; import { fetchThread } from '../../../posts/data/thunks'; import LikeButton from '../../../posts/post/LikeButton'; import { useActions } from '../../../utils'; @@ -57,21 +55,17 @@ const Comment = ({ const { id, parentId, childCount, abuseFlagged, endorsed, threadId, endorsedAt, endorsedBy, endorsedByLabel, renderedBody, voted, following, voteCount, authorLabel, author, createdAt, lastEdit, rawBody, closed, closedBy, closeReason, - editByLabel, closedByLabel, users: postUsers, isDeleted, deletedBy, deletedByLabel, is_spam: isSpam, + editByLabel, closedByLabel, users: postUsers, is_spam: isSpam, } = comment; const intl = useIntl(); const hasChildren = childCount > 0; const isNested = Boolean(parentId); const dispatch = useDispatch(); - const { courseId, learnerUsername } = useContext(DiscussionContext); + const { courseId } = useContext(DiscussionContext); const { isClosed } = useContext(PostCommentsContext); - // Get the post's isDeleted state for priority rules - const post = useSelector(selectThread(threadId)); - const postIsDeleted = post?.isDeleted || false; const [isEditing, setEditing] = useState(false); const [isReplying, setReplying] = useState(false); const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false); - const [isRestoring, showRestoreConfirmation, hideRestoreConfirmation] = useToggle(false); const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false); const inlineReplies = useSelector(selectCommentResponses(id)); const inlineRepliesIds = useSelector(selectCommentResponsesIds(id)); @@ -82,11 +76,6 @@ const Comment = ({ const isUserPrivilegedInPostingRestriction = useUserPostingEnabled(); const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation); const contentCreationRateLimited = useSelector(selectContentCreationRateLimited); - const postFilter = useSelector(state => state.learners?.postFilter); - // Use contentStatus for deleted section - const showDeleted = Boolean( - learnerUsername && postFilter?.contentStatus === PostsStatusFilter.DELETED, - ); // If isSpam is not provided in the API response, default to false const isSpamFlagged = isSpam || false; useEffect(() => { @@ -95,10 +84,9 @@ const Comment = ({ dispatch(fetchCommentResponses(id, { page: 1, reverseOrder: sortedOrder, - showDeleted, })); } - }, [id, sortedOrder, showDeleted]); + }, [id, sortedOrder]); const endorseIcons = useMemo(() => ( actions.find(({ action }) => action === EndorsementStatus.ENDORSED) @@ -135,39 +123,19 @@ const Comment = ({ await dispatch(editComment(id, { voted: !voted })); }, [id, voted]); - const handleRestore = useCallback(() => { - showRestoreConfirmation(); - }, [showRestoreConfirmation]); - - const handleRestoreConfirmation = useCallback(async () => { - try { - const { performRestoreComment } = await import('../../data/thunks'); - const result = await dispatch(performRestoreComment(id, courseId)); - if (result.success) { - // Refresh the thread to reflect the change - await dispatch(fetchThread(threadId, courseId)); - } - } catch (error) { - logError(error); - } - hideRestoreConfirmation(); - }, [id, courseId, threadId, dispatch, hideRestoreConfirmation]); - const actionHandlers = useMemo(() => ({ [ContentActions.EDIT_CONTENT]: handleEditContent, [ContentActions.ENDORSE]: handleCommentEndorse, [ContentActions.DELETE]: showDeleteConfirmation, - [ContentActions.RESTORE]: handleRestore, [ContentActions.REPORT]: handleAbusedFlag, - }), [handleEditContent, handleCommentEndorse, showDeleteConfirmation, handleRestore, handleAbusedFlag]); + }), [handleEditContent, handleCommentEndorse, showDeleteConfirmation, handleAbusedFlag]); const handleLoadMoreComments = useCallback(() => ( dispatch(fetchCommentResponses(id, { page: currentPage + 1, reverseOrder: sortedOrder, - showDeleted, })) - ), [id, currentPage, sortedOrder, showDeleted]); + ), [id, currentPage, sortedOrder]); const handleAddCommentButton = useCallback(() => { if (isUserPrivilegedInPostingRestriction) { @@ -205,18 +173,6 @@ const Comment = ({ closeButtonVariant="tertiary" confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)} /> - {!abuseFlagged && ( - {isDeleted && deletedBy && ( -
- -
- {intl.formatMessage(messages.deletedBy)} - - - -
-
- )} {isEditing ? ( diff --git a/src/discussions/post-comments/comments/comment/CommentHeader.jsx b/src/discussions/post-comments/comments/comment/CommentHeader.jsx index c51420d6c..7674292f2 100644 --- a/src/discussions/post-comments/comments/comment/CommentHeader.jsx +++ b/src/discussions/post-comments/comments/comment/CommentHeader.jsx @@ -40,7 +40,7 @@ const CommentHeader = ({ 'mt-2': hasAnyAlert, })} > -
+
{ const commentData = useSelector(selectCommentOrResponseById(responseId)); const { id, abuseFlagged, author, authorLabel, endorsed, lastEdit, closed, closedBy, - closeReason, createdAt, threadId, parentId, rawBody, renderedBody, editByLabel, - closedByLabel, isDeleted, deletedBy, deletedByLabel, is_spam: isSpam, + closeReason, createdAt, threadId, parentId, rawBody, renderedBody, editByLabel, closedByLabel, is_spam: isSpam, } = commentData; const intl = useIntl(); const dispatch = useDispatch(); - const { courseId } = useContext(DiscussionContext); const [isEditing, setEditing] = useState(false); const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false); - const [isRestoring, showRestoreConfirmation, hideRestoreConfirmation] = useToggle(false); const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false); const colorClass = AvatarOutlineAndLabelColors[authorLabel]; // If isSpam is not provided in the API response, default to false @@ -79,22 +70,6 @@ const Reply = ({ responseId }) => { } }, [abuseFlagged, id, showReportConfirmation]); - const handleRestore = useCallback(() => { - showRestoreConfirmation(); - }, [showRestoreConfirmation]); - - const handleRestoreConfirmation = useCallback(async () => { - try { - const result = await dispatch(performRestoreComment(id, courseId)); - if (result.success) { - await dispatch(fetchThread(threadId, courseId)); - } - } catch (error) { - logError(error); - } - hideRestoreConfirmation(); - }, [id, courseId, threadId, dispatch, hideRestoreConfirmation]); - const handleCloseEditor = useCallback(() => { setEditing(false); }, []); @@ -103,9 +78,8 @@ const Reply = ({ responseId }) => { [ContentActions.EDIT_CONTENT]: handleEditContent, [ContentActions.ENDORSE]: handleReplyEndorse, [ContentActions.DELETE]: showDeleteConfirmation, - [ContentActions.RESTORE]: handleRestore, [ContentActions.REPORT]: handleAbusedFlag, - }), [handleEditContent, handleReplyEndorse, showDeleteConfirmation, handleRestore, handleAbusedFlag]); + }), [handleEditContent, handleReplyEndorse, showDeleteConfirmation, handleAbusedFlag]); return (
@@ -118,14 +92,6 @@ const Reply = ({ responseId }) => { closeButtonVariant="tertiary" confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)} /> - {!abuseFlagged && ( {
)} - {isDeleted && deletedBy && ( -
-
- -
-
-
- -
- {intl.formatMessage(messages.deletedBy)} - - - -
-
-
-
- )}
{ const params = snakeCaseObject({ @@ -36,7 +35,6 @@ export const getThreadComments = async (threadId, { requestedFields: 'profile_image', enableInContextSidebar, mergeQuestionTypeResponses: threadType === ThreadType.QUESTION ? true : null, - showDeleted, }); const { data } = await getAuthenticatedHttpClient().get(getCommentsApiUrl(), { params: { ...params, signal } }); @@ -54,7 +52,6 @@ export const getCommentResponses = async (commentId, { page, pageSize, reverseOrder, - showDeleted = false, } = {}) => { const url = `${getCommentsApiUrl()}${commentId}/`; const params = snakeCaseObject({ @@ -62,7 +59,6 @@ export const getCommentResponses = async (commentId, { pageSize, requestedFields: 'profile_image', reverseOrder, - showDeleted, }); const { data } = await getAuthenticatedHttpClient() .get(url, { params }); @@ -131,19 +127,3 @@ export const deleteComment = async (commentId) => { await getAuthenticatedHttpClient() .delete(url); }; - -/** - * Restores a deleted comment. - * @param {string} commentId ID of comment to restore - * @param {string} courseId Course ID - * @returns {Promise<{}>} - */ -export const restoreComment = async (commentId, courseId) => { - const url = `${getConfig().LMS_BASE_URL}/api/discussion/v1/restore_content`; - const { data } = await getAuthenticatedHttpClient().post(url, { - content_type: 'comment', - content_id: commentId, - course_id: courseId, - }); - return data; -}; diff --git a/src/discussions/post-comments/data/hooks.js b/src/discussions/post-comments/data/hooks.js index 3252dfda5..33f71172d 100644 --- a/src/discussions/post-comments/data/hooks.js +++ b/src/discussions/post-comments/data/hooks.js @@ -7,7 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { EndorsementStatus, PostsStatusFilter } from '../../../data/constants'; +import { EndorsementStatus } from '../../../data/constants'; import useDispatchWithState from '../../../data/hooks'; import DiscussionContext from '../../common/context'; import { selectThread } from '../../posts/data/selectors'; @@ -42,14 +42,6 @@ export function usePost(postId) { return thread || {}; } -const useShowDeletedContent = () => { - const { learnerUsername } = useContext(DiscussionContext); - const postFilter = useSelector(state => state.learners.postFilter); - - // Show deleted content if we're in learner view and the deleted filter is active (contentStatus) - return learnerUsername && postFilter?.contentStatus === PostsStatusFilter.DELETED; -}; - export function usePostComments(threadType) { const { enableInContextSidebar, postId } = useContext(DiscussionContext); const [isLoading, dispatch] = useDispatchWithState(); @@ -57,7 +49,6 @@ export function usePostComments(threadType) { const reverseOrder = useSelector(selectCommentSortOrder); const hasMorePages = useSelector(selectThreadHasMorePages(postId)); const currentPage = useSelector(selectThreadCurrentPage(postId)); - const showDeleted = useShowDeletedContent(); const endorsedCommentsIds = useMemo(() => ( [...filterPosts(comments, 'endorsed')].map(comment => comment.id) @@ -72,11 +63,10 @@ export function usePostComments(threadType) { threadType, page: currentPage + 1, reverseOrder, - showDeleted, }; await dispatch(fetchThreadComments(postId, params)); trackLoadMoreEvent(postId, params); - }, [currentPage, threadType, postId, reverseOrder, showDeleted]); + }, [currentPage, threadType, postId, reverseOrder]); useEffect(() => { const abortController = new AbortController(); @@ -86,14 +76,13 @@ export function usePostComments(threadType) { page: 1, reverseOrder, enableInContextSidebar, - showDeleted, signal: abortController.signal, })); return () => { abortController.abort(); }; - }, [postId, threadType, reverseOrder, enableInContextSidebar, showDeleted]); + }, [postId, threadType, reverseOrder, enableInContextSidebar]); return { endorsedCommentsIds, diff --git a/src/discussions/post-comments/data/thunks.js b/src/discussions/post-comments/data/thunks.js index 2e18d7272..1650e7e51 100644 --- a/src/discussions/post-comments/data/thunks.js +++ b/src/discussions/post-comments/data/thunks.js @@ -60,11 +60,7 @@ function normaliseComments(data) { commentsInThreads[threadId].push(id); } } - // Normalize editableFields to always be an array - commentsById[id] = { - ...comment, - editableFields: comment.editableFields || [], - }; + commentsById[id] = comment; }, ); return { @@ -83,7 +79,6 @@ export function fetchThreadComments( reverseOrder, threadType, enableInContextSidebar, - showDeleted = false, signal, } = {}, ) { @@ -91,7 +86,7 @@ export function fetchThreadComments( try { dispatch(fetchCommentsRequest()); const data = await getThreadComments(threadId, { - page, reverseOrder, threadType, enableInContextSidebar, showDeleted, signal, + page, reverseOrder, threadType, enableInContextSidebar, signal, }); dispatch(fetchCommentsSuccess({ ...normaliseComments(camelCaseObject(data)), @@ -109,11 +104,11 @@ export function fetchThreadComments( }; } -export function fetchCommentResponses(commentId, { page = 1, reverseOrder = true, showDeleted = false } = {}) { +export function fetchCommentResponses(commentId, { page = 1, reverseOrder = true } = {}) { return async (dispatch) => { try { dispatch(fetchCommentResponsesRequest({ commentId })); - const data = await getCommentResponses(commentId, { page, reverseOrder, showDeleted }); + const data = await getCommentResponses(commentId, { page, reverseOrder }); dispatch(fetchCommentResponsesSuccess({ ...normaliseComments(camelCaseObject(data)), page, @@ -190,16 +185,3 @@ export function removeComment(commentId, threadId) { } }; } - -export function performRestoreComment(commentId, courseId) { - return async () => { - try { - const { restoreComment } = await import('./api'); - await restoreComment(commentId, courseId); - return { success: true }; - } catch (error) { - logError(error); - return { success: false, error: error.message }; - } - }; -} diff --git a/src/discussions/post-comments/messages.js b/src/discussions/post-comments/messages.js index 1dd6a9cb2..2b9a85422 100644 --- a/src/discussions/post-comments/messages.js +++ b/src/discussions/post-comments/messages.js @@ -11,20 +11,6 @@ const messages = defineMessages({ defaultMessage: 'Add a response', description: 'Button to add a response to a response', }, - deletedBy: { - id: 'discussions.comments.comment.deletedBy', - defaultMessage: 'Deleted by', - }, - deletedResponse: { - id: 'discussions.comments.comment.deletedResponse', - defaultMessage: 'Deleted Response', - description: 'Badge showing that the response has been deleted', - }, - deletedComment: { - id: 'discussions.comments.comment.deletedComment', - defaultMessage: 'Deleted Comment', - description: 'Badge showing that the comment has been deleted', - }, abuseFlaggedMessage: { id: 'discussions.comments.comment.abuseFlaggedMessage', defaultMessage: 'Content reported for staff to review', @@ -152,16 +138,6 @@ const messages = defineMessages({ defaultMessage: 'Are you sure you want to permanently delete this response?', description: 'Text displayed in confirmation dialog when deleting a response', }, - undeleteResponseTitle: { - id: 'discussions.editor.undelete.response.title', - defaultMessage: 'Restore response', - description: 'Title of confirmation dialog shown when restoring a response', - }, - undeleteResponseDescription: { - id: 'discussions.editor.undelete.response.description', - defaultMessage: 'Are you sure you want to restore this response?', - description: 'Text displayed in confirmation dialog when restoring a response', - }, deleteCommentTitle: { id: 'discussions.editor.delete.comment.title', defaultMessage: 'Delete comment', @@ -172,16 +148,6 @@ const messages = defineMessages({ defaultMessage: 'Are you sure you want to permanently delete this comment?', description: 'Text displayed in confirmation dialog when deleting a comment', }, - undeleteCommentTitle: { - id: 'discussions.editor.undelete.comment.title', - defaultMessage: 'Restore comment', - description: 'Title of confirmation dialog shown when restoring a comment', - }, - undeleteCommentDescription: { - id: 'discussions.editor.undelete.comment.description', - defaultMessage: 'Are you sure you want to restore this comment?', - description: 'Text displayed in confirmation dialog when restoring a comment', - }, deleteConfirmationDelete: { id: 'discussions.delete.confirmation.button.delete', defaultMessage: 'Delete', diff --git a/src/discussions/posts/NoResults.jsx b/src/discussions/posts/NoResults.jsx index 4c6484921..73654d0dd 100644 --- a/src/discussions/posts/NoResults.jsx +++ b/src/discussions/posts/NoResults.jsx @@ -17,7 +17,7 @@ const NoResults = () => { const filters = useSelector((state) => state.threads.filters); const learnersFilter = useSelector(({ learners }) => learners.usernameSearch); const isFiltered = postsFiltered || (topicsFilter !== '') - || (learnersFilter) || (inContextTopicsFilter !== ''); + || (learnersFilter !== null) || (inContextTopicsFilter !== ''); let helpMessage = messages.removeFilters; diff --git a/src/discussions/posts/PostsView.test.jsx b/src/discussions/posts/PostsView.test.jsx index 6e754367e..8e5f01083 100644 --- a/src/discussions/posts/PostsView.test.jsx +++ b/src/discussions/posts/PostsView.test.jsx @@ -215,7 +215,7 @@ describe('PostsView', () => { await renderComponent(); }); dropDownButton = screen.getByRole('button', { - name: /all active posts sorted by recent activity/i, + name: /all posts sorted by recent activity/i, }); await act(async () => { fireEvent.click(dropDownButton); @@ -236,7 +236,7 @@ describe('PostsView', () => { }); dropDownButton = screen.getByRole('button', { - name: /All active posts in Cohort 1 sorted by recent activity/i, + name: /All posts in Cohort 1 sorted by recent activity/i, }); expect(dropDownButton).toBeInTheDocument(); diff --git a/src/discussions/posts/data/__factories__/threads.factory.js b/src/discussions/posts/data/__factories__/threads.factory.js index 67286ff4f..b072860c8 100644 --- a/src/discussions/posts/data/__factories__/threads.factory.js +++ b/src/discussions/posts/data/__factories__/threads.factory.js @@ -45,7 +45,6 @@ Factory.define('thread') non_endorsed_comment_list_url: null, read: false, has_endorsed: false, - is_deleted: false, }); Factory.define('threadsResult') diff --git a/src/discussions/posts/data/api.js b/src/discussions/posts/data/api.js index 57e0f0096..e91044bc1 100644 --- a/src/discussions/posts/data/api.js +++ b/src/discussions/posts/data/api.js @@ -40,7 +40,6 @@ export const getThreads = async (courseId, { threadType, countFlagged, cohort, - isDeleted, } = {}) => { const params = snakeCaseObject({ courseId, @@ -57,7 +56,6 @@ export const getThreads = async (courseId, { flagged, countFlagged, groupId: cohort, - isDeleted, }); const { data } = await getAuthenticatedHttpClient().get(getThreadsApiUrl(), { params }); return data; @@ -216,19 +214,3 @@ export const sendEmailForAccountActivation = async () => { .post(url); return data; }; - -/** - * Restore a deleted thread. - * @param {string} threadId - * @param {string} courseId - * @returns {Promise<{}>} - */ -export const restoreThread = async (threadId, courseId) => { - const url = `${getConfig().LMS_BASE_URL}/api/discussion/v1/restore_content`; - const { data } = await getAuthenticatedHttpClient().post(url, { - content_type: 'thread', - content_id: threadId, - course_id: courseId, - }); - return data; -}; diff --git a/src/discussions/posts/data/selectors.js b/src/discussions/posts/data/selectors.js index dfddf8ab1..b825f93a6 100644 --- a/src/discussions/posts/data/selectors.js +++ b/src/discussions/posts/data/selectors.js @@ -61,5 +61,3 @@ export const selectThreadNextPage = () => state => state.threads.nextPage; export const selectAuthorAvatar = author => state => ( state.threads.avatars?.[camelCase(author)]?.profile.image ); - -export const selectIsDeletedView = () => state => state.threads.isDeletedView; diff --git a/src/discussions/posts/data/slices.js b/src/discussions/posts/data/slices.js index 710e04a63..d17b8a8d3 100644 --- a/src/discussions/posts/data/slices.js +++ b/src/discussions/posts/data/slices.js @@ -46,7 +46,7 @@ const threadsSlice = createSlice({ textSearchRewrite: null, postStatus: RequestStatus.SUCCESSFUL, filters: { - status: PostsStatusFilter.ACTIVE, + status: PostsStatusFilter.ALL, postType: ThreadType.ALL, cohort: '', search: '', @@ -55,7 +55,6 @@ const threadsSlice = createSlice({ redirectToThread: null, sortedBy: ThreadOrdering.BY_LAST_ACTIVITY, confirmEmailStatus: RequestStatus.IDLE, - isDeletedView: false, }, reducers: { fetchLearnerThreadsRequest: (state, { payload }) => ( @@ -400,20 +399,6 @@ const threadsSlice = createSlice({ confirmEmailStatus: RequestStatus.DENIED, } ), - toggleDeletedView: (state) => ( - { - ...state, - isDeletedView: !state.isDeletedView, - pages: [], // Clear pages when switching views - } - ), - setDeletedView: (state, { payload }) => ( - { - ...state, - isDeletedView: payload, - pages: [], // Clear pages when switching views - } - ), }, }); @@ -456,8 +441,6 @@ export const { sendAccountActivationEmailFailed, sendAccountActivationEmailRequest, sendAccountActivationEmailSuccess, - toggleDeletedView, - setDeletedView, } = threadsSlice.actions; export const threadsReducer = threadsSlice.reducer; diff --git a/src/discussions/posts/data/thunks.js b/src/discussions/posts/data/thunks.js index 68ab55139..d905502b2 100644 --- a/src/discussions/posts/data/thunks.js +++ b/src/discussions/posts/data/thunks.js @@ -81,11 +81,7 @@ export function normaliseThreads(data, topicIds = null) { if (!threadsInTopic[topicId].includes(id)) { threadsInTopic[topicId].push(id); } - // Normalize editableFields to always be an array - threadsById[id] = { - ...thread, - editableFields: thread.editableFields || [], - }; + threadsById[id] = thread; avatars = { ...avatars, ...thread.users }; }, ); @@ -145,12 +141,6 @@ export function fetchThreads(courseId, { if (filters.cohort) { options.cohort = filters.cohort; } - if (filters.status === PostsStatusFilter.ACTIVE) { - options.isDeleted = false; - } - if (filters.status === PostsStatusFilter.DELETED) { - options.isDeleted = true; - } return async (dispatch) => { try { dispatch(fetchThreadsRequest({ courseId })); @@ -324,21 +314,6 @@ export function removeThread(threadId) { }; } -export function performRestoreThread(threadId, courseId) { - return async (dispatch) => { - try { - const { restoreThread } = await import('./api'); - const data = await restoreThread(threadId, courseId); - // Update the thread in Redux state to reflect the restore - dispatch(updateThreadSuccess(camelCaseObject(data))); - return { success: true }; - } catch (error) { - logError(error); - return { success: false, error: error.message }; - } - }; -} - export function sendAccountActivationEmail() { return async (dispatch) => { try { diff --git a/src/discussions/posts/index.js b/src/discussions/posts/index.js index acda620ac..f7d3c75dc 100644 --- a/src/discussions/posts/index.js +++ b/src/discussions/posts/index.js @@ -1,5 +1,4 @@ export { showPostEditor } from './data'; export { default as Post } from './post/Post'; -export { default as PostLink } from './post/PostLink'; export { default as messages } from './post-actions-bar/messages'; export { default as PostsView } from './PostsView'; diff --git a/src/discussions/posts/post-filter-bar/messages.js b/src/discussions/posts/post-filter-bar/messages.js index abad87a1b..eed8c084a 100644 --- a/src/discussions/posts/post-filter-bar/messages.js +++ b/src/discussions/posts/post-filter-bar/messages.js @@ -51,16 +51,6 @@ const messages = defineMessages({ defaultMessage: 'Not responded', description: 'Option in dropdown to filter to unresponded posts', }, - filterActive: { - id: 'discussions.posts.status.filter.active', - defaultMessage: 'Active content', - description: 'Option in dropdown to filter to active (non-deleted) posts', - }, - filterDeleted: { - id: 'discussions.posts.status.filter.deleted', - defaultMessage: 'Deleted content', - description: 'Option in dropdown to filter to deleted posts', - }, myPosts: { id: 'discussions.posts.filter.myPosts', defaultMessage: 'My posts', @@ -109,8 +99,6 @@ const messages = defineMessages({ statusReported {reported} statusUnanswered {unanswered} statusUnresponded {unresponded} - statusActive {active} - statusDeleted {deleted} other {{status}} } {type, select, discussion {discussions} diff --git a/src/discussions/posts/post/Post.jsx b/src/discussions/posts/post/Post.jsx index 702533a9c..965a81c67 100644 --- a/src/discussions/posts/post/Post.jsx +++ b/src/discussions/posts/post/Post.jsx @@ -2,7 +2,6 @@ import React, { useCallback, useContext, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Hyperlink, useToggle } from '@openedx/paragon'; -import { DeleteOutline } from '@openedx/paragon/icons'; import classNames from 'classnames'; import { toString } from 'lodash'; import { useDispatch, useSelector } from 'react-redux'; @@ -10,14 +9,11 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { logError } from '@edx/frontend-platform/logging'; import HTMLLoader from '../../../components/HTMLLoader'; -import { AvatarOutlineAndLabelColors, ContentActions, getFullUrl } from '../../../data/constants'; +import { ContentActions, getFullUrl } from '../../../data/constants'; import { selectorForUnitSubsection, selectTopicContext } from '../../../data/selectors'; -import { - AlertBanner, AuthorLabel, AutoSpamAlertBanner, Confirmation, -} from '../../common'; +import { AlertBanner, AutoSpamAlertBanner, Confirmation } from '../../common'; import DiscussionContext from '../../common/context'; import HoverCard from '../../common/HoverCard'; import withPostingRestrictions from '../../common/withPostingRestrictions'; @@ -33,24 +29,22 @@ import PostFooter from './PostFooter'; import PostHeader from './PostHeader'; const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => { - const { enableInContextSidebar, postId, courseId } = useContext(DiscussionContext); - + const { enableInContextSidebar, postId } = useContext(DiscussionContext); const threadData = useSelector(selectThread(postId)); const { topicId, abuseFlagged, closed, pinned, voted, hasEndorsed, following, closedBy, voteCount, groupId, groupName, closeReason, authorLabel, type: postType, author, title, createdAt, renderedBody, lastEdit, editByLabel, - closedByLabel, users: postUsers, isDeleted, deletedBy, deletedByLabel, is_spam: isSpam, + closedByLabel, users: postUsers, is_spam: isSpam, } = threadData; - const intl = useIntl(); const location = useLocation(); const navigate = useNavigate(); const dispatch = useDispatch(); + const { courseId } = useContext(DiscussionContext); const topic = useSelector(selectTopic(topicId)); const getTopicSubsection = useSelector(selectorForUnitSubsection); const topicContext = useSelector(selectTopicContext(topicId)); const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false); - const [isRestoring, showRestoreConfirmation, hideRestoreConfirmation] = useToggle(false); const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false); const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); @@ -109,35 +103,15 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => { } }, [abuseFlagged, postId, showReportConfirmation]); - const handleRestore = useCallback(() => { - showRestoreConfirmation(); - }, [showRestoreConfirmation]); - - const handleRestoreConfirmation = useCallback(async () => { - try { - const { performRestoreThread } = await import('../data/thunks'); - const result = await dispatch(performRestoreThread(postId, courseId)); - // Check if restore failed and log the error - if (result && !result.success) { - logError(`Failed to restore thread: ${result.error || 'Unknown error'}`); - } - } catch (error) { - logError(error); - } - hideRestoreConfirmation(); - }, [postId, courseId, dispatch, hideRestoreConfirmation]); - const actionHandlers = useMemo(() => ({ [ContentActions.EDIT_CONTENT]: handlePostContentEdit, [ContentActions.DELETE]: showDeleteConfirmation, - [ContentActions.RESTORE]: handleRestore, [ContentActions.CLOSE]: handlePostClose, [ContentActions.COPY_LINK]: handlePostCopyLink, [ContentActions.PIN]: handlePostPin, [ContentActions.REPORT]: handlePostReport, }), [ handlePostClose, handlePostContentEdit, handlePostCopyLink, handlePostPin, handlePostReport, showDeleteConfirmation, - handleRestore, ]); const handleClosePostConfirmation = useCallback((closeReasonCode) => { @@ -173,14 +147,6 @@ const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => { closeButtonVariant="tertiary" confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)} /> - {!abuseFlagged && ( { onFollow={handlePostFollow} voted={voted} following={following} - isDeleted={isDeleted} /> - {isDeleted && deletedBy && ( -
- -
- {intl.formatMessage(messages.deletedBy)} - - - -
-
- )} { - if (type === 'response') { - return 'Response'; - } - if (type === 'comment') { - return 'Comment'; - } - return null; - }; - - // For comments/responses, show parent thread title with arrow - const displayTitle = (type === 'response' || type === 'comment') && threadTitle ? threadTitle : title; - - // Strip render_id suffix (e.g., "-thread", "-response", "-comment") for navigation - const stripRenderIdSuffix = (idValue) => { - if (typeof idValue === 'string') { - return idValue.replace(/-(thread|response|comment)$/, ''); - } - return idValue; - }; - - // For comments/responses, navigate to the parent thread instead of the comment itself - const rawNavigationId = (type === 'response' || type === 'comment') && commentThreadId ? commentThreadId : postId; - const navigationPostId = stripRenderIdSuffix(rawNavigationId); - const { pathname } = discussionsPath(Routes.COMMENTS.PAGES[page], { 0: enableInContextSidebar ? 'in-context' : undefined, courseId, topicId, - postId: navigationPostId, + postId, category, learnerUsername, })(); @@ -103,10 +76,8 @@ const PostLink = ({ 'd-flex flex-row pt-2 pb-2 px-4 border-primary-500 position-relative', { 'bg-light-300': isPostRead }, { 'post-summary-card-selected': id === selectedPostId }, - { 'bg-light-200': isDeleted }, // Gray background for deleted threads ) } - style={isDeleted ? { opacity: 0.7 } : {}} // Slightly faded for deleted threads >
- {(type === 'response' || type === 'comment') && threadTitle && ( - <> - - - {getTypeLabel()} in - - - )} - {displayTitle} + {title} {isPostPreviewAvailable(previewBody) ? previewBody : intl.formatMessage(messages.postWithoutPreview)} @@ -162,25 +121,12 @@ const PostLink = ({ {' '}reported )} - {isDeleted && ( - - {intl.formatMessage(messages.deletedPost)} - {' '}deleted - - )} {pinned && ( )} diff --git a/src/discussions/posts/post/messages.js b/src/discussions/posts/post/messages.js index 61c49c87a..f095e1fbe 100644 --- a/src/discussions/posts/post/messages.js +++ b/src/discussions/posts/post/messages.js @@ -24,25 +24,6 @@ const messages = defineMessages({ defaultMessage: 'Reported', description: 'Content reported for staff review', }, - deletedBy: { - id: 'discussions.post.deletedBy', - defaultMessage: 'Deleted by', - }, - deletedPost: { - id: 'discussions.post.deletedPost', - defaultMessage: 'Deleted', - description: 'Badge showing that the post has been deleted', - }, - deletedResponse: { - id: 'discussions.post.deletedResponse', - defaultMessage: 'Deleted', - description: 'Badge showing that the response has been deleted', - }, - deletedComment: { - id: 'discussions.post.deletedComment', - defaultMessage: 'Deleted', - description: 'Badge showing that the comment has been deleted', - }, following: { id: 'discussions.post.following', defaultMessage: 'Following', @@ -125,14 +106,6 @@ const messages = defineMessages({ defaultMessage: 'Delete', description: 'Delete button shown on delete confirmation dialog', }, - undeletePostTitle: { - id: 'discussions.editor.undelete.post.title', - defaultMessage: 'Restore post', - }, - undeletePostDescription: { - id: 'discussions.editor.undelete.post.description', - defaultMessage: 'Are you sure you want to restore this post?', - }, reportPostTitle: { id: 'discussions.editor.report.post.title', defaultMessage: 'Report inappropriate content?', @@ -198,11 +171,6 @@ const messages = defineMessages({ defaultMessage: 'you are not following this post', description: 'tell screen readers if user is not following a post', }, - deleted: { - id: 'discussions.post.deleted', - defaultMessage: 'Deleted', - description: 'Label shown on deleted threads', - }, }); export default messages; diff --git a/src/discussions/utils.js b/src/discussions/utils.js index 461a55021..fb139f3c6 100644 --- a/src/discussions/utils.js +++ b/src/discussions/utils.js @@ -14,7 +14,6 @@ import { import { getConfig } from '@edx/frontend-platform'; -import { ReactComponent as RestoreFromTrash } from '../assets/undelete.svg'; import { DENIED, LOADED } from '../components/NavigationBar/data/slice'; import { ContentActions, Routes, ThreadType, @@ -64,9 +63,9 @@ export function checkPermissions(content, action) { if (content.editableFields.includes(action)) { return true; } - // Both delete and restore actions check `content.canDelete` - if (action === ContentActions.DELETE || action === ContentActions.RESTORE) { - return content.canDelete; + // For delete action we check `content.canDelete` + if (action === ContentActions.DELETE) { + return true; } return false; } @@ -183,14 +182,7 @@ export const ACTIONS_LIST = [ action: ContentActions.DELETE, icon: Delete, label: messages.deleteAction, - conditions: { canDelete: true, isDeleted: false }, - }, - { - id: 'restore', - action: ContentActions.RESTORE, - icon: RestoreFromTrash, - label: messages.restoreAction, - conditions: { canDelete: true, isDeleted: true }, + conditions: { canDelete: true }, }, ]; @@ -206,20 +198,12 @@ export function useActions(contentType, id) { : true ), []); - const isActionDisabled = useCallback((actionId, isDeleted) => ( - // For deleted items, disable all actions except 'copy-link' and 'restore' - isDeleted && actionId !== 'copy-link' && actionId !== 'restore' - ), []); - const actions = useMemo(() => ACTIONS_LIST.filter( ({ action, conditions = null, }) => checkPermissions(content, action) && checkConditions(content, conditions), - ).map(action => ({ - ...action, - disabled: isActionDisabled(action.id, content.isDeleted), - })), [content, checkConditions, isActionDisabled]); + ), [content]); return actions; } diff --git a/src/index.scss b/src/index.scss index b186c72b6..fdff64ff1 100755 --- a/src/index.scss +++ b/src/index.scss @@ -28,19 +28,6 @@ body, background-color: var(--pgn-color-card-bg-base) !important; } -// New learner message styling -.new-learner-message { - font-style: italic; - font-size: 12px; - margin-top: 0.25rem; - line-height: 1.2; - - @media (max-width: 767.98px) { - font-size: 11px; - margin-top: 0.1rem; - } -} - #post, #comment, #reply, @@ -69,22 +56,6 @@ body, outline: var(--pgn-color-success-700) solid 2px; } -.text-learner-color { - color: var(--pgn-color-primary-500); -} - -.outline-learner-color { - outline: var(--pgn-color-primary-500) solid 2px; -} - -.text-new-learner-color { - color: var(--pgn-color-accent-500); -} - -.outline-new-learner-color { - outline: var(--pgn-color-accent-500) solid 2px; -} - .outline-anonymous { outline: var(--pgn-color-light-400) solid 2px; } @@ -587,37 +558,9 @@ code { .actions-dropdown-item { padding: 12px 16px; height: 48px !important; - min-width: 195px !important; - border: none !important; - outline: none !important; -} - -.actions-dropdown-item:hover, -.actions-dropdown-item:focus, -.actions-dropdown-item:active { - border: none !important; - outline: none !important; - box-shadow: none !important; -} - -.learner-submenu-container { - min-width: 280px; - max-width: 320px; - z-index: 1051; - border: 1px solid var(--pgn-color-light-400) !important; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important; - outline: none !important; -} - -.learner-submenu-container .actions-dropdown-item { - min-width: 280px !important; - max-width: 320px !important; - white-space: normal; - text-align: left; - border: none !important; + min-width: 195px !important } - .font-xl { font-size: 18px !important; line-height: 28px !important; @@ -892,29 +835,3 @@ th, td { } } } - -// Learner submenu styling -.learner-submenu-container { - position: absolute; - left: 100%; - top: 0; - min-width: 300px; - max-width: 360px; - z-index: 9999; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - border: 1px solid var(--pgn-color-light-400); - overflow: visible; -} - -// Deleted content icon styling -.deleted-content-icon { - width: 1.5rem; - height: 1.5rem; -} - -// Subdirectory arrow icon styling -.subdirectory-arrow-icon { - width: 16px; - height: 16px; - margin-right: 4px; -}