feat: integrated backend discussions restriction with MFE (#529)
* feat: integrated backend discussions restriction with MFE * test: fixes failed test cases * refactor: fixed lint issues --------- Co-authored-by: ayesha waris <73840786+ayeshoali@users.noreply.github.com> Co-authored-by: SundasNoreen <sundas.noreen@arbisoft.com>
This commit is contained in:
@@ -13,9 +13,9 @@ import {
|
||||
import { MoreHoriz } from '@edx/paragon/icons';
|
||||
|
||||
import { ContentActions } from '../../data/constants';
|
||||
import { selectBlackoutDate } from '../data/selectors';
|
||||
import { selectIsPostingEnabled } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { inBlackoutDateRange, useActions } from '../utils';
|
||||
import { useActions } from '../utils';
|
||||
|
||||
const ActionsDropdown = ({
|
||||
actionHandlers,
|
||||
@@ -29,7 +29,7 @@ const ActionsDropdown = ({
|
||||
const intl = useIntl();
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const actions = useActions(contentType, id);
|
||||
|
||||
const handleActions = useCallback((action) => {
|
||||
@@ -41,12 +41,12 @@ const ActionsDropdown = ({
|
||||
}
|
||||
}, [actionHandlers]);
|
||||
|
||||
// Find and remove edit action if in blackout date range.
|
||||
// Find and remove edit action if in Posting is disabled.
|
||||
useMemo(() => {
|
||||
if (inBlackoutDateRange(blackoutDateRange)) {
|
||||
if (!isPostingEnabled) {
|
||||
actions.splice(actions.findIndex(action => action.id === 'edit'), 1);
|
||||
}
|
||||
}, [actions, blackoutDateRange]);
|
||||
}, [actions, isPostingEnabled]);
|
||||
|
||||
const onClickButton = useCallback(() => {
|
||||
setTarget(buttonRef.current);
|
||||
|
||||
@@ -13,6 +13,8 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { ContentActions } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import messages from '../messages';
|
||||
import { getCommentsApiUrl } from '../post-comments/data/api';
|
||||
import { addComment, fetchThreadComments } from '../post-comments/data/thunks';
|
||||
@@ -29,6 +31,7 @@ let store;
|
||||
let axiosMock;
|
||||
const commentsApiUrl = getCommentsApiUrl();
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const discussionThreadId = 'thread-1';
|
||||
const questionThreadId = 'thread-2';
|
||||
const commentContent = 'This is a comment for thread-1';
|
||||
@@ -170,7 +173,7 @@ const findOpenActionsDropdownButton = async () => (
|
||||
);
|
||||
|
||||
describe('ActionsDropdown', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
@@ -182,6 +185,11 @@ describe('ActionsDropdown', () => {
|
||||
store = initializeStore();
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
|
||||
axiosMock.onGet(`${getCourseConfigApiUrl()}${courseId}/`)
|
||||
.reply(200, { isPostingEnabled: true });
|
||||
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
});
|
||||
|
||||
it.each(Object.values(buildTestContent()))('can open drop down if enabled', async (commentOrPost) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import {
|
||||
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
|
||||
} from '../../components/icons';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../data/hooks';
|
||||
import { useUserPostingEnabled } from '../data/hooks';
|
||||
import { PostCommentsContext } from '../post-comments/postCommentsContext';
|
||||
import ActionsDropdown from './ActionsDropdown';
|
||||
import { DiscussionContext } from './context';
|
||||
@@ -31,7 +31,7 @@ const HoverCard = ({
|
||||
const intl = useIntl();
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const { isClosed } = useContext(PostCommentsContext);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -39,7 +39,7 @@ const HoverCard = ({
|
||||
data-testid={`hover-card-${id}`}
|
||||
id={`hover-card-${id}`}
|
||||
>
|
||||
{userCanAddThreadInBlackoutDate && (
|
||||
{isUserPrivilagedInPostingRestriction && (
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
|
||||
@@ -12,6 +12,8 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getCommentsApiUrl } from '../post-comments/data/api';
|
||||
import { fetchCommentResponses, fetchThreadComments } from '../post-comments/data/thunks';
|
||||
@@ -118,6 +120,7 @@ describe('HoverCard', () => {
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
isPostingEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -142,7 +145,10 @@ describe('HoverCard', () => {
|
||||
thread_id: threadId,
|
||||
})];
|
||||
});
|
||||
axiosMock.onGet(`${getCourseConfigApiUrl()}${courseId}/`)
|
||||
.reply(200, { isPostingEnabled: true });
|
||||
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||
await mockAxiosReturnPagedComments();
|
||||
await mockAxiosReturnPagedCommentsResponses();
|
||||
|
||||
@@ -22,13 +22,13 @@ import tourCheckpoints from '../tours/constants';
|
||||
import { selectTours } from '../tours/data/selectors';
|
||||
import { updateTourShowStatus } from '../tours/data/thunks';
|
||||
import messages from '../tours/messages';
|
||||
import { discussionsPath, inBlackoutDateRange } from '../utils';
|
||||
import { discussionsPath } from '../utils';
|
||||
import {
|
||||
selectAreThreadsFiltered,
|
||||
selectBlackoutDate,
|
||||
selectEnableInContext,
|
||||
selectIsCourseAdmin,
|
||||
selectIsCourseStaff,
|
||||
selectIsPostingEnabled,
|
||||
selectLearnersTabEnabled,
|
||||
selectModerationSettings,
|
||||
selectPostThreadCount,
|
||||
@@ -192,17 +192,16 @@ export const useCurrentDiscussionTopic = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useUserCanAddThreadInBlackoutDate = () => {
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
export const useUserPostingEnabled = () => {
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const isUserAdmin = useSelector(selectUserIsStaff);
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const isUserGroupTA = useSelector(selectUserIsGroupTa);
|
||||
const isCourseAdmin = useSelector(selectIsCourseAdmin);
|
||||
const isCourseStaff = useSelector(selectIsCourseStaff);
|
||||
const isPrivileged = isUserAdmin || userHasModerationPrivileges || isUserGroupTA || isCourseAdmin || isCourseStaff;
|
||||
const isInBlackoutDateRange = useMemo(() => inBlackoutDateRange(blackoutDateRange), [blackoutDateRange]);
|
||||
|
||||
return (!(isInBlackoutDateRange) || (isPrivileged));
|
||||
return (isPostingEnabled || isPrivileged);
|
||||
};
|
||||
|
||||
function camelToConstant(string) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { getCourseConfigApiUrl } from './api';
|
||||
import { useCurrentDiscussionTopic, useUserCanAddThreadInBlackoutDate } from './hooks';
|
||||
import { useCurrentDiscussionTopic, useUserPostingEnabled } from './hooks';
|
||||
import { fetchCourseConfig } from './thunks';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
@@ -19,8 +19,8 @@ const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
const generateApiResponse = (blackouts = [], isCourseAdmin = false) => ({
|
||||
blackouts,
|
||||
const generateApiResponse = (isPostingEnabled, isCourseAdmin = false) => ({
|
||||
isPostingEnabled,
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isCourseAdmin,
|
||||
@@ -102,12 +102,12 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUserCanAddThreadInBlackoutDate', () => {
|
||||
describe('useUserPostingEnabled', () => {
|
||||
const ComponentWithHook = () => {
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
return (
|
||||
<div>
|
||||
{String(userCanAddThreadInBlackoutDate)}
|
||||
{String(isUserPrivilagedInPostingRestriction)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -121,7 +121,7 @@ describe('Hooks', () => {
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
describe('User can add Thread in blackoutdates ', () => {
|
||||
describe('User can add Thread in Posting Restrictions ', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -136,37 +136,34 @@ describe('Hooks', () => {
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
test('when blackoutdates are not active and Role is Learner return true', async () => {
|
||||
test('when posting is not disabled and Role is Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse([], false));
|
||||
.reply(200, generateApiResponse(true, false));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when blackoutdates are not active and Role is not Learner return true', async () => {
|
||||
test('when posting is not disabled and Role is not Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse([], true));
|
||||
.reply(200, generateApiResponse(true, true));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when blackoutdates are active and Role is Learner return false', async () => {
|
||||
test('when posting is disabled and Role is Learner return false', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse([{
|
||||
start: '2022-11-25T00:00:00Z',
|
||||
end: '2050-11-25T23:59:00Z',
|
||||
}], false));
|
||||
.reply(200, generateApiResponse(false, false));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('false')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when blackoutdates are active and Role is not Learner return true', async () => {
|
||||
test('when posting is not disabled and Role is not Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse([
|
||||
{ start: '2022-11-25T00:00:00Z', end: '2050-11-25T23:59:00Z' }], true));
|
||||
.reply(200, generateApiResponse(false, true));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -20,8 +20,6 @@ export const selectUserRoles = state => state.config.userRoles;
|
||||
|
||||
export const selectDivisionSettings = state => state.config.settings;
|
||||
|
||||
export const selectBlackoutDate = state => state.config.blackouts;
|
||||
|
||||
export const selectGroupAtSubsection = state => state.config.groupAtSubsection;
|
||||
|
||||
export const selectIsCourseAdmin = state => state.config.isCourseAdmin;
|
||||
@@ -30,6 +28,8 @@ export const selectIsCourseStaff = state => state.config.isCourseStaff;
|
||||
|
||||
export const selectEnableInContext = state => state.config.enableInContext;
|
||||
|
||||
export const selectIsPostingEnabled = state => state.config.isPostingEnabled;
|
||||
|
||||
export const selectModerationSettings = state => ({
|
||||
postCloseReasons: state.config.postCloseReasons,
|
||||
editReasons: state.config.editReasons,
|
||||
|
||||
@@ -7,7 +7,6 @@ const configSlice = createSlice({
|
||||
name: 'config',
|
||||
initialState: {
|
||||
status: RequestStatus.IN_PROGRESS,
|
||||
blackouts: [],
|
||||
allowAnonymous: false,
|
||||
allowAnonymousToPeers: false,
|
||||
userRoles: [],
|
||||
@@ -18,6 +17,7 @@ const configSlice = createSlice({
|
||||
isCourseStaff: false,
|
||||
isUserAdmin: false,
|
||||
learnersTabEnabled: false,
|
||||
isPostingEnabled: false,
|
||||
settings: {
|
||||
divisionScheme: 'none',
|
||||
alwaysDivideInlineDiscussions: false,
|
||||
|
||||
@@ -30,7 +30,7 @@ const CourseTabsNavigation = lazy(() => import('../../components/NavigationBar/C
|
||||
const LegacyBreadcrumbMenu = lazy(() => import('../navigation/breadcrumb-menu/LegacyBreadcrumbMenu'));
|
||||
const NavigationBar = lazy(() => import('../navigation/navigation-bar/NavigationBar'));
|
||||
const DiscussionsProductTour = lazy(() => import('../tours/DiscussionsProductTour'));
|
||||
const BlackoutInformationBanner = lazy(() => import('./BlackoutInformationBanner'));
|
||||
const DiscussionsRestrictionBanner = lazy(() => import('./DiscussionsRestrictionBanner'));
|
||||
const DiscussionContent = lazy(() => import('./DiscussionContent'));
|
||||
const DiscussionSidebar = lazy(() => import('./DiscussionSidebar'));
|
||||
const InformationBanner = lazy(() => import('./InformationBanner'));
|
||||
@@ -96,7 +96,7 @@ const DiscussionsHome = () => {
|
||||
<PostActionsBar />
|
||||
</div>
|
||||
{isFeedbackBannerVisible && <InformationBanner />}
|
||||
<BlackoutInformationBanner />
|
||||
<DiscussionsRestrictionBanner />
|
||||
</div>
|
||||
{provider === DiscussionProvider.LEGACY && (
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { PageBanner } from '@edx/paragon';
|
||||
|
||||
import { selectBlackoutDate } from '../data/selectors';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { selectconfigLoadingStatus, selectIsPostingEnabled } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { inBlackoutDateRange } from '../utils';
|
||||
|
||||
const BlackoutInformationBanner = () => {
|
||||
const DiscussionsRestrictionBanner = () => {
|
||||
const intl = useIntl();
|
||||
const blackoutDate = useSelector(selectBlackoutDate);
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const configLoadingStatus = useSelector(selectconfigLoadingStatus);
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
|
||||
const isDiscussionsBlackout = useMemo(() => (
|
||||
inBlackoutDateRange(blackoutDate)
|
||||
), [blackoutDate]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setShowBanner(false);
|
||||
}, []);
|
||||
@@ -25,7 +22,7 @@ const BlackoutInformationBanner = () => {
|
||||
return (
|
||||
<PageBanner
|
||||
variant="accentB"
|
||||
show={isDiscussionsBlackout && showBanner}
|
||||
show={!isPostingEnabled && showBanner && configLoadingStatus === RequestStatus.SUCCESSFUL}
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
@@ -36,4 +33,4 @@ const BlackoutInformationBanner = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BlackoutInformationBanner;
|
||||
export default DiscussionsRestrictionBanner;
|
||||
@@ -8,7 +8,7 @@ import { initializeStore } from '../../store';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { fetchConfigSuccess } from '../data/slices';
|
||||
import messages from '../messages';
|
||||
import BlackoutInformationBanner from './BlackoutInformationBanner';
|
||||
import DiscussionsRestrictionBanner from './DiscussionsRestrictionBanner';
|
||||
|
||||
let store;
|
||||
let container;
|
||||
@@ -20,13 +20,13 @@ activeEndDate.setDate(activeEndDate.getDate() + 2);
|
||||
activeStartDate = activeStartDate.toISOString();
|
||||
activeEndDate = activeEndDate.toISOString();
|
||||
|
||||
const getConfigData = (blackouts = []) => ({
|
||||
const getConfigData = (isPostingEnabled) => ({
|
||||
id: 'course-v1:edX+DemoX+Demo_Course',
|
||||
userRoles: ['Admin', 'Student'],
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isUserAdmin: false,
|
||||
blackouts,
|
||||
isPostingEnabled,
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
@@ -34,7 +34,7 @@ function renderComponent() {
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<BlackoutInformationBanner />
|
||||
<DiscussionsRestrictionBanner />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
@@ -43,7 +43,7 @@ function renderComponent() {
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('Blackout Information Banner', () => {
|
||||
describe('Discussions Restriction Banner', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -56,13 +56,11 @@ describe('Blackout Information Banner', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ blackouts: [], visibility: false },
|
||||
{ blackouts: ['2021-12-31T10:15', '2021-12-31T10:20'], visibility: false },
|
||||
{ blackouts: [{ start: activeStartDate, end: activeEndDate }], visibility: true },
|
||||
{ blackouts: [{ start: activeEndDate, end: activeEndDate }], visibility: false },
|
||||
])('Test Blackout Banner is visible on app load if blackout date is active', async ({ blackouts, visibility }) => {
|
||||
{ isPostingEnabled: false, visibility: true },
|
||||
{ isPostingEnabled: true, visibility: false },
|
||||
])('Test Discussions Restriction is visible on app load if posting is disabled', async ({ isPostingEnabled, visibility }) => {
|
||||
store = initializeStore();
|
||||
await store.dispatch(fetchConfigSuccess(getConfigData(blackouts)));
|
||||
await store.dispatch(fetchConfigSuccess(getConfigData(isPostingEnabled)));
|
||||
renderComponent();
|
||||
if (visibility) {
|
||||
const element = await screen.findByRole('alert');
|
||||
@@ -92,7 +92,6 @@ describe('InContext Topic Posts View', () => {
|
||||
enableInContext: true,
|
||||
provider: 'openedx',
|
||||
hasModerationPrivileges: true,
|
||||
blackouts: [],
|
||||
},
|
||||
});
|
||||
Factory.resetAll();
|
||||
|
||||
@@ -185,7 +185,7 @@ const messages = defineMessages({
|
||||
},
|
||||
blackoutDiscussionInformation: {
|
||||
id: 'discussion.blackoutBanner.information',
|
||||
defaultMessage: 'Posting in discussions is temporarily disabled by the course team',
|
||||
defaultMessage: 'Posting in discussions is disabled by the course team',
|
||||
description: 'Informative text when discussion posting is disabled',
|
||||
},
|
||||
imageWarningMessage: {
|
||||
|
||||
@@ -109,6 +109,24 @@ async function getThreadAPIResponse(attr = null) {
|
||||
await executeThunk(fetchThread(discussionPostId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
async function setupCourseConfig(reasonCodesEnabled = true) {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
has_moderation_privileges: true,
|
||||
isPostingEnabled: true,
|
||||
reason_codes_enabled: reasonCodesEnabled,
|
||||
editReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
postCloseReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
function renderComponent(postId) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
@@ -218,6 +236,7 @@ describe('ThreadView', () => {
|
||||
});
|
||||
|
||||
it('should show and hide the editor', async () => {
|
||||
await setupCourseConfig();
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const post = screen.getByTestId('post-thread-1');
|
||||
@@ -232,6 +251,7 @@ describe('ThreadView', () => {
|
||||
});
|
||||
|
||||
it('should allow posting a response', async () => {
|
||||
await setupCourseConfig();
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
@@ -246,6 +266,7 @@ describe('ThreadView', () => {
|
||||
});
|
||||
|
||||
it('should not allow posting a response on a closed post', async () => {
|
||||
await setupCourseConfig();
|
||||
renderComponent(closedPostId);
|
||||
const post = screen.getByTestId('post-thread-2');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-2');
|
||||
@@ -254,6 +275,7 @@ describe('ThreadView', () => {
|
||||
});
|
||||
|
||||
it('should allow posting a comment', async () => {
|
||||
await setupCourseConfig();
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
@@ -267,6 +289,7 @@ describe('ThreadView', () => {
|
||||
});
|
||||
|
||||
it('should allow editing an existing comment', async () => {
|
||||
await setupCourseConfig();
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
@@ -281,23 +304,6 @@ describe('ThreadView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
async function setupCourseConfig(reasonCodesEnabled = true) {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
has_moderation_privileges: true,
|
||||
reason_codes_enabled: reasonCodesEnabled,
|
||||
editReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
postCloseReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
it('should show reason codes when closing a post', async () => {
|
||||
await setupCourseConfig();
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Spinner } from '@edx/paragon';
|
||||
|
||||
import { EndorsementStatus } from '../../../data/constants';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { isLastElementOfList } from '../../utils';
|
||||
import { usePostComments } from '../data/hooks';
|
||||
import messages from '../messages';
|
||||
@@ -16,7 +16,8 @@ const CommentsView = ({ endorsed }) => {
|
||||
const intl = useIntl();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const { isClosed } = useContext(PostCommentsContext);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
|
||||
const {
|
||||
endorsedCommentsIds,
|
||||
unEndorsedCommentsIds,
|
||||
@@ -89,7 +90,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
{handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)}
|
||||
{unEndorsedCommentsIds.length === 0 && <br />}
|
||||
{handleComments(unEndorsedCommentsIds, false)}
|
||||
{(userCanAddThreadInBlackoutDate && !!unEndorsedCommentsIds.length && !isClosed) && (
|
||||
{(isUserPrivilagedInPostingRestriction && !!unEndorsedCommentsIds.length && !isClosed) && (
|
||||
<div className="mx-4">
|
||||
{!addingResponse && (
|
||||
<Button
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../../common'
|
||||
import { DiscussionContext } from '../../../common/context';
|
||||
import HoverCard from '../../../common/HoverCard';
|
||||
import { ContentTypes } from '../../../data/constants';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../../data/hooks';
|
||||
import { useUserPostingEnabled } from '../../../data/hooks';
|
||||
import { fetchThread } from '../../../posts/data/thunks';
|
||||
import LikeButton from '../../../posts/post/LikeButton';
|
||||
import { useActions } from '../../../utils';
|
||||
@@ -61,7 +61,7 @@ const Comment = ({
|
||||
const currentPage = useSelector(selectCommentCurrentPage(id));
|
||||
const sortedOrder = useSelector(selectCommentSortOrder);
|
||||
const actions = useActions(ContentTypes.COMMENT, id);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
|
||||
useEffect(() => {
|
||||
// If the comment has a parent comment, it won't have any children, so don't fetch them.
|
||||
@@ -119,10 +119,10 @@ const Comment = ({
|
||||
), [id, currentPage, sortedOrder]);
|
||||
|
||||
const handleAddCommentButton = useCallback(() => {
|
||||
if (userCanAddThreadInBlackoutDate) {
|
||||
if (isUserPrivilagedInPostingRestriction) {
|
||||
setReplying(true);
|
||||
}
|
||||
}, [userCanAddThreadInBlackoutDate]);
|
||||
}, [isUserPrivilagedInPostingRestriction]);
|
||||
|
||||
const handleCommentLike = useCallback(async () => {
|
||||
await dispatch(editComment(id, { voted: !voted }));
|
||||
@@ -265,7 +265,7 @@ const Comment = ({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
!isClosed && userCanAddThreadInBlackoutDate && (inlineReplies.length >= 5) && (
|
||||
!isClosed && isUserPrivilagedInPostingRestriction && (inlineReplies.length >= 5) && (
|
||||
<Button
|
||||
className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500"
|
||||
variant="plain"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Close } from '@edx/paragon/icons';
|
||||
import Search from '../../../components/Search';
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { selectconfigLoadingStatus, selectEnableInContext } from '../../data/selectors';
|
||||
import { TopicSearchBar as IncontextSearch } from '../../in-context-topics/topic-search';
|
||||
import { postMessageToParent } from '../../utils';
|
||||
@@ -26,7 +26,7 @@ const PostActionsBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const loadingStatus = useSelector(selectconfigLoadingStatus);
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
const { enableInContextSidebar, page } = useContext(DiscussionContext);
|
||||
|
||||
const handleCloseInContext = useCallback(() => {
|
||||
@@ -49,7 +49,7 @@ const PostActionsBar = () => {
|
||||
{intl.formatMessage(messages.title)}
|
||||
</h4>
|
||||
)}
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && userCanAddThreadInBlackoutDate && (
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && isUserPrivilagedInPostingRestriction && (
|
||||
<>
|
||||
{!enableInContextSidebar && <div className="border-right border-light-400 mx-3" />}
|
||||
<Button
|
||||
|
||||
@@ -267,28 +267,6 @@ export const filterPosts = (posts, filterBy) => uniqBy(posts, 'id').filter(
|
||||
post => (filterBy.startsWith('un') ? !post[filterBy.slice(2)] : post[filterBy]),
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to make a check if date is in given range
|
||||
* @param {Date} date this date to be checked in range
|
||||
* @param {Date} start start date
|
||||
* @param {Date} end end date
|
||||
*/
|
||||
export function dateInDateRange(date, start, end) {
|
||||
return date >= start && date <= end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to make a check if date is in given range
|
||||
* @param {array} blackoutDateRanges start date
|
||||
* @return Boolean
|
||||
*/
|
||||
export function inBlackoutDateRange(blackoutDateRanges) {
|
||||
const now = new Date();
|
||||
return blackoutDateRanges.some(
|
||||
(blackoutDateRange) => dateInDateRange(now, new Date(blackoutDateRange.start), new Date(blackoutDateRange.end)),
|
||||
);
|
||||
}
|
||||
|
||||
export function handleKeyDown(event) {
|
||||
const { key } = event;
|
||||
if (key !== 'ArrowDown' && key !== 'ArrowUp') { return; }
|
||||
|
||||
Reference in New Issue
Block a user