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
This commit is contained in:
@@ -53,6 +53,7 @@ const Confirmation = ({
|
||||
<ModalDialog.CloseButton variant={closeButtonVariant}>
|
||||
{closeButtonText || intl.formatMessage(messages.confirmationCancel)}
|
||||
</ModalDialog.CloseButton>
|
||||
{confirmAction && (
|
||||
<StatefulButton
|
||||
labels={{
|
||||
default: confirmButtonText || intl.formatMessage(messages.confirmationConfirm),
|
||||
@@ -63,6 +64,7 @@ const Confirmation = ({
|
||||
variant={confirmButtonVariant}
|
||||
onClick={confirmAction}
|
||||
/>
|
||||
)}
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</>
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
useCallback, useMemo, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { selectConfirmEmailStatus, selectShouldShowEmailConfirmation } from '../data/selectors';
|
||||
import { selectConfirmEmailStatus, selectContentCreationRateLimited, selectShouldShowEmailConfirmation } from '../data/selectors';
|
||||
import { hidePostEditor } from '../posts/data';
|
||||
import { sendAccountActivationEmail } from '../posts/data/thunks';
|
||||
import postMessages from '../posts/post-actions-bar/messages';
|
||||
import { Confirmation } from '.';
|
||||
|
||||
const withEmailConfirmation = (WrappedComponent) => {
|
||||
const EnhancedComponent = (props) => {
|
||||
const withPostingRestrictions = (WrappedComponent) => {
|
||||
const EnhancedComponent = ({ onCloseEditor, ...rest }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
const confirmEmailStatus = useSelector(selectConfirmEmailStatus);
|
||||
|
||||
const openConfirmation = useCallback(() => {
|
||||
@@ -24,7 +29,9 @@ const withEmailConfirmation = (WrappedComponent) => {
|
||||
|
||||
const closeConfirmation = useCallback(() => {
|
||||
setIsConfirming(false);
|
||||
}, []);
|
||||
dispatch(hidePostEditor());
|
||||
onCloseEditor?.();
|
||||
}, [onCloseEditor]);
|
||||
|
||||
const handleConfirmation = useCallback(() => {
|
||||
dispatch(sendAccountActivationEmail());
|
||||
@@ -39,8 +46,9 @@ const withEmailConfirmation = (WrappedComponent) => {
|
||||
return (
|
||||
<>
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
openEmailConfirmation={openConfirmation}
|
||||
{...rest}
|
||||
onCloseEditor={onCloseEditor}
|
||||
openRestrictionDialogue={openConfirmation}
|
||||
/>
|
||||
{shouldShowEmailConfirmation
|
||||
&& (
|
||||
@@ -57,11 +65,26 @@ const withEmailConfirmation = (WrappedComponent) => {
|
||||
confirmButtonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
{contentCreationRateLimited
|
||||
&& (
|
||||
<Confirmation
|
||||
isOpen={isConfirming}
|
||||
title={intl.formatMessage(postMessages.postLimitTitle)}
|
||||
description={intl.formatMessage(postMessages.postLimitDescription)}
|
||||
onClose={closeConfirmation}
|
||||
closeButtonText={intl.formatMessage(postMessages.closeButton)}
|
||||
closeButtonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EnhancedComponent.propTypes = {
|
||||
onCloseEditor: PropTypes.func,
|
||||
};
|
||||
|
||||
return EnhancedComponent;
|
||||
};
|
||||
|
||||
export default withEmailConfirmation;
|
||||
export default withPostingRestrictions;
|
||||
@@ -39,10 +39,14 @@ export const selectCaptchaSettings = state => state.config.captchaSettings;
|
||||
|
||||
export const selectIsEmailVerified = state => state.config.isEmailVerified;
|
||||
|
||||
export const selectContentCreationRateLimited = state => state.config.contentCreationRateLimited;
|
||||
|
||||
export const selectOnlyVerifiedUsersCanPost = state => state.config.onlyVerifiedUsersCanPost;
|
||||
|
||||
export const selectConfirmEmailStatus = state => state.threads.confirmEmailStatus;
|
||||
|
||||
export const selectPostStatus = state => state.comments.postStatus;
|
||||
|
||||
export const selectShouldShowEmailConfirmation = createSelector(
|
||||
[selectIsEmailVerified, selectOnlyVerifiedUsersCanPost],
|
||||
(isEmailVerified, onlyVerifiedUsersCanPost) => !isEmailVerified && onlyVerifiedUsersCanPost,
|
||||
|
||||
@@ -31,6 +31,7 @@ const configSlice = createSlice({
|
||||
postCloseReasons: [],
|
||||
enableInContext: false,
|
||||
isEmailVerified: false,
|
||||
contentCreationRateLimited: false,
|
||||
},
|
||||
reducers: {
|
||||
fetchConfigRequest: (state) => (
|
||||
@@ -56,6 +57,12 @@ const configSlice = createSlice({
|
||||
status: RequestStatus.DENIED,
|
||||
}
|
||||
),
|
||||
setContentCreationRateLimited: (state) => (
|
||||
{
|
||||
...state,
|
||||
contentCreationRateLimited: true,
|
||||
}
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -64,6 +71,7 @@ export const {
|
||||
fetchConfigFailed,
|
||||
fetchConfigRequest,
|
||||
fetchConfigSuccess,
|
||||
setContentCreationRateLimited,
|
||||
} = configSlice.actions;
|
||||
|
||||
export const configReducer = configSlice.reducer;
|
||||
|
||||
@@ -5,10 +5,11 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import withEmailConfirmation from '../common/withEmailConfirmation';
|
||||
import withPostingRestrictions from '../common/withPostingRestrictions';
|
||||
import { useIsOnTablet } from '../data/hooks';
|
||||
import {
|
||||
selectAreThreadsFiltered,
|
||||
selectContentCreationRateLimited,
|
||||
selectPostThreadCount,
|
||||
selectShouldShowEmailConfirmation,
|
||||
} from '../data/selectors';
|
||||
@@ -17,17 +18,22 @@ import { showPostEditor } from '../posts/data';
|
||||
import postMessages from '../posts/post-actions-bar/messages';
|
||||
import EmptyPage from './EmptyPage';
|
||||
|
||||
const EmptyPosts = ({ subTitleMessage, openEmailConfirmation }) => {
|
||||
const EmptyPosts = ({ subTitleMessage, openRestrictionDialogue }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const isOnTabletorDesktop = useIsOnTablet();
|
||||
const isFiltered = useSelector(selectAreThreadsFiltered);
|
||||
const totalThreads = useSelector(selectPostThreadCount);
|
||||
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
|
||||
const addPost = useCallback(() => {
|
||||
if (shouldShowEmailConfirmation) { openEmailConfirmation(); } else { dispatch(showPostEditor()); }
|
||||
}, [shouldShowEmailConfirmation, openEmailConfirmation]);
|
||||
if (shouldShowEmailConfirmation || contentCreationRateLimited) {
|
||||
openRestrictionDialogue();
|
||||
} else {
|
||||
dispatch(showPostEditor());
|
||||
}
|
||||
}, [shouldShowEmailConfirmation, openRestrictionDialogue, contentCreationRateLimited]);
|
||||
|
||||
let title = messages.noPostSelected;
|
||||
let subTitle = null;
|
||||
@@ -64,7 +70,7 @@ EmptyPosts.propTypes = {
|
||||
defaultMessage: propTypes.string,
|
||||
description: propTypes.string,
|
||||
}).isRequired,
|
||||
openEmailConfirmation: propTypes.func.isRequired,
|
||||
openRestrictionDialogue: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withEmailConfirmation(EmptyPosts));
|
||||
export default React.memo(withPostingRestrictions(EmptyPosts));
|
||||
|
||||
@@ -6,15 +6,15 @@ import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import withEmailConfirmation from '../common/withEmailConfirmation';
|
||||
import withPostingRestrictions from '../common/withPostingRestrictions';
|
||||
import { useIsOnTablet, useTotalTopicThreadCount } from '../data/hooks';
|
||||
import { selectShouldShowEmailConfirmation, selectTopicThreadCount } from '../data/selectors';
|
||||
import { selectContentCreationRateLimited, selectShouldShowEmailConfirmation, selectTopicThreadCount } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { showPostEditor } from '../posts/data';
|
||||
import postMessages from '../posts/post-actions-bar/messages';
|
||||
import EmptyPage from './EmptyPage';
|
||||
|
||||
const EmptyTopics = ({ openEmailConfirmation }) => {
|
||||
const EmptyTopics = ({ openRestrictionDialogue }) => {
|
||||
const intl = useIntl();
|
||||
const { topicId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
@@ -22,10 +22,15 @@ const EmptyTopics = ({ openEmailConfirmation }) => {
|
||||
const hasGlobalThreads = useTotalTopicThreadCount() > 0;
|
||||
const topicThreadCount = useSelector(selectTopicThreadCount(topicId));
|
||||
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
|
||||
const addPost = useCallback(() => {
|
||||
if (shouldShowEmailConfirmation) { openEmailConfirmation(); } else { dispatch(showPostEditor()); }
|
||||
}, [shouldShowEmailConfirmation, openEmailConfirmation]);
|
||||
if (shouldShowEmailConfirmation || contentCreationRateLimited) {
|
||||
openRestrictionDialogue();
|
||||
} else {
|
||||
dispatch(showPostEditor());
|
||||
}
|
||||
}, [shouldShowEmailConfirmation, openRestrictionDialogue, contentCreationRateLimited]);
|
||||
|
||||
let title = messages.emptyTitle;
|
||||
let fullWidth = false;
|
||||
@@ -67,7 +72,7 @@ const EmptyTopics = ({ openEmailConfirmation }) => {
|
||||
};
|
||||
|
||||
EmptyTopics.propTypes = {
|
||||
openEmailConfirmation: propTypes.func.isRequired,
|
||||
openRestrictionDialogue: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withEmailConfirmation(EmptyTopics));
|
||||
export default React.memo(withPostingRestrictions(EmptyTopics));
|
||||
|
||||
@@ -13,7 +13,9 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { getApiBaseUrl, Routes as ROUTES } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import executeThunk from '../../test-utils';
|
||||
import * as selectors from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { showPostEditor } from '../posts/data';
|
||||
import fetchCourseTopics from '../topics/data/thunks';
|
||||
import EmptyTopics from './EmptyTopics';
|
||||
|
||||
@@ -85,4 +87,17 @@ describe('EmptyTopics', () => {
|
||||
|
||||
expect(screen.queryByText('Send confirmation link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should dispatch showPostEditor when email confirmation is not required and user clicks "Add a post"', async () => {
|
||||
jest.spyOn(selectors, 'selectShouldShowEmailConfirmation').mockReturnValue(false);
|
||||
|
||||
const dispatchSpy = jest.spyOn(store, 'dispatch');
|
||||
|
||||
renderComponent(`/${courseId}/topics/ncwtopic-1/`);
|
||||
|
||||
const addPostButton = await screen.findByRole('button', { name: 'Add a post' });
|
||||
await userEvent.click(addPostButton);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(showPostEditor());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
fireEvent, render, screen, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
@@ -18,6 +19,7 @@ import { Routes as ROUTES } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import executeThunk from '../../test-utils';
|
||||
import DiscussionContext from '../common/context';
|
||||
import * as selectors from '../data/selectors';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import { getCourseTopicsApiUrl } from './data/api';
|
||||
@@ -302,4 +304,18 @@ describe('InContext Topic Posts View', () => {
|
||||
expect(container.querySelectorAll('.discussion-topic')).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch showPostEditor when email confirmation is not required and user clicks "Add a post"', async () => {
|
||||
jest.spyOn(selectors, 'selectShouldShowEmailConfirmation').mockReturnValue(true);
|
||||
jest.spyOn(selectors, 'selectConfigLoadingStatus').mockReturnValue('successful');
|
||||
|
||||
await setupTopicsMockResponse();
|
||||
await renderComponent();
|
||||
|
||||
const addPostButton = await screen.findByText('Add a post');
|
||||
await userEvent.click(addPostButton);
|
||||
|
||||
const confirmationText = await screen.findByText(/send confirmation link/i);
|
||||
expect(confirmationText).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,8 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeStore } from '../../store';
|
||||
import executeThunk from '../../test-utils';
|
||||
import DiscussionContext from '../common/context';
|
||||
import * as selectors from '../data/selectors';
|
||||
import { showPostEditor } from '../posts';
|
||||
import EmptyTopics from './components/EmptyTopics';
|
||||
import { getCourseTopicsApiUrl } from './data/api';
|
||||
import { selectCoursewareTopics, selectNonCoursewareTopics } from './data/selectors';
|
||||
@@ -270,4 +272,17 @@ describe('InContext Topics View', () => {
|
||||
const confirmationText = await screen.findByText(/send confirmation link/i);
|
||||
expect(confirmationText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should dispatch showPostEditor when email confirmation is not required and user clicks "Add a post"', async () => {
|
||||
jest.spyOn(selectors, 'selectShouldShowEmailConfirmation').mockReturnValue(false);
|
||||
|
||||
const dispatchSpy = jest.spyOn(store, 'dispatch');
|
||||
|
||||
renderEmptyTopicComponent();
|
||||
|
||||
const addPostButton = await screen.findByRole('button', { name: 'Add a post' });
|
||||
await userEvent.click(addPostButton);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(showPostEditor());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,15 +7,15 @@ import { useParams } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import DiscussionContext from '../../common/context';
|
||||
import withEmailConfirmation from '../../common/withEmailConfirmation';
|
||||
import withPostingRestrictions from '../../common/withPostingRestrictions';
|
||||
import { useIsOnTablet } from '../../data/hooks';
|
||||
import { selectPostThreadCount, selectShouldShowEmailConfirmation } from '../../data/selectors';
|
||||
import { selectContentCreationRateLimited, selectPostThreadCount, selectShouldShowEmailConfirmation } from '../../data/selectors';
|
||||
import EmptyPage from '../../empty-posts/EmptyPage';
|
||||
import messages from '../../messages';
|
||||
import { messages as postMessages, showPostEditor } from '../../posts';
|
||||
import { selectCourseWareThreadsCount, selectTotalTopicsThreadsCount } from '../data/selectors';
|
||||
|
||||
const EmptyTopics = ({ openEmailConfirmation }) => {
|
||||
const EmptyTopics = ({ openRestrictionDialogue }) => {
|
||||
const intl = useIntl();
|
||||
const { category, topicId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
@@ -26,10 +26,15 @@ const EmptyTopics = ({ openEmailConfirmation }) => {
|
||||
// hasGlobalThreads is used to determine if there are any post available in courseware and non-courseware topics
|
||||
const hasGlobalThreads = useSelector(selectTotalTopicsThreadsCount) > 0;
|
||||
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
|
||||
const addPost = useCallback(() => {
|
||||
if (shouldShowEmailConfirmation) { openEmailConfirmation(); } else { dispatch(showPostEditor()); }
|
||||
}, [shouldShowEmailConfirmation, openEmailConfirmation]);
|
||||
if (shouldShowEmailConfirmation || contentCreationRateLimited) {
|
||||
openRestrictionDialogue();
|
||||
} else {
|
||||
dispatch(showPostEditor());
|
||||
}
|
||||
}, [shouldShowEmailConfirmation, openRestrictionDialogue, contentCreationRateLimited]);
|
||||
|
||||
let title = messages.emptyTitle;
|
||||
let fullWidth = false;
|
||||
@@ -79,7 +84,7 @@ const EmptyTopics = ({ openEmailConfirmation }) => {
|
||||
};
|
||||
|
||||
EmptyTopics.propTypes = {
|
||||
openEmailConfirmation: PropTypes.func.isRequired,
|
||||
openRestrictionDialogue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withEmailConfirmation(EmptyTopics));
|
||||
export default React.memo(withPostingRestrictions(EmptyTopics));
|
||||
|
||||
@@ -39,6 +39,7 @@ import selectTours from '../tours/data/selectors';
|
||||
import { fetchDiscussionTours } from '../tours/data/thunks';
|
||||
import discussionTourFactory from '../tours/data/tours.factory';
|
||||
import { getCommentsApiUrl } from './data/api';
|
||||
import * as selectors from './data/selectors';
|
||||
import { fetchCommentResponses, removeComment } from './data/thunks';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
@@ -110,6 +111,7 @@ async function setupCourseConfig(
|
||||
isEmailVerified = true,
|
||||
onlyVerifiedUsersCanPost = false,
|
||||
hasModerationPrivileges = true,
|
||||
contentCreationRateLimited = false,
|
||||
) {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
hasModerationPrivileges,
|
||||
@@ -124,6 +126,7 @@ async function setupCourseConfig(
|
||||
],
|
||||
isEmailVerified,
|
||||
onlyVerifiedUsersCanPost,
|
||||
contentCreationRateLimited,
|
||||
});
|
||||
axiosMock.onGet(`${courseSettingsApiUrl}${courseId}/settings`).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
@@ -1105,6 +1108,58 @@ describe('ThreadView', () => {
|
||||
|
||||
expect(screen.queryByText('Send confirmation link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open the comment editor by clicking on add response button.', async () => {
|
||||
await setupCourseConfig(true, true);
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const addResponseButton = screen.getByTestId('add-response');
|
||||
|
||||
await act(async () => { fireEvent.click(addResponseButton); });
|
||||
|
||||
expect(screen.queryByTestId('tinymce-editor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open the rate limit dialogue.', async () => {
|
||||
await setupCourseConfig(true, true);
|
||||
axiosMock.onPost(commentsApiUrl).reply(429);
|
||||
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const addResponseButton = screen.getByTestId('add-response');
|
||||
|
||||
await act(async () => { fireEvent.click(addResponseButton); });
|
||||
|
||||
await act(async () => { fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'New response' } }); });
|
||||
await act(async () => { fireEvent.click(screen.getByText(/submit/i)); });
|
||||
|
||||
expect(screen.queryByText('Post limit reached')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open the dialogue Post limit reached by clicking on add response button.', async () => {
|
||||
await setupCourseConfig(true, true, true, true);
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const addResponseButton = screen.getByTestId('add-response');
|
||||
|
||||
await act(async () => { fireEvent.click(addResponseButton); });
|
||||
|
||||
expect(screen.queryByText('Post limit reached')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open the editor by clicking on add comment button.', async () => {
|
||||
jest.spyOn(selectors, 'selectCommentResponses').mockImplementation(() => () => (
|
||||
['reply-1', 'reply-2', 'reply-3', 'reply-4', 'reply-5']
|
||||
));
|
||||
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const addCommentButton = screen.getByTestId('add-comment-2');
|
||||
|
||||
await act(async () => { fireEvent.click(addCommentButton); });
|
||||
|
||||
expect(screen.queryByTestId('tinymce-editor')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MockReCAPTCHA', () => {
|
||||
|
||||
@@ -7,21 +7,22 @@ import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { ThreadType } from '../../../data/constants';
|
||||
import withEmailConfirmation from '../../common/withEmailConfirmation';
|
||||
import withPostingRestrictions from '../../common/withPostingRestrictions';
|
||||
import { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { selectShouldShowEmailConfirmation } from '../../data/selectors';
|
||||
import { selectContentCreationRateLimited, selectShouldShowEmailConfirmation } from '../../data/selectors';
|
||||
import { isLastElementOfList } from '../../utils';
|
||||
import { usePostComments } from '../data/hooks';
|
||||
import messages from '../messages';
|
||||
import PostCommentsContext from '../postCommentsContext';
|
||||
import { Comment, ResponseEditor } from './comment';
|
||||
|
||||
const CommentsView = ({ threadType, openEmailConfirmation }) => {
|
||||
const CommentsView = ({ threadType, openRestrictionDialogue }) => {
|
||||
const intl = useIntl();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const { isClosed } = useContext(PostCommentsContext);
|
||||
const isUserPrivilegedInPostingRestriction = useUserPostingEnabled();
|
||||
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
|
||||
const {
|
||||
endorsedCommentsIds,
|
||||
@@ -32,8 +33,12 @@ const CommentsView = ({ threadType, openEmailConfirmation }) => {
|
||||
} = usePostComments(threadType);
|
||||
|
||||
const handleAddResponse = useCallback(() => {
|
||||
if (shouldShowEmailConfirmation) { openEmailConfirmation(); } else { setAddingResponse(true); }
|
||||
}, [shouldShowEmailConfirmation, openEmailConfirmation]);
|
||||
if (shouldShowEmailConfirmation || contentCreationRateLimited) {
|
||||
openRestrictionDialogue();
|
||||
} else {
|
||||
setAddingResponse(true);
|
||||
}
|
||||
}, [shouldShowEmailConfirmation, openRestrictionDialogue, contentCreationRateLimited]);
|
||||
|
||||
const handleCloseResponseEditor = useCallback(() => {
|
||||
setAddingResponse(false);
|
||||
@@ -119,7 +124,7 @@ CommentsView.propTypes = {
|
||||
threadType: PropTypes.oneOf([
|
||||
ThreadType.DISCUSSION, ThreadType.QUESTION,
|
||||
]).isRequired,
|
||||
openEmailConfirmation: PropTypes.func.isRequired,
|
||||
openRestrictionDialogue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withEmailConfirmation(CommentsView));
|
||||
export default React.memo(withPostingRestrictions(CommentsView));
|
||||
|
||||
@@ -14,10 +14,10 @@ import { ContentActions, EndorsementStatus } from '../../../../data/constants';
|
||||
import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../../common';
|
||||
import DiscussionContext from '../../../common/context';
|
||||
import HoverCard from '../../../common/HoverCard';
|
||||
import withEmailConfirmation from '../../../common/withEmailConfirmation';
|
||||
import withPostingRestrictions from '../../../common/withPostingRestrictions';
|
||||
import { ContentTypes } from '../../../data/constants';
|
||||
import { useUserPostingEnabled } from '../../../data/hooks';
|
||||
import { selectShouldShowEmailConfirmation } from '../../../data/selectors';
|
||||
import { selectContentCreationRateLimited, selectShouldShowEmailConfirmation } from '../../../data/selectors';
|
||||
import { fetchThread } from '../../../posts/data/thunks';
|
||||
import LikeButton from '../../../posts/post/LikeButton';
|
||||
import { useActions } from '../../../utils';
|
||||
@@ -40,7 +40,7 @@ const Comment = ({
|
||||
commentId,
|
||||
marginBottom,
|
||||
showFullThread = true,
|
||||
openEmailConfirmation,
|
||||
openRestrictionDialogue,
|
||||
}) => {
|
||||
const comment = useSelector(selectCommentOrResponseById(commentId));
|
||||
const {
|
||||
@@ -66,6 +66,7 @@ const Comment = ({
|
||||
const actions = useActions(ContentTypes.COMMENT, id);
|
||||
const isUserPrivilegedInPostingRestriction = useUserPostingEnabled();
|
||||
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
|
||||
useEffect(() => {
|
||||
// If the comment has a parent comment, it won't have any children, so don't fetch them.
|
||||
@@ -183,7 +184,8 @@ const Comment = ({
|
||||
id={id}
|
||||
contentType={ContentTypes.COMMENT}
|
||||
actionHandlers={actionHandlers}
|
||||
handleResponseCommentButton={shouldShowEmailConfirmation ? openEmailConfirmation : handleAddCommentButton}
|
||||
handleResponseCommentButton={shouldShowEmailConfirmation || contentCreationRateLimited
|
||||
? openRestrictionDialogue : handleAddCommentButton}
|
||||
addResponseCommentButtonMessage={intl.formatMessage(messages.addComment)}
|
||||
onLike={handleCommentLike}
|
||||
voted={voted}
|
||||
@@ -274,7 +276,9 @@ const Comment = ({
|
||||
className="d-flex flex-grow mt-2 font-style font-weight-500 text-primary-500 add-comment-btn rounded-0"
|
||||
variant="plain"
|
||||
style={{ height: '36px' }}
|
||||
onClick={shouldShowEmailConfirmation ? openEmailConfirmation : handleAddCommentReply}
|
||||
data-testid="add-comment-2"
|
||||
onClick={shouldShowEmailConfirmation || contentCreationRateLimited
|
||||
? openRestrictionDialogue : handleAddCommentReply}
|
||||
>
|
||||
{intl.formatMessage(messages.addComment)}
|
||||
</Button>
|
||||
@@ -291,7 +295,7 @@ Comment.propTypes = {
|
||||
commentId: PropTypes.string.isRequired,
|
||||
marginBottom: PropTypes.bool,
|
||||
showFullThread: PropTypes.bool,
|
||||
openEmailConfirmation: PropTypes.func.isRequired,
|
||||
openRestrictionDialogue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Comment.defaultProps = {
|
||||
@@ -299,4 +303,4 @@ Comment.defaultProps = {
|
||||
showFullThread: true,
|
||||
};
|
||||
|
||||
export default React.memo(withEmailConfirmation(Comment));
|
||||
export default React.memo(withPostingRestrictions(Comment));
|
||||
|
||||
@@ -15,12 +15,16 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { TinyMCEEditor } from '../../../../components';
|
||||
import FormikErrorFeedback from '../../../../components/FormikErrorFeedback';
|
||||
import PostPreviewPanel from '../../../../components/PostPreviewPanel';
|
||||
import { RequestStatus } from '../../../../data/constants';
|
||||
import useDispatchWithState from '../../../../data/hooks';
|
||||
import DiscussionContext from '../../../common/context';
|
||||
import withPostingRestrictions from '../../../common/withPostingRestrictions';
|
||||
import {
|
||||
selectCaptchaSettings,
|
||||
selectContentCreationRateLimited,
|
||||
selectIsUserLearner,
|
||||
selectModerationSettings,
|
||||
selectPostStatus,
|
||||
selectUserHasModerationPrivileges,
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsStaff,
|
||||
@@ -36,11 +40,13 @@ const CommentEditor = ({
|
||||
edit,
|
||||
formClasses,
|
||||
onCloseEditor,
|
||||
openRestrictionDialogue,
|
||||
}) => {
|
||||
const {
|
||||
id, threadId, parentId, rawBody, author, lastEdit,
|
||||
} = comment;
|
||||
const intl = useIntl();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const editorRef = useRef(null);
|
||||
const formRef = useRef(null);
|
||||
const recaptchaRef = useRef(null);
|
||||
@@ -55,6 +61,8 @@ const CommentEditor = ({
|
||||
const { addDraftContent, getDraftContent, removeDraftContent } = useDraftContent();
|
||||
const captchaSettings = useSelector(selectCaptchaSettings);
|
||||
const isUserLearner = useSelector(selectIsUserLearner);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
const postStatus = useSelector(selectPostStatus);
|
||||
|
||||
const shouldRequireCaptcha = !id && captchaSettings.enabled && isUserLearner;
|
||||
|
||||
@@ -85,6 +93,12 @@ const CommentEditor = ({
|
||||
recaptchaToken: '',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (contentCreationRateLimited) {
|
||||
openRestrictionDialogue();
|
||||
}
|
||||
}, [contentCreationRateLimited]);
|
||||
|
||||
const handleCaptchaChange = useCallback((token, setFieldValue) => {
|
||||
setFieldValue('recaptchaToken', token || '');
|
||||
}, []);
|
||||
@@ -99,8 +113,7 @@ const CommentEditor = ({
|
||||
if (recaptchaRef.current) {
|
||||
recaptchaRef.current.reset();
|
||||
}
|
||||
onCloseEditor();
|
||||
}, [onCloseEditor, initialValues]);
|
||||
}, [initialValues]);
|
||||
|
||||
const deleteEditorContent = useCallback(async () => {
|
||||
const { updatedResponses, updatedComments } = removeDraftContent(parentId, id, threadId);
|
||||
@@ -111,7 +124,14 @@ const CommentEditor = ({
|
||||
}
|
||||
}, [parentId, id, threadId, setDraftComments, setDraftResponses]);
|
||||
|
||||
useEffect(() => {
|
||||
if (postStatus === RequestStatus.SUCCESSFUL && isSubmitting) {
|
||||
onCloseEditor();
|
||||
}
|
||||
}, [postStatus, isSubmitting]);
|
||||
|
||||
const saveUpdatedComment = useCallback(async (values, { resetForm }) => {
|
||||
setIsSubmitting(true);
|
||||
if (id) {
|
||||
const payload = {
|
||||
...values,
|
||||
@@ -259,7 +279,10 @@ const CommentEditor = ({
|
||||
<div className="d-flex py-2 justify-content-end">
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={() => handleCloseEditor(resetForm)}
|
||||
onClick={() => {
|
||||
onCloseEditor();
|
||||
handleCloseEditor(resetForm);
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</Button>
|
||||
@@ -294,6 +317,7 @@ CommentEditor.propTypes = {
|
||||
edit: PropTypes.bool,
|
||||
formClasses: PropTypes.string,
|
||||
onCloseEditor: PropTypes.func.isRequired,
|
||||
openRestrictionDialogue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
CommentEditor.defaultProps = {
|
||||
@@ -308,4 +332,4 @@ CommentEditor.defaultProps = {
|
||||
formClasses: '',
|
||||
};
|
||||
|
||||
export default React.memo(CommentEditor);
|
||||
export default React.memo(withPostingRestrictions(CommentEditor));
|
||||
|
||||
@@ -7,6 +7,7 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
import { ThreadType } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import executeThunk from '../../../test-utils';
|
||||
import { setContentCreationRateLimited } from '../../data/slices';
|
||||
import { getCommentsApiUrl } from './api';
|
||||
import {
|
||||
addComment, editComment, fetchCommentResponses, fetchThreadComments, removeComment,
|
||||
@@ -155,6 +156,17 @@ describe('Comments/Responses data layer tests', () => {
|
||||
.toEqual(parentId);
|
||||
});
|
||||
|
||||
test('successfully dispatches rate limit action on 429 error', async () => {
|
||||
axiosMock.onPost(/comments/).reply(429);
|
||||
|
||||
const dispatch = jest.fn();
|
||||
const thunk = addComment('Test comment', 'thread-123', null, false, 'recaptchaToken');
|
||||
|
||||
await thunk(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(setContentCreationRateLimited());
|
||||
});
|
||||
|
||||
test('successfully handles comment edits', async () => {
|
||||
const threadId = 'test-thread';
|
||||
const commentId = 'comment-1';
|
||||
|
||||
@@ -208,6 +208,7 @@ const commentsSlice = createSlice({
|
||||
[payload.id]: payload,
|
||||
},
|
||||
commentDraft: null,
|
||||
postStatus: RequestStatus.SUCCESSFUL,
|
||||
}),
|
||||
deleteCommentRequest: (state) => (
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { setContentCreationRateLimited } from '../../data/slices';
|
||||
import { getHttpErrorStatus } from '../../utils';
|
||||
import {
|
||||
deleteComment, getCommentResponses, getThreadComments, postComment, updateComment,
|
||||
@@ -155,6 +156,8 @@ export function addComment(comment, threadId, parentId = null, enableInContextSi
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
dispatch(postCommentDenied());
|
||||
} else if (getHttpErrorStatus(error) === 429) {
|
||||
dispatch(setContentCreationRateLimited());
|
||||
} else {
|
||||
dispatch(postCommentFailed());
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { initializeStore } from '../../../store';
|
||||
import executeThunk from '../../../test-utils';
|
||||
import { setContentCreationRateLimited } from '../../data/slices';
|
||||
import { getThreadsApiUrl } from './api';
|
||||
import {
|
||||
createNewThread, fetchThread, fetchThreads, removeThread, updateExistingThread,
|
||||
@@ -152,6 +153,23 @@ describe('Threads/Posts data layer tests', () => {
|
||||
.toEqual(['thread-1']);
|
||||
});
|
||||
|
||||
test('successfully handles 429 rate limit error when creating a thread', async () => {
|
||||
axiosMock.onPost(threadsApiUrl).reply(429);
|
||||
|
||||
const dispatch = jest.fn();
|
||||
const thunk = createNewThread({
|
||||
courseId,
|
||||
topicId: 'test-topic',
|
||||
type: 'discussion',
|
||||
title: 'Rate Limited Thread',
|
||||
content: 'This should trigger rate limit',
|
||||
});
|
||||
|
||||
await thunk(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(setContentCreationRateLimited());
|
||||
});
|
||||
|
||||
test('successfully handles thread updates', async () => {
|
||||
const threadId = 'thread-2';
|
||||
axiosMock.onGet(threadsApiUrl)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { logError } from '@edx/frontend-platform/logging';
|
||||
import {
|
||||
PostsStatusFilter, ThreadType,
|
||||
} from '../../../data/constants';
|
||||
import { setContentCreationRateLimited } from '../../data/slices';
|
||||
import { getHttpErrorStatus } from '../../utils';
|
||||
import {
|
||||
deleteThread, getThread, getThreads, postThread, sendEmailForAccountActivation, updateThread,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
fetchThreadsRequest,
|
||||
fetchThreadsSuccess,
|
||||
fetchThreadSuccess,
|
||||
hidePostEditor,
|
||||
postThreadDenied,
|
||||
postThreadFailed,
|
||||
postThreadRequest,
|
||||
@@ -235,9 +237,12 @@ export function createNewThread({
|
||||
recaptchaToken,
|
||||
}, enableInContextSidebar);
|
||||
dispatch(postThreadSuccess(camelCaseObject(data)));
|
||||
dispatch(hidePostEditor());
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
dispatch(postThreadDenied());
|
||||
} else if (getHttpErrorStatus(error) === 429) {
|
||||
dispatch(setContentCreationRateLimited());
|
||||
} else {
|
||||
dispatch(postThreadFailed());
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import Search from '../../../components/Search';
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import DiscussionContext from '../../common/context';
|
||||
import withEmailConfirmation from '../../common/withEmailConfirmation';
|
||||
import withPostingRestrictions from '../../common/withPostingRestrictions';
|
||||
import { useUserPostingEnabled } from '../../data/hooks';
|
||||
import {
|
||||
selectConfigLoadingStatus,
|
||||
selectContentCreationRateLimited,
|
||||
selectEnableInContext,
|
||||
selectShouldShowEmailConfirmation,
|
||||
} from '../../data/selectors';
|
||||
@@ -27,12 +28,13 @@ import messages from './messages';
|
||||
|
||||
import './actionBar.scss';
|
||||
|
||||
const PostActionsBar = ({ openEmailConfirmation }) => {
|
||||
const PostActionsBar = ({ openRestrictionDialogue }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const loadingStatus = useSelector(selectConfigLoadingStatus);
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
const isUserPrivilegedInPostingRestriction = useUserPostingEnabled();
|
||||
const { enableInContextSidebar, page } = useContext(DiscussionContext);
|
||||
|
||||
@@ -41,8 +43,12 @@ const PostActionsBar = ({ openEmailConfirmation }) => {
|
||||
}, []);
|
||||
|
||||
const handleAddPost = useCallback(() => {
|
||||
if (shouldShowEmailConfirmation) { openEmailConfirmation(); } else { dispatch(showPostEditor()); }
|
||||
}, [shouldShowEmailConfirmation, openEmailConfirmation]);
|
||||
if (shouldShowEmailConfirmation || contentCreationRateLimited) {
|
||||
openRestrictionDialogue();
|
||||
} else {
|
||||
dispatch(showPostEditor());
|
||||
}
|
||||
}, [shouldShowEmailConfirmation, openRestrictionDialogue, contentCreationRateLimited]);
|
||||
|
||||
return (
|
||||
<div className={classNames('d-flex justify-content-end flex-grow-1', { 'py-1': !enableInContextSidebar })}>
|
||||
@@ -91,7 +97,7 @@ const PostActionsBar = ({ openEmailConfirmation }) => {
|
||||
};
|
||||
|
||||
PostActionsBar.propTypes = {
|
||||
openEmailConfirmation: PropTypes.func.isRequired,
|
||||
openRestrictionDialogue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withEmailConfirmation(PostActionsBar));
|
||||
export default React.memo(withPostingRestrictions(PostActionsBar));
|
||||
|
||||
@@ -71,6 +71,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Close',
|
||||
description: 'Close button.',
|
||||
},
|
||||
postLimitTitle: {
|
||||
id: 'discussion.posts.limit.title',
|
||||
defaultMessage: 'Post limit reached',
|
||||
description: 'Confirm email title for unverified users.',
|
||||
},
|
||||
postLimitDescription: {
|
||||
id: 'discussion.posts.limit.description',
|
||||
defaultMessage: 'You’ve reached the current post limit. Please try again later.',
|
||||
description: 'Confirm email description for unverified users.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -25,10 +25,12 @@ import useDispatchWithState from '../../../data/hooks';
|
||||
import selectCourseCohorts from '../../cohorts/data/selectors';
|
||||
import fetchCourseCohorts from '../../cohorts/data/thunks';
|
||||
import DiscussionContext from '../../common/context';
|
||||
import withPostingRestrictions from '../../common/withPostingRestrictions';
|
||||
import { useCurrentDiscussionTopic } from '../../data/hooks';
|
||||
import {
|
||||
selectAnonymousPostingConfig,
|
||||
selectCaptchaSettings,
|
||||
selectContentCreationRateLimited,
|
||||
selectDivisionSettings,
|
||||
selectEnableInContext,
|
||||
selectIsNotifyAllLearnersEnabled,
|
||||
@@ -57,7 +59,7 @@ import messages from './messages';
|
||||
import PostTypeCard from './PostTypeCard';
|
||||
|
||||
const PostEditor = ({
|
||||
editExisting,
|
||||
editExisting, openRestrictionDialogue,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
@@ -88,6 +90,7 @@ const PostEditor = ({
|
||||
const isNotifyAllLearnersEnabled = useSelector(selectIsNotifyAllLearnersEnabled);
|
||||
const captchaSettings = useSelector(selectCaptchaSettings);
|
||||
const isUserLearner = useSelector(selectIsUserLearner);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
|
||||
const canDisplayEditReason = (editExisting
|
||||
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
|
||||
@@ -171,9 +174,14 @@ const PostEditor = ({
|
||||
})(location);
|
||||
navigate({ ...newLocation });
|
||||
}
|
||||
dispatch(hidePostEditor());
|
||||
}, [postId, topicId, post?.author, category, editExisting, commentsPagePath, location]);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentCreationRateLimited) {
|
||||
openRestrictionDialogue();
|
||||
}
|
||||
}, [contentCreationRateLimited]);
|
||||
|
||||
// null stands for no cohort restriction ("All learners" option)
|
||||
const selectedCohort = useCallback(
|
||||
(cohort) => (
|
||||
@@ -543,7 +551,10 @@ const PostEditor = ({
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={() => hideEditor(resetForm)}
|
||||
onClick={() => {
|
||||
dispatch(hidePostEditor());
|
||||
hideEditor(resetForm);
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</Button>
|
||||
@@ -566,10 +577,11 @@ const PostEditor = ({
|
||||
|
||||
PostEditor.propTypes = {
|
||||
editExisting: PropTypes.bool,
|
||||
openRestrictionDialogue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
PostEditor.defaultProps = {
|
||||
editExisting: false,
|
||||
};
|
||||
|
||||
export default React.memo(PostEditor);
|
||||
export default React.memo(withPostingRestrictions(PostEditor));
|
||||
|
||||
@@ -624,5 +624,28 @@ describe('PostEditor', () => {
|
||||
expect(container.querySelector('[data-testid="hide-help-button"]')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should open the rate limit dialogue.', async () => {
|
||||
axiosMock.onPost(threadsApiUrl).reply(429);
|
||||
|
||||
await renderComponent();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('topic-select'), {
|
||||
target: { value: 'ncw-topic-1' },
|
||||
});
|
||||
const postTitle = await screen.findByTestId('post-title-input');
|
||||
const tinymceEditor = await screen.findByTestId('tinymce-editor');
|
||||
|
||||
fireEvent.change(postTitle, { target: { value: 'Test Post Title' } });
|
||||
fireEvent.change(tinymceEditor, { target: { value: 'Test Post Content' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Post limit reached')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,9 +16,9 @@ import { selectorForUnitSubsection, selectTopicContext } from '../../../data/sel
|
||||
import { AlertBanner, Confirmation } from '../../common';
|
||||
import DiscussionContext from '../../common/context';
|
||||
import HoverCard from '../../common/HoverCard';
|
||||
import withEmailConfirmation from '../../common/withEmailConfirmation';
|
||||
import withPostingRestrictions from '../../common/withPostingRestrictions';
|
||||
import { ContentTypes } from '../../data/constants';
|
||||
import { selectShouldShowEmailConfirmation, selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import { selectContentCreationRateLimited, selectShouldShowEmailConfirmation, selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import { selectTopic } from '../../topics/data/selectors';
|
||||
import { truncatePath } from '../../utils';
|
||||
import { selectThread } from '../data/selectors';
|
||||
@@ -28,7 +28,7 @@ import messages from './messages';
|
||||
import PostFooter from './PostFooter';
|
||||
import PostHeader from './PostHeader';
|
||||
|
||||
const Post = ({ handleAddResponseButton, openEmailConfirmation }) => {
|
||||
const Post = ({ handleAddResponseButton, openRestrictionDialogue }) => {
|
||||
const { enableInContextSidebar, postId } = useContext(DiscussionContext);
|
||||
const {
|
||||
topicId, abuseFlagged, closed, pinned, voted, hasEndorsed, following, closedBy, voteCount, groupId, groupName,
|
||||
@@ -48,6 +48,7 @@ const Post = ({ handleAddResponseButton, openEmailConfirmation }) => {
|
||||
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const shouldShowEmailConfirmation = useSelector(selectShouldShowEmailConfirmation);
|
||||
const contentCreationRateLimited = useSelector(selectContentCreationRateLimited);
|
||||
|
||||
const displayPostFooter = following || voteCount || closed || (groupId && userHasModerationPrivileges);
|
||||
|
||||
@@ -158,7 +159,8 @@ const Post = ({ handleAddResponseButton, openEmailConfirmation }) => {
|
||||
id={postId}
|
||||
contentType={ContentTypes.POST}
|
||||
actionHandlers={actionHandlers}
|
||||
handleResponseCommentButton={shouldShowEmailConfirmation ? openEmailConfirmation : handleAddResponseButton}
|
||||
handleResponseCommentButton={shouldShowEmailConfirmation || contentCreationRateLimited
|
||||
? openRestrictionDialogue : handleAddResponseButton}
|
||||
addResponseCommentButtonMessage={intl.formatMessage(messages.addResponse)}
|
||||
onLike={handlePostLike}
|
||||
onFollow={handlePostFollow}
|
||||
@@ -238,7 +240,7 @@ const Post = ({ handleAddResponseButton, openEmailConfirmation }) => {
|
||||
|
||||
Post.propTypes = {
|
||||
handleAddResponseButton: PropTypes.func.isRequired,
|
||||
openEmailConfirmation: PropTypes.func.isRequired,
|
||||
openRestrictionDialogue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(withEmailConfirmation(Post));
|
||||
export default React.memo(withPostingRestrictions(Post));
|
||||
|
||||
Reference in New Issue
Block a user