From 68ecaaef9c1e160bcc97455ef49bb9c45c87563c Mon Sep 17 00:00:00 2001 From: sundasnoreen12 <72802712+sundasnoreen12@users.noreply.github.com> Date: Mon, 24 Jul 2023 07:13:20 -0700 Subject: [PATCH] chore: add paragon messages (#530) (#534) Co-authored-by: Mashal Malik <107556986+Mashal-m@users.noreply.github.com> --- .../discussions/DiscussionsSettings.test.jsx | 8 +- .../discussions/app-list/AppList.jsx | 95 ++++++++++++++++-- .../discussions/app-list/AppList.scss | 31 +++++- .../discussions/app-list/AppList.test.jsx | 35 ++++--- .../discussions/app-list/messages.js | 20 ++++ .../discussions/data/api.js | 2 +- .../discussions/data/hook.js | 6 ++ .../discussions/data/redux.test.js | 97 ++++++++++--------- .../discussions/data/slice.js | 1 + 9 files changed, 224 insertions(+), 71 deletions(-) create mode 100644 src/pages-and-resources/discussions/data/hook.js diff --git a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx index 73af4b9e0f..310fca7690 100644 --- a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx +++ b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx @@ -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 () => { diff --git a/src/pages-and-resources/discussions/app-list/AppList.jsx b/src/pages-and-resources/discussions/app-list/AppList.jsx index 04509d3a21..4b1ad4eb2c 100644 --- a/src/pages-and-resources/discussions/app-list/AppList.jsx +++ b/src/pages-and-resources/discussions/app-list/AppList.jsx @@ -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 ( -
-

- {intl.formatMessage(messages.heading)} -

+
+
+

+ {intl.formatMessage(messages.heading)} +

+ + Hide discussion tab + +
{ lg: 4, xl: 4, }} + className={!isOnSmallcreen && 'mt-5'} > {(isGlobalStaff || ltiProvider) ? showAppCard(apps) : showAppCard(showOneEdxProvider)} @@ -96,6 +158,23 @@ const AppList = ({ intl }) => { />
+ + + + + )} + > +

+ {intl.formatMessage(messages.hideDiscussionTabMessage)} +

+
); }; diff --git a/src/pages-and-resources/discussions/app-list/AppList.scss b/src/pages-and-resources/discussions/app-list/AppList.scss index 0182483c85..83d3c3c915 100644 --- a/src/pages-and-resources/discussions/app-list/AppList.scss +++ b/src/pages-and-resources/discussions/app-list/AppList.scss @@ -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; } } diff --git a/src/pages-and-resources/discussions/app-list/AppList.test.jsx b/src/pages-and-resources/discussions/app-list/AppList.test.jsx index 08a1b3e421..9bfa74d8f7 100644 --- a/src/pages-and-resources/discussions/app-list/AppList.test.jsx +++ b/src/pages-and-resources/discussions/app-list/AppList.test.jsx @@ -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)); }); }); }); diff --git a/src/pages-and-resources/discussions/app-list/messages.js b/src/pages-and-resources/discussions/app-list/messages.js index 531d333cbb..96b882e3cf 100644 --- a/src/pages-and-resources/discussions/app-list/messages.js +++ b/src/pages-and-resources/discussions/app-list/messages.js @@ -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; diff --git a/src/pages-and-resources/discussions/data/api.js b/src/pages-and-resources/discussions/data/api.js index a960b6a272..12d0177f87 100644 --- a/src/pages-and-resources/discussions/data/api.js +++ b/src/pages-and-resources/discussions/data/api.js @@ -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, diff --git a/src/pages-and-resources/discussions/data/hook.js b/src/pages-and-resources/discussions/data/hook.js new file mode 100644 index 0000000000..f6b45f40ba --- /dev/null +++ b/src/pages-and-resources/discussions/data/hook.js @@ -0,0 +1,6 @@ +import { breakpoints, useWindowSize } from '@edx/paragon'; + +export default function useIsOnSmallScreen() { + const windowSize = useWindowSize(); + return windowSize.width < breakpoints.medium.minWidth; +} diff --git a/src/pages-and-resources/discussions/data/redux.test.js b/src/pages-and-resources/discussions/data/redux.test.js index 9eb72e0232..baf6ff3d6f 100644 --- a/src/pages-and-resources/discussions/data/redux.test.js +++ b/src/pages-and-resources/discussions/data/redux.test.js @@ -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, + }); }); }); }); diff --git a/src/pages-and-resources/discussions/data/slice.js b/src/pages-and-resources/discussions/data/slice.js index 3029b00ecf..8608d30fe4 100644 --- a/src/pages-and-resources/discussions/data/slice.js +++ b/src/pages-and-resources/discussions/data/slice.js @@ -29,6 +29,7 @@ const slice = createSlice({ enableGradedUnits: false, unitLevelVisibility: false, postingRestrictions: null, + enabled: true, }, reducers: { loadApps: (state, { payload }) => {