Skip to content

Commit 76da74a

Browse files
feat: Modified discussions FE so that ONLY users with verified emails (#789)
* feat: Modified discussions FE so that ONLY users with verified emails can create content * feat: added comment and response functionality * test: fixed test cases * refactor: refactor code and added HOC * test: added test cases * test: added test cases for failed and denied * feat: added states for button * refactor: added callback function * test: added test case to close the dialogue * refactor: refactor function
1 parent 750720f commit 76da74a

File tree

19 files changed

+379
-39
lines changed

19 files changed

+379
-39
lines changed

src/discussions/common/Confirmation.jsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ const Confirmation = ({
1919
onClose,
2020
confirmAction,
2121
closeButtonVariant,
22+
confirmButtonState,
2223
confirmButtonVariant,
2324
confirmButtonText,
2425
isDataLoading,
2526
isConfirmButtonPending,
2627
pendingConfirmButtonText,
28+
closeButtonText,
2729
}) => {
2830
const intl = useIntl();
2931

@@ -42,22 +44,22 @@ const Confirmation = ({
4244
{title}
4345
</ModalDialog.Title>
4446
</ModalDialog.Header>
45-
<ModalDialog.Body>
47+
<ModalDialog.Body style={{ whiteSpace: 'pre-line' }}>
4648
{description}
4749
{boldDescription && <><br /><p className="font-weight-bold pt-2">{boldDescription}</p></>}
4850
</ModalDialog.Body>
4951
<ModalDialog.Footer>
5052
<ActionRow>
5153
<ModalDialog.CloseButton variant={closeButtonVariant}>
52-
{intl.formatMessage(messages.confirmationCancel)}
54+
{closeButtonText || intl.formatMessage(messages.confirmationCancel)}
5355
</ModalDialog.CloseButton>
5456
<StatefulButton
5557
labels={{
5658
default: confirmButtonText || intl.formatMessage(messages.confirmationConfirm),
5759
pending: pendingConfirmButtonText || confirmButtonText
5860
|| intl.formatMessage(messages.confirmationConfirm),
5961
}}
60-
state={isConfirmButtonPending ? 'pending' : confirmButtonVariant}
62+
state={isConfirmButtonPending ? 'pending' : confirmButtonState}
6163
variant={confirmButtonVariant}
6264
onClick={confirmAction}
6365
/>
@@ -82,6 +84,8 @@ Confirmation.propTypes = {
8284
isDataLoading: PropTypes.bool,
8385
isConfirmButtonPending: PropTypes.bool,
8486
pendingConfirmButtonText: PropTypes.string,
87+
closeButtonText: PropTypes.string,
88+
confirmButtonState: PropTypes.string,
8589
};
8690

8791
Confirmation.defaultProps = {
@@ -92,6 +96,8 @@ Confirmation.defaultProps = {
9296
isDataLoading: false,
9397
isConfirmButtonPending: false,
9498
pendingConfirmButtonText: '',
99+
closeButtonText: '',
100+
confirmButtonState: 'default',
95101
};
96102

97103
export default React.memo(Confirmation);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { useCallback, useMemo, useState } from 'react';
2+
3+
import { useDispatch, useSelector } from 'react-redux';
4+
5+
import { useIntl } from '@edx/frontend-platform/i18n';
6+
7+
import { RequestStatus } from '../../data/constants';
8+
import { selectConfirmEmailStatus, selectOnlyVerifiedUsersCanPost } from '../data/selectors';
9+
import { sendAccountActivationEmail } from '../posts/data/thunks';
10+
import postMessages from '../posts/post-actions-bar/messages';
11+
import { Confirmation } from '.';
12+
13+
const withEmailConfirmation = (WrappedComponent) => {
14+
const EnhancedComponent = (props) => {
15+
const intl = useIntl();
16+
const dispatch = useDispatch();
17+
const [isConfirming, setIsConfirming] = useState(false);
18+
const onlyVerifiedUsersCanPost = useSelector(selectOnlyVerifiedUsersCanPost);
19+
const confirmEmailStatus = useSelector(selectConfirmEmailStatus);
20+
21+
const openConfirmation = useCallback(() => {
22+
setIsConfirming(true);
23+
}, []);
24+
25+
const closeConfirmation = useCallback(() => {
26+
setIsConfirming(false);
27+
}, []);
28+
29+
const handleConfirmation = useCallback(() => {
30+
dispatch(sendAccountActivationEmail());
31+
}, [dispatch]);
32+
33+
const confirmButtonState = useMemo(() => {
34+
if (confirmEmailStatus === RequestStatus.IN_PROGRESS) { return 'pending'; }
35+
if (confirmEmailStatus === RequestStatus.SUCCESSFUL) { return 'complete'; }
36+
return 'primary';
37+
}, [confirmEmailStatus]);
38+
39+
return (
40+
<>
41+
<WrappedComponent
42+
{...props}
43+
openEmailConfirmation={openConfirmation}
44+
/>
45+
{!onlyVerifiedUsersCanPost
46+
&& (
47+
<Confirmation
48+
isOpen={isConfirming}
49+
title={intl.formatMessage(postMessages.confirmEmailTitle)}
50+
description={intl.formatMessage(postMessages.confirmEmailDescription)}
51+
onClose={closeConfirmation}
52+
confirmAction={handleConfirmation}
53+
closeButtonVariant="tertiary"
54+
confirmButtonState={confirmButtonState}
55+
confirmButtonText={intl.formatMessage(postMessages.confirmEmailButton)}
56+
closeButtonText={intl.formatMessage(postMessages.closeButton)}
57+
confirmButtonVariant="danger"
58+
/>
59+
)}
60+
</>
61+
);
62+
};
63+
64+
return EnhancedComponent;
65+
};
66+
67+
export default withEmailConfirmation;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { fireEvent, render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { IntlProvider } from 'react-intl';
4+
import { Context as ResponsiveContext } from 'react-responsive';
5+
import { MemoryRouter } from 'react-router';
6+
7+
import { initializeMockApp } from '@edx/frontend-platform';
8+
import { AppProvider } from '@edx/frontend-platform/react';
9+
10+
import { initializeStore } from '../../store';
11+
import EmptyPosts from '../empty-posts/EmptyPosts';
12+
import messages from '../messages';
13+
import { sendEmailForAccountActivation } from '../posts/data/api';
14+
15+
let store;
16+
const courseId = 'course-v1:edX+DemoX+Demo_Course';
17+
18+
jest.mock('../posts/data/api', () => ({
19+
sendEmailForAccountActivation: jest.fn(),
20+
}));
21+
22+
function renderComponent(location = `/${courseId}/`) {
23+
return render(
24+
<IntlProvider locale="en">
25+
<ResponsiveContext.Provider value={{ width: 1280 }}>
26+
<AppProvider store={store} wrapWithRouter={false}>
27+
<MemoryRouter initialEntries={[location]}>
28+
<EmptyPosts subTitleMessage={messages.emptyMyPosts} />
29+
</MemoryRouter>
30+
</AppProvider>
31+
</ResponsiveContext.Provider>
32+
</IntlProvider>,
33+
);
34+
}
35+
36+
describe('EmptyPage', () => {
37+
beforeEach(async () => {
38+
initializeMockApp({
39+
authenticatedUser: {
40+
userId: 3,
41+
username: 'abc123',
42+
administrator: true,
43+
roles: [],
44+
},
45+
});
46+
47+
store = initializeStore();
48+
});
49+
50+
it('should open the confirmation link dialogue box.', async () => {
51+
renderComponent(`/${courseId}/my-posts/`);
52+
53+
const addPostButton = screen.getByRole('button', { name: 'Add a post' });
54+
await userEvent.click(addPostButton);
55+
56+
expect(screen.queryByText('Send confirmation link')).toBeInTheDocument();
57+
});
58+
59+
it('dispatches sendAccountActivationEmail on confirm', async () => {
60+
sendEmailForAccountActivation.mockResolvedValue({ success: true });
61+
renderComponent(`/${courseId}/my-posts/`);
62+
63+
const addPostButton = screen.getByRole('button', { name: 'Add a post' });
64+
await userEvent.click(addPostButton);
65+
const confirmButton = screen.getByText('Send confirmation link');
66+
fireEvent.click(confirmButton);
67+
expect(sendEmailForAccountActivation).toHaveBeenCalled();
68+
});
69+
70+
it('should close the confirmation dialogue box.', async () => {
71+
renderComponent(`/${courseId}/my-posts/`);
72+
73+
const addPostButton = screen.getByRole('button', { name: 'Add a post' });
74+
await userEvent.click(addPostButton);
75+
const confirmButton = screen.getByText('Close');
76+
fireEvent.click(confirmButton);
77+
78+
expect(sendEmailForAccountActivation).toHaveBeenCalled();
79+
80+
expect(screen.queryByText('Close')).not.toBeInTheDocument();
81+
});
82+
});

src/discussions/data/selectors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export const selectIsNotifyAllLearnersEnabled = state => state.config.isNotifyAl
3737

3838
export const selectCaptchaSettings = state => state.config.captchaSettings;
3939

40+
export const selectIsEmailVerified = state => state.config.isEmailVerified;
41+
42+
export const selectOnlyVerifiedUsersCanPost = state => state.config.onlyVerifiedUsersCanPost;
43+
44+
export const selectConfirmEmailStatus = state => state.threads.confirmEmailStatus;
45+
4046
export const selectModerationSettings = state => ({
4147
postCloseReasons: state.config.postCloseReasons,
4248
editReasons: state.config.editReasons,

src/discussions/data/slices.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const configSlice = createSlice({
3030
editReasons: [],
3131
postCloseReasons: [],
3232
enableInContext: false,
33+
isEmailVerified: false,
3334
},
3435
reducers: {
3536
fetchConfigRequest: (state) => (

src/discussions/discussions-home/DiscussionsHome.test.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ describe('DiscussionsHome', () => {
207207
});
208208

209209
it('should display post editor form when click on add a post button for posts', async () => {
210+
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
211+
enableInContext: true, provider: 'openedx', hasModerationPrivileges: true, isEmailVerified: true,
212+
});
210213
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
211214
await renderComponent(`/${courseId}/my-posts`);
212215

@@ -221,7 +224,7 @@ describe('DiscussionsHome', () => {
221224

222225
it('should display post editor form when click on add a post button in legacy topics view', async () => {
223226
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
224-
enable_in_context: false, hasModerationPrivileges: true,
227+
enable_in_context: false, hasModerationPrivileges: true, isEmailVerified: true,
225228
});
226229
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
227230
await renderComponent(`/${courseId}/topics`);

src/discussions/empty-posts/EmptyPosts.jsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,29 @@ import { useDispatch, useSelector } from 'react-redux';
55

66
import { useIntl } from '@edx/frontend-platform/i18n';
77

8+
import withEmailConfirmation from '../common/withEmailConfirmation';
89
import { useIsOnTablet } from '../data/hooks';
9-
import { selectAreThreadsFiltered, selectPostThreadCount } from '../data/selectors';
10+
import {
11+
selectAreThreadsFiltered,
12+
selectIsEmailVerified,
13+
selectPostThreadCount,
14+
} from '../data/selectors';
1015
import messages from '../messages';
1116
import { showPostEditor } from '../posts/data';
1217
import postMessages from '../posts/post-actions-bar/messages';
1318
import EmptyPage from './EmptyPage';
1419

15-
const EmptyPosts = ({ subTitleMessage }) => {
20+
const EmptyPosts = ({ subTitleMessage, openEmailConfirmation }) => {
1621
const intl = useIntl();
1722
const dispatch = useDispatch();
1823
const isOnTabletorDesktop = useIsOnTablet();
1924
const isFiltered = useSelector(selectAreThreadsFiltered);
2025
const totalThreads = useSelector(selectPostThreadCount);
26+
const isEmailVerified = useSelector(selectIsEmailVerified);
2127

22-
const addPost = useCallback(() => (
23-
dispatch(showPostEditor())
24-
), []);
28+
const addPost = useCallback(() => {
29+
if (isEmailVerified) { dispatch(showPostEditor()); } else { openEmailConfirmation(); }
30+
}, [isEmailVerified, openEmailConfirmation]);
2531

2632
let title = messages.noPostSelected;
2733
let subTitle = null;
@@ -58,6 +64,7 @@ EmptyPosts.propTypes = {
5864
defaultMessage: propTypes.string,
5965
description: propTypes.string,
6066
}).isRequired,
67+
openEmailConfirmation: propTypes.func.isRequired,
6168
};
6269

63-
export default React.memo(EmptyPosts);
70+
export default React.memo(withEmailConfirmation(EmptyPosts));

src/discussions/empty-posts/EmptyTopics.jsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
import React, { useCallback } from 'react';
2+
import propTypes from 'prop-types';
23

34
import { useDispatch, useSelector } from 'react-redux';
45
import { useParams } from 'react-router-dom';
56

67
import { useIntl } from '@edx/frontend-platform/i18n';
78

9+
import withEmailConfirmation from '../common/withEmailConfirmation';
810
import { useIsOnTablet, useTotalTopicThreadCount } from '../data/hooks';
9-
import { selectTopicThreadCount } from '../data/selectors';
11+
import {
12+
selectIsEmailVerified, selectTopicThreadCount,
13+
} from '../data/selectors';
1014
import messages from '../messages';
1115
import { showPostEditor } from '../posts/data';
1216
import postMessages from '../posts/post-actions-bar/messages';
1317
import EmptyPage from './EmptyPage';
1418

15-
const EmptyTopics = () => {
19+
const EmptyTopics = ({ openEmailConfirmation }) => {
1620
const intl = useIntl();
1721
const { topicId } = useParams();
1822
const dispatch = useDispatch();
1923
const isOnTabletorDesktop = useIsOnTablet();
2024
const hasGlobalThreads = useTotalTopicThreadCount() > 0;
2125
const topicThreadCount = useSelector(selectTopicThreadCount(topicId));
26+
const isEmailVerified = useSelector(selectIsEmailVerified);
2227

23-
const addPost = useCallback(() => (
24-
dispatch(showPostEditor())
25-
), []);
28+
const addPost = useCallback(() => {
29+
if (isEmailVerified) { dispatch(showPostEditor()); } else { openEmailConfirmation(); }
30+
}, [isEmailVerified, openEmailConfirmation]);
2631

2732
let title = messages.emptyTitle;
2833
let fullWidth = false;
@@ -63,4 +68,8 @@ const EmptyTopics = () => {
6368
);
6469
};
6570

66-
export default EmptyTopics;
71+
EmptyTopics.propTypes = {
72+
openEmailConfirmation: propTypes.func.isRequired,
73+
};
74+
75+
export default React.memo(withEmailConfirmation(EmptyTopics));

src/discussions/in-context-topics/components/EmptyTopics.jsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import React, { useCallback, useContext } from 'react';
2+
import PropTypes from 'prop-types';
23

34
import { useDispatch, useSelector } from 'react-redux';
45
import { useParams } from 'react-router-dom';
56

67
import { useIntl } from '@edx/frontend-platform/i18n';
78

89
import DiscussionContext from '../../common/context';
10+
import withEmailConfirmation from '../../common/withEmailConfirmation';
911
import { useIsOnTablet } from '../../data/hooks';
10-
import { selectPostThreadCount } from '../../data/selectors';
12+
import { selectIsEmailVerified, selectPostThreadCount } from '../../data/selectors';
1113
import EmptyPage from '../../empty-posts/EmptyPage';
1214
import messages from '../../messages';
1315
import { messages as postMessages, showPostEditor } from '../../posts';
1416
import { selectCourseWareThreadsCount, selectTotalTopicsThreadsCount } from '../data/selectors';
1517

16-
const EmptyTopics = () => {
18+
const EmptyTopics = ({ openEmailConfirmation }) => {
1719
const intl = useIntl();
1820
const { category, topicId } = useParams();
1921
const dispatch = useDispatch();
@@ -23,10 +25,11 @@ const EmptyTopics = () => {
2325
const topicThreadsCount = useSelector(selectPostThreadCount);
2426
// hasGlobalThreads is used to determine if there are any post available in courseware and non-courseware topics
2527
const hasGlobalThreads = useSelector(selectTotalTopicsThreadsCount) > 0;
28+
const isEmailVerified = useSelector(selectIsEmailVerified);
2629

27-
const addPost = useCallback(() => (
28-
dispatch(showPostEditor())
29-
), []);
30+
const addPost = useCallback(() => {
31+
if (isEmailVerified) { dispatch(showPostEditor()); } else { openEmailConfirmation(); }
32+
}, [isEmailVerified, openEmailConfirmation]);
3033

3134
let title = messages.emptyTitle;
3235
let fullWidth = false;
@@ -75,4 +78,8 @@ const EmptyTopics = () => {
7578
);
7679
};
7780

78-
export default EmptyTopics;
81+
EmptyTopics.propTypes = {
82+
openEmailConfirmation: PropTypes.func.isRequired,
83+
};
84+
85+
export default React.memo(withEmailConfirmation(EmptyTopics));

0 commit comments

Comments
 (0)