Skip to content

Commit f97a15a

Browse files
committed
feat: added soft delete functionality
1 parent 71d4584 commit f97a15a

47 files changed

Lines changed: 1258 additions & 192 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/assets/undelete.svg

Lines changed: 3 additions & 0 deletions
Loading

src/components/FilterBar.jsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ const FilterBar = ({
7575
label: intl.formatMessage(messages.filterUnresponded),
7676
value: PostsStatusFilter.UNRESPONDED,
7777
},
78+
{
79+
id: 'status-active',
80+
label: intl.formatMessage(messages.filterActive),
81+
value: PostsStatusFilter.ACTIVE,
82+
},
83+
{
84+
id: 'status-deleted',
85+
label: intl.formatMessage(messages.filterDeleted),
86+
value: PostsStatusFilter.DELETED,
87+
},
7888
{
7989
id: 'sort-activity',
8090
label: intl.formatMessage(messages.lastActivityAt),
@@ -124,7 +134,7 @@ const FilterBar = ({
124134
<Collapsible.Body className="collapsible-body px-4 pb-3 pt-0">
125135
<Form>
126136
<div className="d-flex flex-row py-2 justify-content-between">
127-
{filters.map((value) => (
137+
{filters.filter(f => !f.hasSeparator).map((value) => (
128138
<Form.RadioSet
129139
key={value.name}
130140
name={value.name}
@@ -150,6 +160,38 @@ const FilterBar = ({
150160
</Form.RadioSet>
151161
))}
152162
</div>
163+
{filters.some(f => f.hasSeparator) && (
164+
<>
165+
<div className="border-bottom my-2" />
166+
<div className="d-flex flex-row py-2 justify-content-between">
167+
{filters.filter(f => f.hasSeparator).map((value) => (
168+
<Form.RadioSet
169+
key={value.name}
170+
name={value.name}
171+
className="d-flex flex-column list-group list-group-flush"
172+
value={selectedFilters[value.name]}
173+
onChange={handleFilterToggle}
174+
>
175+
{value.filters.map(filterName => {
176+
const element = allFilters.find(obj => obj.id === filterName);
177+
if (element) {
178+
return (
179+
<ActionItem
180+
key={element.id}
181+
id={element.id}
182+
label={element.label}
183+
value={element.value}
184+
selected={selectedFilters[value.name]}
185+
/>
186+
);
187+
}
188+
return false;
189+
})}
190+
</Form.RadioSet>
191+
))}
192+
</div>
193+
</>
194+
)}
153195
{showCohortsFilter && (
154196
<>
155197
<div className="border-bottom my-2" />
@@ -199,6 +241,7 @@ FilterBar.propTypes = {
199241
selectedFilters: PropTypes.shape({
200242
postType: ThreadType,
201243
status: PostsStatusFilter,
244+
contentStatus: PostsStatusFilter,
202245
orderBy: ThreadOrdering,
203246
cohort: PropTypes.string,
204247
}).isRequired,

src/data/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const ContentActions = {
5151
COPY_LINK: 'copy_link',
5252
REPORT: 'abuse_flagged',
5353
DELETE: 'delete',
54+
RESTORE: 'restore',
5455
FOLLOWING: 'following',
5556
CHANGE_GROUP: 'group_id',
5657
MARK_READ: 'read',
@@ -60,6 +61,8 @@ export const ContentActions = {
6061
VOTE: 'voted',
6162
DELETE_COURSE_POSTS: 'delete-course-posts',
6263
DELETE_ORG_POSTS: 'delete-org-posts',
64+
RESTORE_COURSE_POSTS: 'restore-course-posts',
65+
RESTORE_ORG_POSTS: 'restore-org-posts',
6366
};
6467

6568
/**
@@ -109,6 +112,8 @@ export const PostsStatusFilter = {
109112
REPORTED: 'statusReported',
110113
UNANSWERED: 'statusUnanswered',
111114
UNRESPONDED: 'statusUnresponded',
115+
ACTIVE: 'statusActive',
116+
DELETED: 'statusDeleted',
112117
};
113118

114119
/**
@@ -132,6 +137,7 @@ export const LearnersOrdering = {
132137
BY_FLAG: 'flagged',
133138
BY_LAST_ACTIVITY: 'activity',
134139
BY_RECENCY: 'recency',
140+
BY_DELETED: 'deleted',
135141
};
136142

137143
/**

src/discussions/common/ActionsDropdown.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,13 @@ const ActionsDropdown = ({
7878
size="inline"
7979
onClick={() => {
8080
close();
81-
handleActions(action.action);
81+
if (!action.disabled) {
82+
handleActions(action.action);
83+
}
8284
}}
8385
className="d-flex justify-content-start actions-dropdown-item"
8486
data-testId={action.id}
87+
disabled={action.disabled}
8588
>
8689
<Icon
8790
src={action.icon}

src/discussions/common/HoverCard.jsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const HoverCard = ({
2929
voted,
3030
following,
3131
endorseIcons,
32+
isDeleted,
3233
}) => {
3334
const intl = useIntl();
3435
const { enableInContextSidebar } = useContext(DiscussionContext);
@@ -50,9 +51,9 @@ const HoverCard = ({
5051
'px-2.5 py-2 border-0 font-style text-gray-700',
5152
{ 'w-100': enableInContextSidebar },
5253
)}
53-
onClick={() => handleResponseCommentButton()}
54-
disabled={isClosed}
55-
style={{ lineHeight: '20px' }}
54+
onClick={handleResponseCommentButton}
55+
disabled={isClosed || isDeleted}
56+
style={{ lineHeight: '20px', ...(isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}) }}
5657
>
5758
{addResponseCommentButtonMessage}
5859
</Button>
@@ -78,6 +79,8 @@ const HoverCard = ({
7879
className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'}
7980
size="sm"
8081
alt="Endorse"
82+
disabled={isDeleted}
83+
style={isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}}
8184
/>
8285
</OverlayTrigger>
8386
</div>
@@ -95,8 +98,9 @@ const HoverCard = ({
9598
iconAs={Icon}
9699
size="sm"
97100
alt="Like"
98-
disabled={!userHasLikePermission}
101+
disabled={!userHasLikePermission || isDeleted}
99102
iconClassNames="like-icon-dimensions"
103+
style={isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}}
100104
onClick={(e) => {
101105
e.preventDefault();
102106
onLike();
@@ -119,6 +123,8 @@ const HoverCard = ({
119123
size="sm"
120124
alt="Follow"
121125
iconClassNames="follow-icon-dimensions"
126+
disabled={isDeleted}
127+
style={isDeleted ? { opacity: 0.3, cursor: 'not-allowed' } : {}}
122128
onClick={(e) => {
123129
e.preventDefault();
124130
onFollow();
@@ -165,12 +171,14 @@ HoverCard.propTypes = {
165171
)),
166172
onFollow: PropTypes.func,
167173
following: PropTypes.bool,
174+
isDeleted: PropTypes.bool,
168175
};
169176

170177
HoverCard.defaultProps = {
171178
onFollow: () => null,
172179
endorseIcons: null,
173180
following: undefined,
181+
isDeleted: false,
174182
};
175183

176184
export default React.memo(HoverCard);

src/discussions/data/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ export const ContentTypes = {
1010
POST: 'POST',
1111
COMMENT: 'COMMENT',
1212
};
13+
14+
export const THREAD_FILTER_TYPES = {
15+
ACTIVE: 'active',
16+
DELETED: 'deleted',
17+
};

src/discussions/data/selectors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function selectAreThreadsFiltered(state) {
6767
}
6868

6969
return !(
70-
filters.status === PostsStatusFilter.ALL
70+
(filters.status === PostsStatusFilter.ALL || filters.status === PostsStatusFilter.ACTIVE)
7171
&& filters.postType === ThreadType.ALL
7272
);
7373
}

src/discussions/learners/LearnerActionsDropdown.jsx

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import React, {
2-
useCallback, useRef, useState,
2+
useCallback, useEffect, useRef, useState,
33
} from 'react';
4+
import ReactDOM from 'react-dom';
45
import PropTypes from 'prop-types';
56

67
import {
78
Button, Dropdown, Icon, IconButton, ModalPopup, useToggle,
89
} from '@openedx/paragon';
9-
import { MoreHoriz } from '@openedx/paragon/icons';
10+
import { ChevronRight, MoreHoriz } from '@openedx/paragon/icons';
1011

1112
import { useIntl } from '@edx/frontend-platform/i18n';
1213

13-
import { useLearnerActions } from './utils';
14+
import { useLearnerActionsMenu } from './utils';
1415

1516
const LearnerActionsDropdown = ({
1617
actionHandlers,
@@ -21,14 +22,16 @@ const LearnerActionsDropdown = ({
2122
const intl = useIntl();
2223
const [isOpen, open, close] = useToggle(false);
2324
const [target, setTarget] = useState(null);
24-
const actions = useLearnerActions(userHasBulkDeletePrivileges);
25+
const [activeSubmenu, setActiveSubmenu] = useState(null);
26+
const menuItems = useLearnerActionsMenu(intl, userHasBulkDeletePrivileges);
2527

2628
const handleActions = useCallback((action) => {
2729
const actionFunction = actionHandlers[action];
2830
if (actionFunction) {
2931
actionFunction();
32+
close();
3033
}
31-
}, [actionHandlers]);
34+
}, [actionHandlers, close]);
3235

3336
const onClickButton = useCallback((event) => {
3437
event.preventDefault();
@@ -39,8 +42,15 @@ const LearnerActionsDropdown = ({
3942
const onCloseModal = useCallback(() => {
4043
close();
4144
setTarget(null);
45+
setActiveSubmenu(null);
4246
}, [close]);
4347

48+
// Cleanup portal on unmount to prevent memory leaks and orphaned DOM nodes
49+
useEffect(() => () => {
50+
setTarget(null);
51+
setActiveSubmenu(null);
52+
}, []);
53+
4454
return (
4555
<>
4656
<IconButton
@@ -53,41 +63,69 @@ const LearnerActionsDropdown = ({
5363
iconClassNames={dropDownIconSize ? 'dropdown-icon-dimensions' : ''}
5464
/>
5565
<div className="actions-dropdown">
56-
<ModalPopup
57-
onClose={onCloseModal}
58-
positionRef={target}
59-
isOpen={isOpen}
60-
placement="bottom-start"
61-
>
62-
<div
63-
className="bg-white shadow d-flex flex-column mt-1"
64-
data-testid="learner-actions-dropdown-modal-popup"
66+
{isOpen && ReactDOM.createPortal(
67+
<ModalPopup
68+
onClose={onCloseModal}
69+
positionRef={target}
70+
isOpen={isOpen}
71+
placement="bottom-start"
72+
style={{ zIndex: 9998 }}
6573
>
66-
{actions.map(action => (
67-
<React.Fragment key={action.id}>
68-
<Dropdown.Item
69-
as={Button}
70-
variant="tertiary"
71-
size="inline"
72-
onClick={() => {
73-
close();
74-
handleActions(action.action);
75-
}}
76-
className="d-flex justify-content-start actions-dropdown-item"
77-
data-testId={action.id}
74+
<div
75+
className="bg-white shadow d-flex flex-column mt-1"
76+
data-testid="learner-actions-dropdown-modal-popup"
77+
style={{ position: 'relative', zIndex: 9998 }}
78+
>
79+
{menuItems.map(item => (
80+
<div
81+
key={item.id}
82+
className="position-relative"
83+
onMouseEnter={() => setActiveSubmenu(item.id)}
84+
onMouseLeave={() => setActiveSubmenu(null)}
85+
style={{ zIndex: 2 }}
7886
>
79-
<Icon
80-
src={action.icon}
81-
className="icon-size-24"
82-
/>
83-
<span className="font-weight-normal ml-2">
84-
{action.label.defaultMessage}
85-
</span>
86-
</Dropdown.Item>
87-
</React.Fragment>
88-
))}
89-
</div>
90-
</ModalPopup>
87+
<Dropdown.Item
88+
as={Button}
89+
variant="tertiary"
90+
size="inline"
91+
className="d-flex justify-content-between align-items-center actions-dropdown-item"
92+
data-testid={item.id}
93+
>
94+
<div className="d-flex align-items-center">
95+
<span className="font-weight-normal">
96+
{item.label}
97+
</span>
98+
</div>
99+
<Icon
100+
src={ChevronRight}
101+
className="icon-size-16"
102+
/>
103+
</Dropdown.Item>
104+
{activeSubmenu === item.id && (
105+
<div className="bg-white learner-submenu-container">
106+
{item.submenu.map(subItem => (
107+
<Dropdown.Item
108+
key={subItem.id}
109+
as={Button}
110+
variant="tertiary"
111+
size="inline"
112+
onClick={() => handleActions(subItem.action)}
113+
className="d-flex justify-content-start actions-dropdown-item"
114+
data-testid={subItem.id}
115+
>
116+
<span className="font-weight-normal">
117+
{subItem.label}
118+
</span>
119+
</Dropdown.Item>
120+
))}
121+
</div>
122+
)}
123+
</div>
124+
))}
125+
</div>
126+
</ModalPopup>,
127+
document.body,
128+
)}
91129
</div>
92130
</>
93131
);

0 commit comments

Comments
 (0)