Skip to content

Commit a026911

Browse files
feat: added rate limit dialogue (#796)
* feat: added rate limit dialogue * test: added test cases for post comment * test: added test for content creation dialogue * test: added test cases for empty topics * test: added test cases * feat: added content rate limit dialogue on post API for post and comment * fix: fixed editor close issue on submit * test: addd test cases
1 parent 3b527d9 commit a026911

25 files changed

+341
-62
lines changed

src/discussions/common/Confirmation.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const Confirmation = ({
5353
<ModalDialog.CloseButton variant={closeButtonVariant}>
5454
{closeButtonText || intl.formatMessage(messages.confirmationCancel)}
5555
</ModalDialog.CloseButton>
56+
{confirmAction && (
5657
<StatefulButton
5758
labels={{
5859
default: confirmButtonText || intl.formatMessage(messages.confirmationConfirm),
@@ -63,6 +64,7 @@ const Confirmation = ({
6364
variant={confirmButtonVariant}
6465
onClick={confirmAction}
6566
/>
67+
)}
6668
</ActionRow>
6769
</ModalDialog.Footer>
6870
</>

src/discussions/common/withEmailConfirmation.jsx renamed to src/discussions/common/withPostingRestrictions.jsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
import React, { useCallback, useMemo, useState } from 'react';
1+
import React, {
2+
useCallback, useMemo, useState,
3+
} from 'react';
4+
import PropTypes from 'prop-types';
25

36
import { useDispatch, useSelector } from 'react-redux';
47

58
import { useIntl } from '@edx/frontend-platform/i18n';
69

710
import { RequestStatus } from '../../data/constants';
8-
import { selectConfirmEmailStatus, selectShouldShowEmailConfirmation } from '../data/selectors';
11+
import { selectConfirmEmailStatus, selectContentCreationRateLimited, selectShouldShowEmailConfirmation } from '../data/selectors';
12+
import { hidePostEditor } from '../posts/data';
913
import { sendAccountActivationEmail } from '../posts/data/thunks';
1014
import postMessages from '../posts/post-actions-bar/messages';
1115
import { Confirmation } from '.';
1216

13-
const withEmailConfirmation = (WrappedComponent) => {
14-
const EnhancedComponent = (props) => {
17+
const withPostingRestrictions = (WrappedComponent) => {
18+
const EnhancedComponent = ({ onCloseEditor, ...rest }) => {
1519
const intl = useIntl();
1620
const dispatch = useDispatch();
1721
const [isConfirming, setIsConfirming] = useState(false);
1822
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
23+
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
1924
const confirmEmailStatus = useSelector(selectConfirmEmailStatus);
2025

2126
const openConfirmation = useCallback(() => {
@@ -24,7 +29,9 @@ const withEmailConfirmation = (WrappedComponent) => {
2429

2530
const closeConfirmation = useCallback(() => {
2631
setIsConfirming(false);
27-
}, []);
32+
dispatch(hidePostEditor());
33+
onCloseEditor?.();
34+
}, [onCloseEditor]);
2835

2936
const handleConfirmation = useCallback(() => {
3037
dispatch(sendAccountActivationEmail());
@@ -39,8 +46,9 @@ const withEmailConfirmation = (WrappedComponent) => {
3946
return (
4047
<>
4148
<WrappedComponent
42-
{...props}
43-
openEmailConfirmation={openConfirmation}
49+
{...rest}
50+
onCloseEditor={onCloseEditor}
51+
openRestrictionDialogue={openConfirmation}
4452
/>
4553
{shouldShowEmailConfirmation
4654
&& (
@@ -57,11 +65,26 @@ const withEmailConfirmation = (WrappedComponent) => {
5765
confirmButtonVariant="danger"
5866
/>
5967
)}
68+
{contentCreationRateLimited
69+
&& (
70+
<Confirmation
71+
isOpen={isConfirming}
72+
title={intl.formatMessage(postMessages.postLimitTitle)}
73+
description={intl.formatMessage(postMessages.postLimitDescription)}
74+
onClose={closeConfirmation}
75+
closeButtonText={intl.formatMessage(postMessages.closeButton)}
76+
closeButtonVariant="danger"
77+
/>
78+
)}
6079
</>
6180
);
6281
};
6382

83+
EnhancedComponent.propTypes = {
84+
onCloseEditor: PropTypes.func,
85+
};
86+
6487
return EnhancedComponent;
6588
};
6689

67-
export default withEmailConfirmation;
90+
export default withPostingRestrictions;

src/discussions/data/selectors.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ export const selectCaptchaSettings = state => state.config.captchaSettings;
3939

4040
export const selectIsEmailVerified = state => state.config.isEmailVerified;
4141

42+
export const selectContentCreationRateLimited = state => state.config.contentCreationRateLimited;
43+
4244
export const selectOnlyVerifiedUsersCanPost = state => state.config.onlyVerifiedUsersCanPost;
4345

4446
export const selectConfirmEmailStatus = state => state.threads.confirmEmailStatus;
4547

48+
export const selectPostStatus = state => state.comments.postStatus;
49+
4650
export const selectShouldShowEmailConfirmation = createSelector(
4751
[selectIsEmailVerified, selectOnlyVerifiedUsersCanPost],
4852
(isEmailVerified, onlyVerifiedUsersCanPost) => !isEmailVerified && onlyVerifiedUsersCanPost,

src/discussions/data/slices.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const configSlice = createSlice({
3131
postCloseReasons: [],
3232
enableInContext: false,
3333
isEmailVerified: false,
34+
contentCreationRateLimited: false,
3435
},
3536
reducers: {
3637
fetchConfigRequest: (state) => (
@@ -56,6 +57,12 @@ const configSlice = createSlice({
5657
status: RequestStatus.DENIED,
5758
}
5859
),
60+
setContentCreationRateLimited: (state) => (
61+
{
62+
...state,
63+
contentCreationRateLimited: true,
64+
}
65+
),
5966
},
6067
});
6168

@@ -64,6 +71,7 @@ export const {
6471
fetchConfigFailed,
6572
fetchConfigRequest,
6673
fetchConfigSuccess,
74+
setContentCreationRateLimited,
6775
} = configSlice.actions;
6876

6977
export const configReducer = configSlice.reducer;

src/discussions/empty-posts/EmptyPosts.jsx

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

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

8-
import withEmailConfirmation from '../common/withEmailConfirmation';
8+
import withPostingRestrictions from '../common/withPostingRestrictions';
99
import { useIsOnTablet } from '../data/hooks';
1010
import {
1111
selectAreThreadsFiltered,
12+
selectContentCreationRateLimited,
1213
selectPostThreadCount,
1314
selectShouldShowEmailConfirmation,
1415
} from '../data/selectors';
@@ -17,17 +18,22 @@ import { showPostEditor } from '../posts/data';
1718
import postMessages from '../posts/post-actions-bar/messages';
1819
import EmptyPage from './EmptyPage';
1920

20-
const EmptyPosts = ({ subTitleMessage, openEmailConfirmation }) => {
21+
const EmptyPosts = ({ subTitleMessage, openRestrictionDialogue }) => {
2122
const intl = useIntl();
2223
const dispatch = useDispatch();
2324
const isOnTabletorDesktop = useIsOnTablet();
2425
const isFiltered = useSelector(selectAreThreadsFiltered);
2526
const totalThreads = useSelector(selectPostThreadCount);
2627
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
28+
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
2729

2830
const addPost = useCallback(() => {
29-
if (shouldShowEmailConfirmation) { openEmailConfirmation(); } else { dispatch(showPostEditor()); }
30-
}, [shouldShowEmailConfirmation, openEmailConfirmation]);
31+
if (shouldShowEmailConfirmation || contentCreationRateLimited) {
32+
openRestrictionDialogue();
33+
} else {
34+
dispatch(showPostEditor());
35+
}
36+
}, [shouldShowEmailConfirmation, openRestrictionDialogue, contentCreationRateLimited]);
3137

3238
let title = messages.noPostSelected;
3339
let subTitle = null;
@@ -64,7 +70,7 @@ EmptyPosts.propTypes = {
6470
defaultMessage: propTypes.string,
6571
description: propTypes.string,
6672
}).isRequired,
67-
openEmailConfirmation: propTypes.func.isRequired,
73+
openRestrictionDialogue: propTypes.func.isRequired,
6874
};
6975

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

src/discussions/empty-posts/EmptyTopics.jsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,31 @@ import { useParams } from 'react-router-dom';
66

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

9-
import withEmailConfirmation from '../common/withEmailConfirmation';
9+
import withPostingRestrictions from '../common/withPostingRestrictions';
1010
import { useIsOnTablet, useTotalTopicThreadCount } from '../data/hooks';
11-
import { selectShouldShowEmailConfirmation, selectTopicThreadCount } from '../data/selectors';
11+
import { selectContentCreationRateLimited, selectShouldShowEmailConfirmation, selectTopicThreadCount } from '../data/selectors';
1212
import messages from '../messages';
1313
import { showPostEditor } from '../posts/data';
1414
import postMessages from '../posts/post-actions-bar/messages';
1515
import EmptyPage from './EmptyPage';
1616

17-
const EmptyTopics = ({ openEmailConfirmation }) => {
17+
const EmptyTopics = ({ openRestrictionDialogue }) => {
1818
const intl = useIntl();
1919
const { topicId } = useParams();
2020
const dispatch = useDispatch();
2121
const isOnTabletorDesktop = useIsOnTablet();
2222
const hasGlobalThreads = useTotalTopicThreadCount() > 0;
2323
const topicThreadCount = useSelector(selectTopicThreadCount(topicId));
2424
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
25+
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
2526

2627
const addPost = useCallback(() => {
27-
if (shouldShowEmailConfirmation) { openEmailConfirmation(); } else { dispatch(showPostEditor()); }
28-
}, [shouldShowEmailConfirmation, openEmailConfirmation]);
28+
if (shouldShowEmailConfirmation || contentCreationRateLimited) {
29+
openRestrictionDialogue();
30+
} else {
31+
dispatch(showPostEditor());
32+
}
33+
}, [shouldShowEmailConfirmation, openRestrictionDialogue, contentCreationRateLimited]);
2934

3035
let title = messages.emptyTitle;
3136
let fullWidth = false;
@@ -67,7 +72,7 @@ const EmptyTopics = ({ openEmailConfirmation }) => {
6772
};
6873

6974
EmptyTopics.propTypes = {
70-
openEmailConfirmation: propTypes.func.isRequired,
75+
openRestrictionDialogue: propTypes.func.isRequired,
7176
};
7277

73-
export default React.memo(withEmailConfirmation(EmptyTopics));
78+
export default React.memo(withPostingRestrictions(EmptyTopics));

src/discussions/empty-posts/EmptyTopics.test.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import { AppProvider } from '@edx/frontend-platform/react';
1313
import { getApiBaseUrl, Routes as ROUTES } from '../../data/constants';
1414
import { initializeStore } from '../../store';
1515
import executeThunk from '../../test-utils';
16+
import * as selectors from '../data/selectors';
1617
import messages from '../messages';
18+
import { showPostEditor } from '../posts/data';
1719
import fetchCourseTopics from '../topics/data/thunks';
1820
import EmptyTopics from './EmptyTopics';
1921

@@ -85,4 +87,17 @@ describe('EmptyTopics', () => {
8587

8688
expect(screen.queryByText('Send confirmation link')).toBeInTheDocument();
8789
});
90+
91+
it('should dispatch showPostEditor when email confirmation is not required and user clicks "Add a post"', async () => {
92+
jest.spyOn(selectors, 'selectShouldShowEmailConfirmation').mockReturnValue(false);
93+
94+
const dispatchSpy = jest.spyOn(store, 'dispatch');
95+
96+
renderComponent(`/${courseId}/topics/ncwtopic-1/`);
97+
98+
const addPostButton = await screen.findByRole('button', { name: 'Add a post' });
99+
await userEvent.click(addPostButton);
100+
101+
expect(dispatchSpy).toHaveBeenCalledWith(showPostEditor());
102+
});
88103
});

src/discussions/in-context-topics/TopicPostsView.test.jsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
fireEvent, render, screen, waitFor, within,
33
} from '@testing-library/react';
4+
import userEvent from '@testing-library/user-event';
45
import MockAdapter from 'axios-mock-adapter';
56
import { act } from 'react-dom/test-utils';
67
import { IntlProvider } from 'react-intl';
@@ -18,6 +19,7 @@ import { Routes as ROUTES } from '../../data/constants';
1819
import { initializeStore } from '../../store';
1920
import executeThunk from '../../test-utils';
2021
import DiscussionContext from '../common/context';
22+
import * as selectors from '../data/selectors';
2123
import { getThreadsApiUrl } from '../posts/data/api';
2224
import { fetchThreads } from '../posts/data/thunks';
2325
import { getCourseTopicsApiUrl } from './data/api';
@@ -302,4 +304,18 @@ describe('InContext Topic Posts View', () => {
302304
expect(container.querySelectorAll('.discussion-topic')).toHaveLength(3);
303305
});
304306
});
307+
308+
it('should dispatch showPostEditor when email confirmation is not required and user clicks "Add a post"', async () => {
309+
jest.spyOn(selectors, 'selectShouldShowEmailConfirmation').mockReturnValue(true);
310+
jest.spyOn(selectors, 'selectConfigLoadingStatus').mockReturnValue('successful');
311+
312+
await setupTopicsMockResponse();
313+
await renderComponent();
314+
315+
const addPostButton = await screen.findByText('Add a post');
316+
await userEvent.click(addPostButton);
317+
318+
const confirmationText = await screen.findByText(/send confirmation link/i);
319+
expect(confirmationText).toBeInTheDocument();
320+
});
305321
});

src/discussions/in-context-topics/TopicsView.test.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { AppProvider } from '@edx/frontend-platform/react';
1919
import { initializeStore } from '../../store';
2020
import executeThunk from '../../test-utils';
2121
import DiscussionContext from '../common/context';
22+
import * as selectors from '../data/selectors';
23+
import { showPostEditor } from '../posts';
2224
import EmptyTopics from './components/EmptyTopics';
2325
import { getCourseTopicsApiUrl } from './data/api';
2426
import { selectCoursewareTopics, selectNonCoursewareTopics } from './data/selectors';
@@ -270,4 +272,17 @@ describe('InContext Topics View', () => {
270272
const confirmationText = await screen.findByText(/send confirmation link/i);
271273
expect(confirmationText).toBeInTheDocument();
272274
});
275+
276+
it('should dispatch showPostEditor when email confirmation is not required and user clicks "Add a post"', async () => {
277+
jest.spyOn(selectors, 'selectShouldShowEmailConfirmation').mockReturnValue(false);
278+
279+
const dispatchSpy = jest.spyOn(store, 'dispatch');
280+
281+
renderEmptyTopicComponent();
282+
283+
const addPostButton = await screen.findByRole('button', { name: 'Add a post' });
284+
await userEvent.click(addPostButton);
285+
286+
expect(dispatchSpy).toHaveBeenCalledWith(showPostEditor());
287+
});
273288
});

0 commit comments

Comments
 (0)