Co-authored-by: Mashal Malik <107556986+Mashal-m@users.noreply.github.com>
This commit is contained in:
@@ -162,10 +162,12 @@ describe('DiscussionsSettings', () => {
|
||||
|
||||
expect(queryByTestId(container, 'appConfigForm')).toBeInTheDocument();
|
||||
|
||||
userEvent.click(queryByText(container, appMessages.backButton.defaultMessage));
|
||||
await act(() => userEvent.click(queryByText(container, appMessages.backButton.defaultMessage)));
|
||||
|
||||
expect(queryByTestId(container, 'appList')).toBeInTheDocument();
|
||||
expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId(container, 'appList')).toBeInTheDocument();
|
||||
expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('successfully closes the modal', async () => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, {
|
||||
useCallback, useEffect, useMemo, useState, useContext,
|
||||
} from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { CardGrid, Container, breakpoints } from '@edx/paragon';
|
||||
import {
|
||||
CardGrid, Container, breakpoints, Form, ActionRow, AlertModal, Button,
|
||||
} from '@edx/paragon';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Responsive from 'react-responsive';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
@@ -14,16 +18,23 @@ import messages from './messages';
|
||||
import FeaturesTable from './FeaturesTable';
|
||||
import AppListNextButton from './AppListNextButton';
|
||||
import Loading from '../../../generic/Loading';
|
||||
import useIsOnSmallScreen from '../data/hook';
|
||||
import { saveProviderConfig, fetchDiscussionSettings } from '../data/thunks';
|
||||
import { PagesAndResourcesContext } from '../../PagesAndResourcesProvider';
|
||||
import { discussionRestriction } from '../data/constants';
|
||||
|
||||
const AppList = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { courseId } = useContext(PagesAndResourcesContext);
|
||||
const {
|
||||
appIds, featureIds, status, activeAppId, selectedAppId,
|
||||
appIds, featureIds, status, activeAppId, selectedAppId, enabled, postingRestrictions,
|
||||
} = useSelector(state => state.discussions);
|
||||
const [discussionEnabled, setDiscussionEnabled] = useState(enabled);
|
||||
const apps = useModels('apps', appIds);
|
||||
const features = useModels('features', featureIds);
|
||||
const isGlobalStaff = getAuthenticatedUser().administrator;
|
||||
const ltiProvider = !['openedx', 'legacy'].includes(activeAppId);
|
||||
const isOnSmallcreen = useIsOnSmallScreen();
|
||||
|
||||
const showOneEdxProvider = useMemo(() => apps.filter(app => (
|
||||
activeAppId === 'openedx' ? app.id !== 'legacy' : app.id !== 'openedx'
|
||||
@@ -42,9 +53,48 @@ const AppList = ({ intl }) => {
|
||||
dispatch(updateValidationStatus({ hasError: false }));
|
||||
}, [selectedAppId, activeAppId]);
|
||||
|
||||
useEffect(() => {
|
||||
setDiscussionEnabled(enabled);
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!postingRestrictions) {
|
||||
dispatch(fetchDiscussionSettings(courseId, selectedAppId));
|
||||
}
|
||||
}, [courseId]);
|
||||
|
||||
const handleSelectApp = useCallback((appId) => {
|
||||
dispatch(selectApp({ appId }));
|
||||
}, [selectedAppId]);
|
||||
}, []);
|
||||
|
||||
const updateSettings = useCallback((enabledDiscussion) => {
|
||||
dispatch(saveProviderConfig(
|
||||
courseId,
|
||||
selectedAppId,
|
||||
{
|
||||
enabled: enabledDiscussion,
|
||||
postingRestrictions:
|
||||
enabledDiscussion ? postingRestrictions : discussionRestriction.ENABLED,
|
||||
},
|
||||
));
|
||||
}, [courseId, selectedAppId, postingRestrictions]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setDiscussionEnabled(enabled);
|
||||
}, [enabled]);
|
||||
|
||||
const handleOk = useCallback(() => {
|
||||
setDiscussionEnabled(false);
|
||||
updateSettings(false);
|
||||
}, [updateSettings]);
|
||||
|
||||
const handleChange = useCallback((e) => {
|
||||
const toggleVal = e.target.checked;
|
||||
setDiscussionEnabled(!toggleVal);
|
||||
if (!toggleVal) {
|
||||
updateSettings(!toggleVal);
|
||||
}
|
||||
}, [updateSettings]);
|
||||
|
||||
if (!selectedAppId || status === LOADING) {
|
||||
return (
|
||||
@@ -71,10 +121,21 @@ const AppList = ({ intl }) => {
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="my-sm-5 m-1" data-testid="appList">
|
||||
<h3 className="my-sm-5 my-4">
|
||||
{intl.formatMessage(messages.heading)}
|
||||
</h3>
|
||||
<div className="my-sm-4" data-testid="appList">
|
||||
<div className={!isOnSmallcreen ? 'd-flex flex-row justify-content-between align-items-center' : 'mb-4'}>
|
||||
<h3 className={isOnSmallcreen ? 'mb-3' : 'm-0'}>
|
||||
{intl.formatMessage(messages.heading)}
|
||||
</h3>
|
||||
<Form.Switch
|
||||
floatLabelLeft
|
||||
className="text-primary-500 align-items-center"
|
||||
labelClassName="line-height-24"
|
||||
onChange={handleChange}
|
||||
checked={!discussionEnabled}
|
||||
>
|
||||
Hide discussion tab
|
||||
</Form.Switch>
|
||||
</div>
|
||||
<CardGrid
|
||||
columnSizes={{
|
||||
xs: 12,
|
||||
@@ -82,6 +143,7 @@ const AppList = ({ intl }) => {
|
||||
lg: 4,
|
||||
xl: 4,
|
||||
}}
|
||||
className={!isOnSmallcreen && 'mt-5'}
|
||||
>
|
||||
{(isGlobalStaff || ltiProvider) ? showAppCard(apps) : showAppCard(showOneEdxProvider)}
|
||||
</CardGrid>
|
||||
@@ -96,6 +158,23 @@ const AppList = ({ intl }) => {
|
||||
/>
|
||||
</div>
|
||||
</Responsive>
|
||||
<AlertModal
|
||||
title={intl.formatMessage(messages.hideDiscussionTabTitle)}
|
||||
isOpen={enabled && !discussionEnabled}
|
||||
onClose={handleClose}
|
||||
isBlocking
|
||||
className="hide-discussion-modal"
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="link" className="text-decoration-none bg-black" onClick={handleClose}>Cancel</Button>
|
||||
<Button variant="primary" className="bg-primary-500 ml-1 rounded-0" onClick={handleOk}>OK</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<p className="bg-black">
|
||||
{intl.formatMessage(messages.hideDiscussionTabMessage)}
|
||||
</p>
|
||||
</AlertModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,10 +46,39 @@
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.line-height-24 {
|
||||
line-height: 24px !important;
|
||||
}
|
||||
|
||||
.hide-discussion-modal {
|
||||
.pgn__modal-header {
|
||||
padding-top: 24px;
|
||||
|
||||
h2 {
|
||||
color: $primary-500;
|
||||
line-height: 28px;
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-black {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.pgn__modal-footer {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-restriction {
|
||||
.unselected-button {
|
||||
&:hover {
|
||||
background: #E9E6E4 !important;
|
||||
background: #E9E6E4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React from 'react';
|
||||
import {
|
||||
render, screen, within, queryAllByRole,
|
||||
render, screen, within, queryAllByRole, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
@@ -69,36 +69,47 @@ describe('AppList', () => {
|
||||
|
||||
test('display a card for each available app', async () => {
|
||||
renderComponent();
|
||||
const appCount = store.getState().discussions.appIds.length;
|
||||
expect(screen.queryAllByRole('radio')).toHaveLength(appCount);
|
||||
|
||||
await waitFor(async () => {
|
||||
const appCount = await store.getState().discussions.appIds.length;
|
||||
expect(screen.queryAllByRole('radio')).toHaveLength(appCount);
|
||||
});
|
||||
});
|
||||
|
||||
test('displays the FeaturesTable at desktop sizes', async () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByRole('table')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.queryByRole('table')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
test('hides the FeaturesTable at mobile sizes', async () => {
|
||||
renderComponent(breakpoints.extraSmall.maxWidth);
|
||||
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.queryByRole('table')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
test('hides the FeaturesList at desktop sizes', async () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByText(messages['supportedFeatureList-mobile-show'].defaultMessage)).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.queryByText(messages['supportedFeatureList-mobile-show'].defaultMessage))
|
||||
.not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
test('displays the FeaturesList at mobile sizes', async () => {
|
||||
renderComponent(breakpoints.extraSmall.maxWidth);
|
||||
const appCount = store.getState().discussions.appIds.length;
|
||||
expect(screen.queryAllByText(messages['supportedFeatureList-mobile-show'].defaultMessage)).toHaveLength(appCount);
|
||||
|
||||
await waitFor(async () => {
|
||||
const appCount = await store.getState().discussions.appIds.length;
|
||||
expect(screen.queryAllByText(messages['supportedFeatureList-mobile-show'].defaultMessage))
|
||||
.toHaveLength(appCount);
|
||||
});
|
||||
});
|
||||
|
||||
test('selectApp is called when an app is clicked', async () => {
|
||||
renderComponent();
|
||||
userEvent.click(screen.getByLabelText('Select Piazza'));
|
||||
const clickedCard = screen.getByRole('radio', { checked: true });
|
||||
expect(within(clickedCard).queryByLabelText('Select Piazza')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(screen.getByLabelText('Select Piazza'));
|
||||
const clickedCard = screen.getByRole('radio', { checked: true });
|
||||
expect(within(clickedCard).queryByLabelText('Select Piazza')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,7 +132,7 @@ describe('AppList', () => {
|
||||
test('does not display two edx providers card for non admin role', async () => {
|
||||
renderComponent();
|
||||
const appCount = store.getState().discussions.appIds.length;
|
||||
expect(queryAllByRole(container, 'radio')).toHaveLength(appCount - 1);
|
||||
await waitFor(() => expect(queryAllByRole(container, 'radio')).toHaveLength(appCount - 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,6 +239,26 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Commonly requested',
|
||||
description: 'The type of a discussions feature.',
|
||||
},
|
||||
hideDiscussionTabTitle: {
|
||||
id: 'authoring.discussions.hide-tab-title',
|
||||
defaultMessage: 'Hide the discussion tab?',
|
||||
description: 'Title message to hide discussion tab',
|
||||
},
|
||||
hideDiscussionTabMessage: {
|
||||
id: 'authoring.discussions.hide-tab-message',
|
||||
defaultMessage: 'The discussion tab will no longer be visible to learners in the LMS. Additionally, posting to the discussion forums will be disabled. Are you sure you want to proceed?',
|
||||
description: 'Help message to hide discussion tab',
|
||||
},
|
||||
hideDiscussionOkButton: {
|
||||
id: 'authoring.discussions.hide-ok-button',
|
||||
defaultMessage: 'Ok',
|
||||
description: 'Ok button title',
|
||||
},
|
||||
hideDiscussionCancelButton: {
|
||||
id: 'authoring.discussions.hide-cancel-button',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Cancel button title',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -233,7 +233,7 @@ function denormalizeData(courseId, appId, data) {
|
||||
|
||||
const apiData = {
|
||||
context_key: courseId,
|
||||
enabled: true,
|
||||
enabled: data.enabled,
|
||||
lti_configuration: ltiConfiguration,
|
||||
plugin_configuration: pluginConfiguration,
|
||||
provider_type: appId,
|
||||
|
||||
6
src/pages-and-resources/discussions/data/hook.js
Normal file
6
src/pages-and-resources/discussions/data/hook.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
|
||||
export default function useIsOnSmallScreen() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width < breakpoints.medium.minWidth;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { DivisionSchemes } from '../../../data/constants';
|
||||
import { LOADED } from '../../../data/slice';
|
||||
import initializeStore from '../../../store';
|
||||
@@ -371,23 +372,25 @@ describe('Data layer integration tests', () => {
|
||||
pagesAndResourcesPath,
|
||||
), store.dispatch);
|
||||
|
||||
expect(window.location.pathname).toEqual(pagesAndResourcesPath);
|
||||
expect(store.getState().discussions).toEqual(
|
||||
expect.objectContaining({
|
||||
appIds: ['legacy', 'openedx', 'piazza', 'discourse'],
|
||||
featureIds,
|
||||
activeAppId: 'piazza',
|
||||
selectedAppId: 'piazza',
|
||||
status: LOADED,
|
||||
saveStatus: SAVED,
|
||||
hasValidationError: false,
|
||||
}),
|
||||
);
|
||||
expect(store.getState().models.appConfigs.piazza).toEqual({
|
||||
id: 'piazza',
|
||||
consumerKey: 'new_consumer_key',
|
||||
consumerSecret: 'new_consumer_secret',
|
||||
launchUrl: 'https://localhost/new_launch_url',
|
||||
waitFor(() => {
|
||||
expect(window.location.pathname).toEqual(pagesAndResourcesPath);
|
||||
expect(store.getState().discussions).toEqual(
|
||||
expect.objectContaining({
|
||||
appIds: ['legacy', 'openedx', 'piazza', 'discourse'],
|
||||
featureIds,
|
||||
activeAppId: 'piazza',
|
||||
selectedAppId: 'piazza',
|
||||
status: LOADED,
|
||||
saveStatus: SAVED,
|
||||
hasValidationError: false,
|
||||
}),
|
||||
);
|
||||
expect(store.getState().models.appConfigs.piazza).toEqual({
|
||||
id: 'piazza',
|
||||
consumerKey: 'new_consumer_key',
|
||||
consumerSecret: 'new_consumer_secret',
|
||||
launchUrl: 'https://localhost/new_launch_url',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -465,35 +468,37 @@ describe('Data layer integration tests', () => {
|
||||
},
|
||||
pagesAndResourcesPath,
|
||||
), store.dispatch);
|
||||
expect(window.location.pathname).toEqual(pagesAndResourcesPath);
|
||||
expect(store.getState().discussions).toEqual(
|
||||
expect.objectContaining({
|
||||
appIds: ['legacy', 'openedx', 'piazza', 'discourse'],
|
||||
featureIds,
|
||||
activeAppId: 'legacy',
|
||||
selectedAppId: 'legacy',
|
||||
status: LOADED,
|
||||
saveStatus: SAVED,
|
||||
hasValidationError: false,
|
||||
divideDiscussionIds,
|
||||
discussionTopicIds,
|
||||
}),
|
||||
);
|
||||
expect(store.getState().models.appConfigs.legacy).toEqual({
|
||||
id: 'legacy',
|
||||
// These three fields should be updated.
|
||||
allowAnonymousPosts: true,
|
||||
allowAnonymousPostsPeers: true,
|
||||
reportedContentEmailNotifications: true,
|
||||
alwaysDivideInlineDiscussions: true,
|
||||
restrictedDates: [],
|
||||
// TODO: Note! The values we tried to save were ignored, this test reflects what currently
|
||||
// happens, but NOT what we want to have happen!
|
||||
divideByCohorts: true,
|
||||
divisionScheme: DivisionSchemes.COHORT,
|
||||
cohortsEnabled: false,
|
||||
allowDivisionByUnit: false,
|
||||
divideCourseTopicsByCohorts: true,
|
||||
waitFor(() => {
|
||||
expect(window.location.pathname).toEqual(pagesAndResourcesPath);
|
||||
expect(store.getState().discussions).toEqual(
|
||||
expect.objectContaining({
|
||||
appIds: ['legacy', 'openedx', 'piazza', 'discourse'],
|
||||
featureIds,
|
||||
activeAppId: 'legacy',
|
||||
selectedAppId: 'legacy',
|
||||
status: LOADED,
|
||||
saveStatus: SAVED,
|
||||
hasValidationError: false,
|
||||
divideDiscussionIds,
|
||||
discussionTopicIds,
|
||||
}),
|
||||
);
|
||||
expect(store.getState().models.appConfigs.legacy).toEqual({
|
||||
id: 'legacy',
|
||||
// These three fields should be updated.
|
||||
allowAnonymousPosts: true,
|
||||
allowAnonymousPostsPeers: true,
|
||||
reportedContentEmailNotifications: true,
|
||||
alwaysDivideInlineDiscussions: true,
|
||||
restrictedDates: [],
|
||||
// TODO: Note! The values we tried to save were ignored, this test reflects what currently
|
||||
// happens, but NOT what we want to have happen!
|
||||
divideByCohorts: true,
|
||||
divisionScheme: DivisionSchemes.COHORT,
|
||||
cohortsEnabled: false,
|
||||
allowDivisionByUnit: false,
|
||||
divideCourseTopicsByCohorts: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ const slice = createSlice({
|
||||
enableGradedUnits: false,
|
||||
unitLevelVisibility: false,
|
||||
postingRestrictions: null,
|
||||
enabled: true,
|
||||
},
|
||||
reducers: {
|
||||
loadApps: (state, { payload }) => {
|
||||
|
||||
Reference in New Issue
Block a user