From 1b1afcf195ec9992d9350c815324de8058012eaf Mon Sep 17 00:00:00 2001 From: ayesha waris <73840786+ayesha-waris@users.noreply.github.com> Date: Tue, 6 Jun 2023 14:55:19 +0500 Subject: [PATCH] feat: integrated backend discussions restriction with UI (#507) * feat: integrated backend discussions restriction with UI * refactor: code refactoring * test: fixes test cases * refactor: discussion restriction component --------- Co-authored-by: ayesha waris <73840786+ayeshoali@users.noreply.github.com> Co-authored-by: Awais Ansari --- src/generic/ConfirmationPopup.jsx | 2 +- .../apps/openedx/OpenedXConfigForm.jsx | 6 +- .../apps/openedx/OpenedXConfigForm.test.jsx | 1 + .../apps/shared/DiscussionRestriction.jsx | 166 +++++++----------- .../apps/shared/InContextDiscussionFields.jsx | 1 - .../DiscussionRestrictionOption.jsx | 29 --- .../RestrictionSchedules.jsx | 84 +++++++++ .../discussions/app-config-form/messages.js | 12 +- .../discussions/app-list/AppList.scss | 17 +- .../discussions/data/api.js | 5 +- .../discussions/data/constants.js | 28 +-- .../discussions/data/slice.js | 1 + .../discussions/factories/mockApiResponses.js | 1 + 13 files changed, 191 insertions(+), 162 deletions(-) delete mode 100644 src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/DiscussionRestrictionOption.jsx create mode 100644 src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/RestrictionSchedules.jsx diff --git a/src/generic/ConfirmationPopup.jsx b/src/generic/ConfirmationPopup.jsx index 43e57528f..22d791fb2 100644 --- a/src/generic/ConfirmationPopup.jsx +++ b/src/generic/ConfirmationPopup.jsx @@ -14,7 +14,7 @@ const ConfirmationPopup = ({ cancelButtonClass, sectionClasses, }) => ( - + { const { - selectedAppId, enableGradedUnits, discussionTopicIds, divideDiscussionIds, + selectedAppId, enableGradedUnits, discussionTopicIds, divideDiscussionIds, postingRestrictions, } = useSelector(state => state.discussions); const appConfigObj = useModel('appConfigs', selectedAppId); const discussionTopicsModel = useModels('discussionTopics', discussionTopicIds); @@ -34,6 +34,7 @@ const OpenedXConfigForm = ({ divideDiscussionIds, enableInContext: true, enableGradedUnits, + postingRestrictions, unitLevelVisibility: true, allowAnonymousPostsPeers: appConfigObj?.allowAnonymousPostsPeers || false, reportedContentEmailNotifications: appConfigObj?.reportedContentEmailNotifications || false, @@ -109,6 +110,7 @@ const OpenedXConfigForm = ({ validDiscussionTopics, setValidDiscussionTopics, discussionTopicErrors, + postingRestrictions, restrictedDatesErrors, isFormInvalid: discussionTopicErrors.some((error) => error) @@ -117,7 +119,7 @@ const OpenedXConfigForm = ({ return ( - +

{intl.formatMessage(messages[`appName-${selectedAppId}`])}

diff --git a/src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.test.jsx b/src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.test.jsx index 7efa06450..ed8e9dcac 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.test.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.test.jsx @@ -40,6 +40,7 @@ const defaultAppConfig = (divideDiscussionIds = []) => ({ { name: 'General', id: 'course' }, ], divideDiscussionIds, + postingRestrictions: 'scheduled', enableGradedUnits: undefined, enableInContext: true, groupAtSubsection: false, diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/DiscussionRestriction.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/DiscussionRestriction.jsx index 691993268..359e75e77 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/DiscussionRestriction.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/DiscussionRestriction.jsx @@ -1,137 +1,89 @@ -import React, { useCallback, useState } from 'react'; -import { injectIntl, useIntl } from '@edx/frontend-platform/i18n'; +import React, { useCallback, useState, useMemo } from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { Button, ButtonGroup } from '@edx/paragon'; -import { Add } from '@edx/paragon/icons'; +import classNames from 'classnames'; -import { FieldArray, useFormikContext } from 'formik'; -import { v4 as uuid } from 'uuid'; +import { useFormikContext } from 'formik'; import ConfirmationPopup from '../../../../../generic/ConfirmationPopup'; import messages from '../../messages'; -import DiscussionRestrictionItem from './discussion-restrictions/DiscussionRestrictionItem'; -import { checkStatus } from '../../utils'; -import { denormalizeRestrictedDate } from '../../../data/api'; -import { restrictedDatesStatus as STATUS, discussionRestrictionOptions } from '../../../data/constants'; -import DiscussionRestrictionOption from './discussion-restrictions/DiscussionRestrictionOption'; +import { discussionRestrictionOptions, discussionRestriction } from '../../../data/constants'; +import RestrictionSchedules from './discussion-restrictions/RestrictionSchedules'; const DiscussionRestriction = () => { + const intl = useIntl(); const { values: appConfig, setFieldValue, - errors, - validateForm, } = useFormikContext(); - const intl = useIntl(); - const { restrictedDates } = appConfig; - const [selectedOption, setSelectedOption] = useState(''); - - const handleOnClose = useCallback((index) => { - const updatedRestrictedDates = [...restrictedDates]; - updatedRestrictedDates[index] = { - ...updatedRestrictedDates[index], - status: checkStatus(denormalizeRestrictedDate(updatedRestrictedDates[index])), - }; - setFieldValue('restrictedDates', updatedRestrictedDates); - }, [restrictedDates]); - - const newRestrictedDateItem = { - id: uuid(), - startDate: '', - startTime: '', - endDate: '', - endTime: '', - status: STATUS.UPCOMING, - }; - - const onAddNewItem = useCallback(async (push) => { - await push(newRestrictedDateItem); - validateForm(); - }, []); + const { postingRestrictions } = appConfig; + const [selectedRestrictionOption, setSelectedRestrictionOption] = useState(postingRestrictions); const handleClick = useCallback((value) => { - setSelectedOption(value); + setSelectedRestrictionOption(value); + + if (value !== discussionRestriction.ENABLED) { + setFieldValue('postingRestrictions', value); + } + }, []); + + const handleConfirmation = useCallback(() => { + setSelectedRestrictionOption(discussionRestriction.ENABLED); + setFieldValue('postingRestrictions', discussionRestriction.ENABLED); }, []); const handleCancel = useCallback(() => { - setSelectedOption(''); - }, []); + setSelectedRestrictionOption(postingRestrictions); + }, [postingRestrictions]); + + const discussionRestrictionButtons = useMemo(() => discussionRestrictionOptions.map((restriction) => ( + + )), [selectedRestrictionOption]); + + const selectedRestrictionMessage = useMemo(() => ( + discussionRestrictionOptions.find(option => option.value === selectedRestrictionOption).message + ), [selectedRestrictionOption]); return (
{intl.formatMessage(messages.discussionRestrictionLabel)}
- - {discussionRestrictionOptions.map((option) => ( - {option.label} - - - ))} + + {discussionRestrictionButtons} - {(selectedOption === 'on' || selectedOption === 'off') && ( -
- {intl.formatMessage(messages.discussionRestrictionHelp)} -
- )} - - {selectedOption === 'on' && ( - - )} - - {selectedOption === 'scheduled' && ( -
-
- {intl.formatMessage(messages.discussionRestrictionDatesHelp)} -
- ( -
- {restrictedDates.map((restrictedDate, index) => ( - remove(index)} - onClose={() => handleOnClose(index)} - hasError={Boolean(errors?.restrictedDates?.[index])} - /> - ))} -
- -
-
- )} - /> +
+ {intl.formatMessage(selectedRestrictionMessage)}
+ {(postingRestrictions !== discussionRestriction.ENABLED + && selectedRestrictionOption === discussionRestriction.ENABLED + ) && ( + )} + {selectedRestrictionOption === discussionRestriction.SCHEDULED && }
); }; -export default injectIntl(React.memo(DiscussionRestriction)); +export default React.memo(DiscussionRestriction); diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/InContextDiscussionFields.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/InContextDiscussionFields.jsx index 99d04793b..bfa2e44da 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/InContextDiscussionFields.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/InContextDiscussionFields.jsx @@ -18,7 +18,6 @@ const InContextDiscussionFields = ({ } = useFormikContext(); const [showPopup, setShowPopup] = useState(false); - const handleConfirmation = () => { setFieldValue('enableGradedUnits', !values.enableGradedUnits); setShowPopup(false); diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/DiscussionRestrictionOption.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/DiscussionRestrictionOption.jsx deleted file mode 100644 index bace1424a..000000000 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/DiscussionRestrictionOption.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { Button } from '@edx/paragon'; -import PropTypes from 'prop-types'; - -const DiscussionRestrictionOption = ({ - value, - label, - onClick, - selectedOption, -}) => ( - - ); - -DiscussionRestrictionOption.propTypes = { - value: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - selectedOption: PropTypes.string.isRequired, -}; - -export default React.memo(DiscussionRestrictionOption); diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/RestrictionSchedules.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/RestrictionSchedules.jsx new file mode 100644 index 000000000..5641f6e43 --- /dev/null +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-restrictions/RestrictionSchedules.jsx @@ -0,0 +1,84 @@ +import React, { useCallback } from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button } from '@edx/paragon'; +import { Add } from '@edx/paragon/icons'; + +import { FieldArray, useFormikContext } from 'formik'; +import { v4 as uuid } from 'uuid'; + +import messages from '../../../messages'; +import DiscussionRestrictionItem from './DiscussionRestrictionItem'; +import { checkStatus } from '../../../utils'; +import { denormalizeRestrictedDate } from '../../../../data/api'; +import { restrictedDatesStatus as STATUS } from '../../../../data/constants'; + +const RestrictionSchedules = () => { + const intl = useIntl(); + const { + values: appConfig, + setFieldValue, + errors, + validateForm, + } = useFormikContext(); + + const { restrictedDates } = appConfig; + + const handleOnClose = useCallback((index) => { + const updatedRestrictedDates = [...restrictedDates]; + updatedRestrictedDates[index] = { + ...updatedRestrictedDates[index], + status: checkStatus(denormalizeRestrictedDate(updatedRestrictedDates[index])), + }; + setFieldValue('restrictedDates', updatedRestrictedDates); + }, [restrictedDates]); + + const newRestrictedDateItem = { + id: uuid(), + startDate: '', + startTime: '', + endDate: '', + endTime: '', + status: STATUS.UPCOMING, + }; + + const onAddNewItem = useCallback(async (push) => { + await push(newRestrictedDateItem); + validateForm(); + }, []); + + return ( +
+ ( +
+ {restrictedDates.map((restrictedDate, index) => ( + remove(index)} + onClose={() => handleOnClose(index)} + hasError={Boolean(errors?.restrictedDates?.[index])} + /> + ))} +
+ +
+
+ )} + /> +
+ ); +}; + +export default React.memo(RestrictionSchedules); diff --git a/src/pages-and-resources/discussions/app-config-form/messages.js b/src/pages-and-resources/discussions/app-config-form/messages.js index a8c782875..e6cb18b49 100644 --- a/src/pages-and-resources/discussions/app-config-form/messages.js +++ b/src/pages-and-resources/discussions/app-config-form/messages.js @@ -403,17 +403,17 @@ const messages = defineMessages({ defaultMessage: 'If deleted, learners will be able to post in discussions during these dates.', description: 'Help text for delete a upcoming restricted dates from restricted dates section.', }, - discussionRestrictionOffLabel: { + discussionRestrictionOffLabelHelpText: { id: 'authoring.discussions.discussionRestrictionOff.label', - defaultMessage: 'Off', + defaultMessage: 'If enabled, learners will be able to post in discussions', }, - discussionRestrictionOnLabel: { + discussionRestrictionOnLabelHelpText: { id: 'authoring.discussions.discussionRestrictionOn.label', - defaultMessage: 'On', + defaultMessage: 'If enabled, learners will not be able to post in discussions', }, - discussionRestrictionScheduledLabel: { + discussionRestrictionScheduledLabelHelpText: { id: 'authoring.discussions.discussionRestrictionScheduled.label', - defaultMessage: 'Scheduled', + defaultMessage: 'If added, learners will not be able to post in discussions between these dates.', }, enableRestrictedDatesConfirmationLabel: { id: 'authoring.discussions.enableRestrictedDatesConfirmation.label', diff --git a/src/pages-and-resources/discussions/app-list/AppList.scss b/src/pages-and-resources/discussions/app-list/AppList.scss index 3aa109914..250aea5ff 100644 --- a/src/pages-and-resources/discussions/app-list/AppList.scss +++ b/src/pages-and-resources/discussions/app-list/AppList.scss @@ -29,19 +29,30 @@ .height-36{ height: 2.25rem !important; } + .line-height-20{ line-height: 1.25rem !important; } + .font-size-14{ font-size: 14px !important; } +.font-weight-500 { + font-weight: 500 !important; +} + +.py-7px{ + padding: 0 7px 0 7px ; +} + .discussion-restriction{ .unselected-button{ &:hover{ - background: #e9e6e4 !important; + background: #e9e6e4 ; } } + .action-btn{ padding: 10px 16px; width: 80px; @@ -50,19 +61,23 @@ font-size: 18px; line-height: 24px; } + .w-92{ width: 92px; } + .card-body-section{ padding-top: 12px !important; padding-bottom: 20px !important; } + .form-control{ border-radius: 0px !important; font-weight: 400; font-size: 14px; line-height: 24px; } + .collapsible-card{ padding: 14px 14px 14px 24px !important; min-height:100px; diff --git a/src/pages-and-resources/discussions/data/api.js b/src/pages-and-resources/discussions/data/api.js index 20122c7f7..a960b6a27 100644 --- a/src/pages-and-resources/discussions/data/api.js +++ b/src/pages-and-resources/discussions/data/api.js @@ -143,6 +143,7 @@ function normalizeSettings(data) { enableInContext: data.enable_in_context, enableGradedUnits: data.enable_graded_units, unitLevelVisibility: data.unit_level_visibility, + postingRestrictions: data.posting_restrictions, appConfig: normalizeAppConfig(data), piiConfig: normalizePiiSharing(data.lti_configuration), discussionTopicIds: data.plugin_configuration.discussion_topics @@ -246,6 +247,9 @@ function denormalizeData(courseId, appId, data) { if ('unitLevelVisibility' in data) { apiData.unit_level_visibility = data.unitLevelVisibility; } + if ('postingRestrictions' in data) { + apiData.posting_restrictions = data.postingRestrictions; + } return apiData; } @@ -272,7 +276,6 @@ export async function getDiscussionsSettings(courseId, providerId = null) { const url = getDiscussionsSettingsUrl(courseId); const { data } = await getAuthenticatedHttpClient() .get(url, params); - return normalizeSettings(data); } diff --git a/src/pages-and-resources/discussions/data/constants.js b/src/pages-and-resources/discussions/data/constants.js index a87c48639..2a1bf010e 100644 --- a/src/pages-and-resources/discussions/data/constants.js +++ b/src/pages-and-resources/discussions/data/constants.js @@ -21,32 +21,32 @@ export const deleteRestrictedDatesHelperText = { }; export const discussionRestriction = { - OFF: 'off', - ON: 'on', + DISABLED: 'disabled', + ENABLED: 'enabled', SCHEDULED: 'scheduled', }; -export const discussionRestrictionLabel = { - [discussionRestriction.OFF]: 'Off', - [discussionRestriction.ON]: 'On', - [discussionRestriction.SCHEDULED]: 'Scheduled', +export const discussionRestrictionLabels = { + OFF: 'Off', + ON: 'On', + SCHEDULED: 'Scheduled', }; export const discussionRestrictionOptions = [ { - value: discussionRestriction.OFF, - description: messages.discussionRestrictionOffLabel, - label: discussionRestrictionLabel[discussionRestriction.OFF], + value: discussionRestriction.DISABLED, + message: messages.discussionRestrictionOffLabelHelpText, + label: discussionRestrictionLabels.OFF, }, { - value: discussionRestriction.ON, - description: messages.discussionRestrictionOnLabel, - label: discussionRestrictionLabel[discussionRestriction.ON], + value: discussionRestriction.ENABLED, + message: messages.discussionRestrictionOnLabelHelpText, + label: discussionRestrictionLabels.ON, }, { value: discussionRestriction.SCHEDULED, - description: messages.discussionRestrictionScheduledLabel, - label: discussionRestrictionLabel[discussionRestriction.SCHEDULED], + message: messages.discussionRestrictionScheduledLabelHelpText, + label: discussionRestrictionLabels.SCHEDULED, }, ]; diff --git a/src/pages-and-resources/discussions/data/slice.js b/src/pages-and-resources/discussions/data/slice.js index 174b31e12..3029b00ec 100644 --- a/src/pages-and-resources/discussions/data/slice.js +++ b/src/pages-and-resources/discussions/data/slice.js @@ -28,6 +28,7 @@ const slice = createSlice({ enableInContext: false, enableGradedUnits: false, unitLevelVisibility: false, + postingRestrictions: null, }, reducers: { loadApps: (state, { payload }) => { diff --git a/src/pages-and-resources/discussions/factories/mockApiResponses.js b/src/pages-and-resources/discussions/factories/mockApiResponses.js index 46baab45a..e16814935 100644 --- a/src/pages-and-resources/discussions/factories/mockApiResponses.js +++ b/src/pages-and-resources/discussions/factories/mockApiResponses.js @@ -103,6 +103,7 @@ export const generateProvidersApiResponse = (piazzaAdminOnlyConfig = false, acti export const generateLegacyApiResponse = () => ({ context_key: 'course-v1:edX+DemoX+Demo_Course', enabled: true, + posting_restrictions: 'scheduled', provider_type: 'legacy', lti_configuration: {}, plugin_configuration: {