From 14873839451a7a2d1bf4c1535c0ae48405dcb299 Mon Sep 17 00:00:00 2001 From: Mashal Malik <107556986+Mashal-m@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:08:39 +0500 Subject: [PATCH] chore: add paragon messages (#530) --- src/i18n/index.js | 3 +- .../discussions/DiscussionsSettings.test.jsx | 87 ++++++++++------- .../discussions/app-list/AppList.jsx | 95 ++++++++++++++++-- .../discussions/app-list/AppList.scss | 31 +++++- .../discussions/app-list/AppList.test.jsx | 25 +++-- .../discussions/app-list/messages.js | 20 ++++ .../discussions/data/api.js | 2 +- .../discussions/data/hook.js | 6 ++ .../discussions/data/redux.test.js | 97 ++++++++++--------- 9 files changed, 264 insertions(+), 102 deletions(-) create mode 100644 src/pages-and-resources/discussions/data/hook.js diff --git a/src/i18n/index.js b/src/i18n/index.js index d0dc802c3c..2abb2b7cf4 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -1,5 +1,5 @@ import { messages as footerMessages } from '@edx/frontend-component-footer'; - +import { messages as paragonMessages } from '@edx/paragon'; import arMessages from './messages/ar.json'; import frMessages from './messages/fr.json'; import es419Messages from './messages/es_419.json'; @@ -35,5 +35,6 @@ const appMessages = { export default [ footerMessages, + paragonMessages, appMessages, ]; diff --git a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx index 73af4b9e0f..9e31c2b498 100644 --- a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx +++ b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx @@ -162,10 +162,14 @@ describe('DiscussionsSettings', () => { expect(queryByTestId(container, 'appConfigForm')).toBeInTheDocument(); - userEvent.click(queryByText(container, appMessages.backButton.defaultMessage)); + await act(async () => { + userEvent.click(queryByText(container, appMessages.backButton.defaultMessage)); + }); - expect(queryByTestId(container, 'appList')).toBeInTheDocument(); - expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument(); + waitFor(() => { + expect(queryByTestId(container, 'appList')).toBeInTheDocument(); + expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument(); + }); }); test('successfully closes the modal', async () => { @@ -217,14 +221,16 @@ describe('DiscussionsSettings', () => { // content has been loaded - prior to proceeding with our expectations. await waitForElementToBeRemoved(screen.getByRole('status')); - userEvent.click(getByRole(container, 'checkbox', { name: 'Select Discourse' })); - userEvent.click(getByRole(container, 'button', { name: 'Next' })); + await act(async () => userEvent.click(getByRole(container, 'checkbox', { name: 'Select Discourse' }))); + await act(async () => userEvent.click(getByRole(container, 'button', { name: 'Next' }))); - await findByRole(container, 'button', { name: 'Save' }); - userEvent.type(getByRole(container, 'textbox', { name: 'Consumer Key' }), 'key'); - userEvent.type(getByRole(container, 'textbox', { name: 'Consumer Secret' }), 'secret'); - userEvent.type(getByRole(container, 'textbox', { name: 'Launch URL' }), 'http://example.test'); - userEvent.click(getByRole(container, 'button', { name: 'Save' })); + waitFor(async () => { + await findByRole(container, 'button', { name: 'Save' }); + userEvent.type(getByRole(container, 'textbox', { name: 'Consumer Key' }), 'key'); + userEvent.type(getByRole(container, 'textbox', { name: 'Consumer Secret' }), 'secret'); + userEvent.type(getByRole(container, 'textbox', { name: 'Launch URL' }), 'http://example.test'); + userEvent.click(getByRole(container, 'button', { name: 'Save' })); + }); await waitFor(() => expect(getByRole(container, 'dialog', { name: 'OK' })).toBeInTheDocument()); }); @@ -364,17 +370,18 @@ describe('DiscussionsSettings', () => { userEvent.click(getByRole(container, 'button', { name: 'Save' })); - await waitFor(() => expect(axiosMock.history.post.length).toBe(1)); - - expect(queryByTestId(container, 'appList')).not.toBeInTheDocument(); - expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument(); + await waitFor(async () => { + expect(axiosMock.history.post.length).toBe(1); + expect(queryByTestId(container, 'appList')).not.toBeInTheDocument(); + expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument(); - // We don't technically leave the route in this case, though the modal is hidden. - expect(window.location.pathname).toEqual(`/course/${courseId}/pages-and-resources/discussion/configure/piazza`); + // We don't technically leave the route in this case, though the modal is hidden. + expect(window.location.pathname).toEqual(`/course/${courseId}/pages-and-resources/discussion/configure/piazza`); - const alert = await findByRole(container, 'alert'); - expect(alert).toBeInTheDocument(); - expect(alert.textContent).toEqual(expect.stringContaining('You are not authorized to view this page.')); + const alert = await findByRole(container, 'alert'); + expect(alert).toBeInTheDocument(); + expect(alert.textContent).toEqual(expect.stringContaining('You are not authorized to view this page.')); + }); }); }); }); @@ -421,17 +428,20 @@ describe.each([ // content has been loaded - prior to proceeding with our expectations. await waitForElementToBeRemoved(screen.getByRole('status')); - userEvent.click(queryByLabelText(container, 'Select Piazza')); - userEvent.click(queryByText(container, messages.nextButton.defaultMessage)); - await waitForElementToBeRemoved(screen.getByRole('status')); + await act(async () => userEvent.click(queryByLabelText(container, 'Select Piazza'))); + await act(async () => userEvent.click(queryByText(container, messages.nextButton.defaultMessage))); - if (showLTIConfig) { - expect(queryByText(container, ltiMessages.formInstructions.defaultMessage)).toBeInTheDocument(); - expect(queryByTestId(container, 'ltiConfigFields')).toBeInTheDocument(); - } else { - expect(queryByText(container, ltiMessages.formInstructions.defaultMessage)).not.toBeInTheDocument(); - expect(queryByTestId(container, 'ltiConfigFields')).not.toBeInTheDocument(); - } + waitFor(async () => { + await waitForElementToBeRemoved(screen.getByRole('status')); + + if (showLTIConfig) { + expect(queryByText(container, ltiMessages.formInstructions.defaultMessage)).toBeInTheDocument(); + expect(queryByTestId(container, 'ltiConfigFields')).toBeInTheDocument(); + } else { + expect(queryByText(container, ltiMessages.formInstructions.defaultMessage)).not.toBeInTheDocument(); + expect(queryByTestId(container, 'ltiConfigFields')).not.toBeInTheDocument(); + } + }); }); }); @@ -477,13 +487,16 @@ describe.each([ // content has been loaded - prior to proceeding with our expectations. await waitForElementToBeRemoved(screen.getByRole('status')); - userEvent.click(queryByLabelText(container, 'Select Piazza')); - userEvent.click(queryByText(container, messages.nextButton.defaultMessage)); - await waitForElementToBeRemoved(screen.getByRole('status')); - if (enablePIISharing) { - expect(queryByTestId(container, 'piiSharingFields')).toBeInTheDocument(); - } else { - expect(queryByTestId(container, 'piiSharingFields')).not.toBeInTheDocument(); - } + await act(async () => userEvent.click(queryByLabelText(container, 'Select Piazza'))); + await act(async () => userEvent.click(queryByText(container, messages.nextButton.defaultMessage))); + + waitFor(() => { + waitForElementToBeRemoved(screen.getByRole('status')); + if (enablePIISharing) { + expect(queryByTestId(container, 'piiSharingFields')).toBeInTheDocument(); + } else { + expect(queryByTestId(container, 'piiSharingFields')).not.toBeInTheDocument(); + } + }); }); }); diff --git a/src/pages-and-resources/discussions/app-list/AppList.jsx b/src/pages-and-resources/discussions/app-list/AppList.jsx index 04509d3a21..3be56eeabe 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,25 @@ 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 [showDiscussionAlert, setShowDiscussionAlert] = useState(false); + 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,8 +55,49 @@ const AppList = ({ intl }) => { dispatch(updateValidationStatus({ hasError: false })); }, [selectedAppId, activeAppId]); + useEffect(() => { + setDiscussionEnabled(enabled); + }, [enabled]); + + useEffect(() => { + dispatch(fetchDiscussionSettings(courseId, selectedAppId)); + }, [courseId, selectedAppId]); + const handleSelectApp = useCallback((appId) => { dispatch(selectApp({ appId })); + }, []); + + const updateSettings = useCallback((enabledDiscussion) => { + dispatch(saveProviderConfig( + courseId, + selectedAppId, + { + enabled: enabledDiscussion, + postingRestrictions: + enabledDiscussion ? postingRestrictions : discussionRestriction.ENABLED, + }, + )); + }, [courseId, selectedAppId, postingRestrictions, discussionEnabled]); + + const handleClose = useCallback(() => { + setShowDiscussionAlert(false); + setDiscussionEnabled(enabled); + }, [enabled]); + + const handleOk = useCallback(() => { + setShowDiscussionAlert(false); + setDiscussionEnabled(false); + updateSettings(false); + }, []); + + const handleChange = useCallback((e) => { + const toggleVal = e.target.checked; + + setShowDiscussionAlert(toggleVal); + setDiscussionEnabled(!toggleVal); + if (!toggleVal) { + updateSettings(toggleVal); + } }, [selectedAppId]); if (!selectedAppId || status === LOADING) { @@ -72,9 +126,20 @@ const AppList = ({ intl }) => { return (
-

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

+
+

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

+ + Hide discussion tab + +
{ lg: 4, xl: 4, }} + className="mt-4 my-sm-5" > {(isGlobalStaff || ltiProvider) ? showAppCard(apps) : showAppCard(showOneEdxProvider)} @@ -96,6 +162,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..21eb89d256 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'; @@ -70,35 +70,40 @@ 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); + waitFor(() => expect(screen.queryAllByRole('radio')).toHaveLength(appCount)); }); test('displays the FeaturesTable at desktop sizes', async () => { renderComponent(); - expect(screen.queryByRole('table')).toBeInTheDocument(); + waitFor(() => expect(screen.queryByRole('table')).toBeInTheDocument()); }); test('hides the FeaturesTable at mobile sizes', async () => { renderComponent(breakpoints.extraSmall.maxWidth); - expect(screen.queryByRole('table')).not.toBeInTheDocument(); + 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(); + 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); + waitFor(() => 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(); + + waitFor(() => { + userEvent.click(screen.getByLabelText('Select Piazza')); + const clickedCard = screen.getByRole('radio', { checked: true }); + expect(within(clickedCard).queryByLabelText('Select Piazza')).toBeInTheDocument(); + }); }); }); @@ -121,7 +126,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); + 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, + }); }); }); });