-
Notifications
You must be signed in to change notification settings - Fork 0
feat: added soft delete functionality #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
c7fb8c8 to
3af0d15
Compare
1d1ac54 to
8711640
Compare
d997095 to
fd9406c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements comprehensive soft delete functionality for discussion threads, responses, and comments, allowing content to be marked as deleted rather than permanently removed from the database. The implementation includes UI indicators for deleted content, restore capabilities, and bulk operations for managing user contributions.
Key Changes:
- Added soft delete and restore actions with confirmation dialogs for threads, responses, and comments
- Implemented filtering between active and deleted content with dedicated views for moderators/staff
- Added bulk delete and restore operations for user posts at course and organization levels
Reviewed changes
Copilot reviewed 47 out of 48 changed files in this pull request and generated 26 comments.
Show a summary per file
| File | Description |
|---|---|
src/index.scss |
Added styling for deleted content indicators, learner message badges, and submenu containers |
src/discussions/utils.js |
Extended action permissions to support restore operations and disabled actions for deleted items |
src/discussions/posts/post/messages.js |
Added internationalized messages for delete/restore confirmations and deleted content badges |
src/discussions/posts/post/PostLink.jsx |
Enhanced post links to display deleted state with visual indicators and handle response/comment navigation |
src/discussions/posts/post/Post.jsx |
Integrated restore confirmation dialogs and deleted-by attribution display |
src/discussions/posts/post-filter-bar/messages.js |
Added filter labels for active vs deleted content status |
src/discussions/posts/index.js |
Exported PostLink component for reuse in learner views |
src/discussions/posts/data/thunks.js |
Implemented thread restore thunk and filter logic for active/deleted content |
src/discussions/posts/data/slices.js |
Changed default filter to active content and added deleted view state management |
src/discussions/posts/data/selectors.js |
Added selector for deleted view state |
src/discussions/posts/data/api.js |
Created API endpoint for restoring deleted threads |
src/discussions/posts/data/__factories__/threads.factory.js |
Added is_deleted field to thread factory for testing |
src/discussions/posts/PostsView.test.jsx |
Updated tests to reflect new default "active" filter |
src/discussions/posts/NoResults.jsx |
Fixed optional chaining for learner state access |
src/discussions/post-comments/messages.js |
Added restore confirmation messages for responses and comments |
src/discussions/post-comments/data/thunks.js |
Extended fetch operations to support showDeleted parameter and added restore thunk |
src/discussions/post-comments/data/hooks.js |
Implemented deleted content visibility logic based on filter state |
src/discussions/post-comments/data/api.js |
Added showDeleted parameter to API calls and restore comment endpoint |
src/discussions/post-comments/data/__factories__/comments.factory.js |
Added is_deleted field to comment factory |
src/discussions/post-comments/comments/comment/Reply.jsx |
Integrated restore functionality and deleted-by attribution for replies |
src/discussions/post-comments/comments/comment/CommentHeader.jsx |
Added flex-wrap to header for better responsive layout |
src/discussions/post-comments/comments/comment/Comment.jsx |
Implemented restore logic and deleted state handling for comments and responses |
src/discussions/post-comments/PostCommentsView.test.jsx |
Updated test expectations for show_deleted parameter |
src/discussions/messages.js |
Added restore action message and bulk operation labels |
src/discussions/learners/utils.js |
Created learner actions menu with nested delete/restore submenus |
src/discussions/learners/messages.js |
Added messages for deleted activity stats and restore operations |
src/discussions/learners/learner/proptypes.js |
Extended learner shape with deleted content counts |
src/discussions/learners/learner/LearnerFooter.jsx |
Added deleted activity indicator for staff/moderators |
src/discussions/learners/learner/LearnerFilterBar.jsx |
Added deleted activity sorting option |
src/discussions/learners/learner/LearnerCard.jsx |
Passed deleted stats to footer component |
src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.test.jsx |
Updated tests for new content status filter radiogroup |
src/discussions/learners/learner-post-filter-bar/LearnerPostFilterBar.jsx |
Added content status filter for active/deleted distinction |
src/discussions/learners/data/thunks.js |
Implemented deleted content fetching and restore operations |
src/discussions/learners/data/slices.js |
Added restore action reducers and merged filter updates |
src/discussions/learners/data/redux.test.jsx |
Added test assertion for default contentStatus |
src/discussions/learners/data/api.js |
Created APIs for bulk restore and fetching deleted content |
src/discussions/learners/LearnerPostsView.test.jsx |
Updated tests to handle submenu interactions |
src/discussions/learners/LearnerPostsView.jsx |
Integrated restore confirmations and post refresh logic |
src/discussions/learners/LearnerActionsDropdown.test.jsx |
Added submenu hover interactions to tests |
src/discussions/learners/LearnerActionsDropdown.jsx |
Implemented nested submenu with portal rendering for delete/restore actions |
src/discussions/data/thunks.js |
Added performRestoreThread thunk |
src/discussions/data/selectors.js |
Updated filter logic to treat ACTIVE status as unfiltered |
src/discussions/data/constants.js |
Added THREAD_FILTER_TYPES constants |
src/discussions/common/HoverCard.jsx |
Disabled interactive actions for deleted content |
src/discussions/common/ActionsDropdown.jsx |
Added disabled state handling for action items |
src/data/constants.js |
Added RESTORE action and ACTIVE/DELETED status filter constants |
src/components/FilterBar.jsx |
Enhanced filter bar to support separated filter sections |
src/assets/undelete.svg |
Added restore/undelete icon SVG |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| import FilterBar from '../../../components/FilterBar'; | ||
| import { PostsStatusFilter, ThreadType } from '../../../data/constants'; | ||
| // import { PostsStatusFilter, ThreadType } from '../../../data/constants'; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The commented-out import statement should be removed rather than commented. If these imports are needed, they should be active; if not, remove them entirely. Commented code creates confusion and clutter in the codebase.
| // import { PostsStatusFilter, ThreadType } from '../../../data/constants'; |
| 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; | ||
| } |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The missing semicolon on line 590 has been added, but the surrounding code shows inconsistent patterns for button states. The actions-dropdown-item has border and outline rules defined multiple times with different specificity. Consider consolidating these styles to avoid potential conflicts.
| ).map(action => ({ | ||
| ...action, | ||
| // For deleted items, disable all actions except 'copy-link' and 'restore' | ||
| disabled: content.isDeleted && action.id !== 'copy-link' && action.id !== 'restore', | ||
| })), [content]); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The action filtering logic now adds a disabled property based on isDeleted state. However, this mutates the action objects during rendering which could cause issues with memoization. Consider moving this logic into the checkPermissions or checkConditions functions instead, or creating new action objects rather than spreading and modifying existing ones.
| if (result.success) { | ||
| window.location.reload(); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After restoring a thread, the code calls window.location.reload() which causes a full page refresh. This is not ideal for user experience as it loses the current state and causes a jarring transition. Consider using Redux state updates to refresh just the thread data instead of reloading the entire page.
| const learnersFilter = useSelector(({ learners }) => learners?.usernameSearch); | ||
| const isFiltered = postsFiltered || (topicsFilter !== '') | ||
| || (learnersFilter !== null) || (inContextTopicsFilter !== ''); | ||
| || (learnersFilter) || (inContextTopicsFilter !== ''); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The optional chaining learners?.usernameSearch and learners?.postFilter suggests that the learners state might be undefined in some cases. However, this defensive programming is inconsistent - other parts of the codebase access state.learners directly without null checks. Consider either making the learners reducer always defined or consistently use optional chaining throughout.
| 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; | ||
| }; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The useShowDeletedContent hook checks postFilter?.contentStatus === PostsStatusFilter.DELETED but only when in learner view. This logic is duplicated in multiple places (lines 45-51, 85-89). Consider extracting this to a shared selector or utility function to maintain consistency and avoid code duplication.
| const { getDeletedContent } = await import('./api'); | ||
| data = await getDeletedContent(courseId, { | ||
| author, | ||
| page, | ||
| pageSize: 10, | ||
| }); | ||
| } else { | ||
| // Use regular learner posts endpoint for active content | ||
| const { getUserPosts } = await import('./api'); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code dynamically imports API functions inside the thunk (const { getDeletedContent } = await import('./api')). While lazy loading can help with bundle size, this creates inconsistency since other API calls in the same file use static imports. Either use dynamic imports consistently or use static imports for all APIs in this file to avoid confusion and ensure predictable loading behavior.
| contentType, | ||
| } = {}) { | ||
| const params = snakeCaseObject({ | ||
| authorId: author, // The backend expects author_id |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getDeletedContent function uses authorId in the params but the JSDoc says @param {string} author. The parameter name should match the documentation. Either change the JSDoc to authorId or change the API parameter to use author consistently with other functions.
| authorId: author, // The backend expects author_id | |
| author, // The backend expects author |
94b4ad6 to
f296e1e
Compare
5adc134 to
2f2eff9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 47 out of 48 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const handleRestoreConfirmation = useCallback(async () => { | ||
| try { | ||
| const { performRestoreThread } = await import('../data/thunks'); | ||
| const result = await dispatch(performRestoreThread(postId, courseId)); | ||
| if (result.success) { | ||
| await dispatch(fetchThread(postId, courseId)); | ||
| } | ||
| } catch (error) { | ||
| logError(error); | ||
| } | ||
| hideRestoreConfirmation(); | ||
| }, [postId, courseId, dispatch, hideRestoreConfirmation]); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent return type handling. The function performRestoreThread returns an object with success and optionally error properties, but the calling code in Post.jsx only checks result.success. If the restore fails and returns success: false, there's no user feedback about the error. Consider logging the error or displaying it to the user.
|
|
||
| import FilterBar from '../../../components/FilterBar'; | ||
| import { PostsStatusFilter, ThreadType } from '../../../data/constants'; | ||
| // import { PostsStatusFilter, ThreadType } from '../../../data/constants'; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented out import statement should be removed. The commented import is replaced by the new imports above it, so this line should be deleted to keep the code clean.
| // import { PostsStatusFilter, ThreadType } from '../../../data/constants'; |
| }, | ||
| { | ||
| name: 'status', | ||
| name: 'status', // secondary status |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline comment 'secondary status' is potentially misleading. This comment suggests there's a distinction between primary and secondary status filters, but this isn't clearly documented elsewhere. Consider either adding more comprehensive documentation or removing the comment if it doesn't add clarity.
| if (filters.contentStatus === PostsStatusFilter.DELETED) { | ||
| const { getDeletedContent } = await import('./api'); | ||
| data = await getDeletedContent(courseId, { | ||
| author, | ||
| page, | ||
| pageSize: 10, | ||
| }); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for the dynamic import. If the import of 'getDeletedContent' fails, it will throw an unhandled error. Consider wrapping the import in a try-catch block or handling the error appropriately.
| }; | ||
|
|
||
| // For comments/responses, show parent thread title with arrow | ||
| const displayTitle = (type === 'response' || type === 'comment') && threadTitle ? threadTitle : title; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ternary operation uses threadTitle and title which may both be falsy. If both are undefined or null, displayTitle could be an empty value. Consider adding a fallback or validation to ensure a valid title is always displayed.
| const displayTitle = (type === 'response' || type === 'comment') && threadTitle ? threadTitle : title; | ||
|
|
||
| // For comments/responses, navigate to the parent thread instead of the comment itself | ||
| const navigationPostId = (type === 'response' || type === 'comment') && commentThreadId ? commentThreadId : postId; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The navigationPostId calculation could result in undefined if both commentThreadId and postId are falsy. This could lead to navigation errors or broken links. Add validation to ensure a valid post ID is always used.
| } from '../../../../data/constants'; | ||
| import { | ||
| AlertBanner, AuthorLabel, AutoSpamAlertBanner, Confirmation, EndorsedAlertBanner, | ||
| } from '../../../common'; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate import statement. Line 24 imports from '../../../data/constants' but lines 19-20 already import from the same module. These should be combined into a single import statement to avoid duplication.
| } from '../../../common'; |
2f2eff9 to
94b4ad6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 46 out of 47 changed files in this pull request and generated 16 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {isOpen && ReactDOM.createPortal( | ||
| <ModalPopup | ||
| onClose={onCloseModal} | ||
| positionRef={target} | ||
| isOpen={isOpen} | ||
| placement="bottom-start" | ||
| style={{ zIndex: 9998 }} | ||
| > | ||
| {actions.map(action => ( | ||
| <React.Fragment key={action.id}> | ||
| <Dropdown.Item | ||
| as={Button} | ||
| variant="tertiary" | ||
| size="inline" | ||
| onClick={() => { | ||
| close(); | ||
| handleActions(action.action); | ||
| }} | ||
| className="d-flex justify-content-start actions-dropdown-item" | ||
| data-testId={action.id} | ||
| <div | ||
| className="bg-white shadow d-flex flex-column mt-1" | ||
| data-testid="learner-actions-dropdown-modal-popup" | ||
| style={{ position: 'relative', zIndex: 9998 }} | ||
| > | ||
| {menuItems.map(item => ( | ||
| <div | ||
| key={item.id} | ||
| className="position-relative" | ||
| onMouseEnter={() => setActiveSubmenu(item.id)} | ||
| onMouseLeave={() => setActiveSubmenu(null)} | ||
| style={{ zIndex: 2 }} | ||
| > | ||
| <Icon | ||
| src={action.icon} | ||
| className="icon-size-24" | ||
| /> | ||
| <span className="font-weight-normal ml-2"> | ||
| {action.label.defaultMessage} | ||
| </span> | ||
| </Dropdown.Item> | ||
| </React.Fragment> | ||
| ))} | ||
| </div> | ||
| </ModalPopup> | ||
| <Dropdown.Item | ||
| as={Button} | ||
| variant="tertiary" | ||
| size="inline" | ||
| className="d-flex justify-content-between align-items-center actions-dropdown-item" | ||
| data-testid={item.id} | ||
| > | ||
| <div className="d-flex align-items-center"> | ||
| <span className="font-weight-normal"> | ||
| {item.label} | ||
| </span> | ||
| </div> | ||
| <Icon | ||
| src={ChevronRight} | ||
| className="icon-size-16" | ||
| /> | ||
| </Dropdown.Item> | ||
| {activeSubmenu === item.id && ( | ||
| <div | ||
| className="bg-white learner-submenu-container" | ||
| style={{ | ||
| position: 'absolute', | ||
| left: '100%', | ||
| top: 0, | ||
| minWidth: 300, | ||
| maxWidth: 360, | ||
| zIndex: 9999, | ||
| boxShadow: '0 2px 8px rgba(0,0,0,0.15)', | ||
| border: '1px solid var(--pgn-color-light-400)', | ||
| overflow: 'visible', | ||
| }} | ||
| > | ||
| {item.submenu.map(subItem => ( | ||
| <Dropdown.Item | ||
| key={subItem.id} | ||
| as={Button} | ||
| variant="tertiary" | ||
| size="inline" | ||
| onClick={() => handleActions(subItem.action)} | ||
| className="d-flex justify-content-start actions-dropdown-item" | ||
| data-testid={subItem.id} | ||
| > | ||
| <span className="font-weight-normal"> | ||
| {subItem.label} | ||
| </span> | ||
| </Dropdown.Item> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </ModalPopup>, | ||
| document.body, | ||
| )} |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ReactDOM.createPortal is used to render the modal popup into document.body, but the portal is only created when isOpen is true (line 69). When isOpen changes to false, React will automatically unmount the portal, but there's no cleanup in case the component unmounts while the portal is open. The useEffect cleanup on lines 49-55 attempts this, but has a dependency issue (see separate comment).
| onClick={() => !isDeleted && handleResponseCommentButton()} | ||
| disabled={isClosed || isDeleted} | ||
| style={{ lineHeight: '20px', ...(isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}) }} |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The onClick handler on line 54 checks !isDeleted before calling the function, but the button is also disabled when isDeleted is true (line 55). This creates redundant protection. Either the disabled state is sufficient, or if you need the onClick check for some reason, document why both are necessary.
| */ | ||
| export function checkPermissions(content, action) { | ||
| if (content.editableFields.includes(action)) { | ||
| if (content.editableFields && content.editableFields.includes(action)) { |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defensive check added for editableFields is good, but this suggests content.editableFields might be undefined or null. Consider whether this is expected behavior or if the API response should be normalized to always include editableFields as an empty array when not present.
| { 'bg-light-200': isDeleted }, // Gray background for deleted threads | ||
| ) | ||
| } | ||
| style={isDeleted ? { opacity: 0.7 } : {}} // Slightly faded for deleted threads |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline style opacity: 0.7 uses a magic number. Consider defining this as a CSS class or CSS variable for consistency and maintainability.
| * @returns {Promise<{}>} | ||
| */ | ||
| export const restoreThread = async (threadId, courseId) => { | ||
| const url = `${getConfig().LMS_BASE_URL}/api/discussion/v1/restore_content`; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API function restoreThread uses a hardcoded endpoint '/api/discussion/v1/restore_content'. This endpoint pattern is inconsistent with other endpoints in the file which use helper functions like getThreadsApiUrl(). Consider creating a helper function for consistency.
| } | ||
|
|
||
| if (userHasModerationPrivileges || userIsGroupTa) { | ||
| // Add reported filter only for group TA and moderators |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says 'Add reported filter only for group TA and moderators' but the condition checks for both userHasModerationPrivileges OR userIsGroupTa. This means moderators AND group TAs will see this filter, not just group TAs and moderators as a combined role. The comment should be clarified to match the actual logic.
| contentType, | ||
| } = {}) { | ||
| const params = snakeCaseObject({ | ||
| authorId: author, // The backend expects author_id |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API parameter name has been changed from 'authorId' (with snake_case conversion to 'author_id') on line 153 to use 'authorId' which will be converted to 'author_id' by snakeCaseObject. However, the comment on line 153 says 'The backend expects author_id'. Verify that the backend actually accepts this parameter name, as the getUserPosts function uses 'username' (line 81) for the author parameter.
| useEffect(() => () => { | ||
| if (isOpen) { | ||
| close(); | ||
| setTarget(null); | ||
| setActiveSubmenu(null); | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cleanup effect on lines 49-55 will run on every render because it doesn't have a dependency array. This means it sets up a new cleanup function on every render, which is inefficient. Add an empty dependency array [] to ensure this cleanup only runs on unmount.
| useEffect(() => () => { | |
| if (isOpen) { | |
| close(); | |
| setTarget(null); | |
| setActiveSubmenu(null); | |
| } | |
| useEffect(() => { | |
| return () => { | |
| if (isOpen) { | |
| close(); | |
| setTarget(null); | |
| setActiveSubmenu(null); | |
| } | |
| }; |
| * @returns {Promise<{}>} | ||
| */ | ||
| export const restoreComment = async (commentId, courseId) => { | ||
| const url = `${getConfig().LMS_BASE_URL}/api/discussion/v1/restore_content`; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API function restoreComment uses a hardcoded endpoint '/api/discussion/v1/restore_content'. This is the same endpoint as used in threads (src/discussions/posts/data/api.js line 227). Consider extracting this to a shared constant or helper function to avoid duplication and ensure consistency.
| <span className={classNames( | ||
| 'font-weight-500 text-primary-500 font-style align-bottom mr-1', | ||
| { 'font-weight-bolder': !read }, | ||
| { 'text-decoration-line-through': isDeleted }, // Line-through for deleted threads |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inline comment on line 138 says 'Line-through for deleted threads' but this style is applied to all deleted content (threads, responses, and comments based on the displayTitle logic). The comment should be updated to reflect this broader scope or be more generic.
Description
Implements soft delete functionality for discussion threads, responses, and comments using the
is_deletedflag instead of permanently deleting records.This enables safe deletion and restoration of discussion content while preserving existing data.
Changes Made
JIRA Tickets
Related Pull Requests