diff --git a/src/discussions/data/api.js b/src/discussions/data/api.js index eb966727..6dcd3460 100644 --- a/src/discussions/data/api.js +++ b/src/discussions/data/api.js @@ -20,3 +20,13 @@ export async function getDiscussionsConfig(courseId) { const { data } = await getAuthenticatedHttpClient().get(url); return data; } + +/** + * Get discussions course config + * @param {string} courseId + */ +export async function getDiscussionsSettings(courseId) { + const url = `${courseConfigApiUrl}${courseId}/settings`; + const { data } = await getAuthenticatedHttpClient().get(url); + return data; +} diff --git a/src/discussions/data/selectors.js b/src/discussions/data/selectors.js index 6284bb23..f6ac95ec 100644 --- a/src/discussions/data/selectors.js +++ b/src/discussions/data/selectors.js @@ -3,5 +3,8 @@ export const selectAnonymousPostingConfig = state => ({ allowAnonymous: state.config.allowAnonymous, allowAnonymousToPeers: state.config.allowAnonymousToPeers, -} -); +}); + +export const selectUserIsPrivileged = state => state.config.userIsPrivileged; + +export const selectDivisionSettings = state => state.config.settings; diff --git a/src/discussions/data/slices.js b/src/discussions/data/slices.js index e573603e..95fc0bb5 100644 --- a/src/discussions/data/slices.js +++ b/src/discussions/data/slices.js @@ -10,6 +10,14 @@ const configSlice = createSlice({ blackouts: [], allowAnonymous: false, allowAnonymousToPeers: false, + userRoles: [], + userIsPrivileged: false, + settings: { + divisionScheme: 'none', + alwaysDivideInlineDiscussions: false, + dividedInlineDiscussions: [], + dividedCourseWideDiscussions: [], + }, }, reducers: { fetchConfigRequest: (state) => { diff --git a/src/discussions/data/thunks.js b/src/discussions/data/thunks.js index bec4a0d9..3b102c73 100644 --- a/src/discussions/data/thunks.js +++ b/src/discussions/data/thunks.js @@ -3,7 +3,7 @@ import { camelCaseObject } from '@edx/frontend-platform'; import { logError } from '@edx/frontend-platform/logging'; import { getHttpErrorStatus } from '../utils'; -import { getDiscussionsConfig } from './api'; +import { getDiscussionsConfig, getDiscussionsSettings } from './api'; import { fetchConfigDenied, fetchConfigFailed, fetchConfigRequest, fetchConfigSuccess, } from './slices'; @@ -17,8 +17,12 @@ export function fetchCourseConfig(courseId) { return async (dispatch) => { try { dispatch(fetchConfigRequest()); - const data = await getDiscussionsConfig(courseId); - dispatch(fetchConfigSuccess(camelCaseObject(data))); + const config = await getDiscussionsConfig(courseId); + if (config.user_is_privileged) { + const settings = await getDiscussionsSettings(courseId); + Object.assign(config, { settings }); + } + dispatch(fetchConfigSuccess(camelCaseObject(config))); } catch (error) { if (getHttpErrorStatus(error) === 403) { dispatch(fetchConfigDenied()); diff --git a/src/discussions/posts/data/api.js b/src/discussions/posts/data/api.js index 4f600332..6df5a3d5 100644 --- a/src/discussions/posts/data/api.js +++ b/src/discussions/posts/data/api.js @@ -77,6 +77,7 @@ export async function getThread(threadId) { * @param {ThreadType} type The thread's type (either "question" or "discussion") * @param {string} title * @param {string} content + * @param {number} cohort * @param {boolean} following Follow the thread after creating * @param {boolean} anonymous Should the thread be anonymous to all users * @param {boolean} anonymousToPeers Should the thread be anonymous to peers diff --git a/src/discussions/posts/post-editor/PostEditor.jsx b/src/discussions/posts/post-editor/PostEditor.jsx index 52258af9..5307b486 100644 --- a/src/discussions/posts/post-editor/PostEditor.jsx +++ b/src/discussions/posts/post-editor/PostEditor.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { Formik } from 'formik'; @@ -7,16 +7,18 @@ import { useHistory, useParams } from 'react-router'; import * as Yup from 'yup'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { AppContext } from '@edx/frontend-platform/react'; -import { Card, Form, StatefulButton } from '@edx/paragon'; +import { + Button, Card, Form, Spinner, StatefulButton, +} from '@edx/paragon'; import { Help, Post } from '@edx/paragon/icons'; import { TinyMCEEditor } from '../../../components'; import FormikErrorFeedback from '../../../components/FormikErrorFeedback'; +import { useDispatchWithState } from '../../../data/hooks'; import { selectCourseCohorts } from '../../cohorts/data/selectors'; import { fetchCourseCohorts } from '../../cohorts/data/thunks'; -import { selectAnonymousPostingConfig } from '../../data/selectors'; -import { selectCoursewareTopics, selectNonCoursewareTopics } from '../../topics/data/selectors'; +import { selectAnonymousPostingConfig, selectDivisionSettings, selectUserIsPrivileged } from '../../data/selectors'; +import { selectCoursewareTopics, selectNonCoursewareIds, selectNonCoursewareTopics } from '../../topics/data/selectors'; import { fetchCourseTopics } from '../../topics/data/thunks'; import { discussionsPath, formikCompatibleHandler, isFormikFieldInvalid, useCommentsPagePath, @@ -62,8 +64,8 @@ function PostEditor({ intl, editExisting, }) { - const { authenticatedUser } = useContext(AppContext); const dispatch = useDispatch(); + const [submitting, dispatchSubmit] = useDispatchWithState(); const history = useHistory(); const commentsPagePath = useCommentsPagePath(); const { @@ -73,33 +75,26 @@ function PostEditor({ } = useParams(); const coursewareTopics = useSelector(selectCoursewareTopics); const nonCoursewareTopics = useSelector(selectNonCoursewareTopics); + const nonCoursewareIds = useSelector(selectNonCoursewareIds); const { allowAnonymous, allowAnonymousToPeers, } = useSelector(selectAnonymousPostingConfig); const cohorts = useSelector(selectCourseCohorts); const post = useSelector(selectThread(postId)); - let initialValues = { - postType: 'discussion', - topic: topicId || nonCoursewareTopics?.[0]?.id, - title: '', - comment: '', - follow: true, - anonymous: false, - anonymousToPeers: false, + const userIsPrivileged = useSelector(selectUserIsPrivileged); + const settings = useSelector(selectDivisionSettings); + const canSelectCohort = (tId) => { + // If the user isn't privileged, they can't edit the cohort. + // If the topic is being edited the cohort can't be changed. + if (!userIsPrivileged || editExisting) { + return false; + } + if (nonCoursewareIds.includes(tId)) { + return settings.dividedCourseWideDiscussions.includes(tId); + } + return settings.alwaysDivideInlineDiscussions || settings.dividedInlineDiscussions.includes(tId); }; - if (editExisting) { - initialValues = { - postType: post.type || 'discussion', - topic: post.topicId || topicId || nonCoursewareTopics?.[0]?.id, - title: post.title || '', - comment: post.rawBody || '', - follow: (post.following === null || post.following === undefined) ? true : post.following, - anonymous: allowAnonymous ? false : undefined, - anonymousToPeers: allowAnonymousToPeers ? false : undefined, - }; - } - const canSelectCohort = authenticatedUser.administrator && !editExisting; const hideEditor = () => { if (editExisting) { history.push(discussionsPath(commentsPagePath, { @@ -113,27 +108,27 @@ function PostEditor({ const submitForm = async (values) => { if (editExisting) { - dispatch(updateExistingThread(postId, { + await dispatchSubmit(updateExistingThread(postId, { topicId: values.topic, type: values.postType, title: values.title, content: values.comment, })); } else { - const cohort = canSelectCohort + const cohort = canSelectCohort(values.topic) // null stands for no cohort restriction ("All learners" option) ? (values.cohort || null) // if not allowed to set cohort, always undefined, so no value is sent to backend : undefined; - dispatch(createNewThread({ + await dispatchSubmit(createNewThread({ courseId, topicId: values.topic, type: values.postType, title: values.title, content: values.comment, following: values.following, - anonymous: values.anonymous, - anonymousToPeers: values.anonymousToPeers, + anonymous: allowAnonymous ? values.anonymous : undefined, + anonymousToPeers: allowAnonymousToPeers ? values.anonymousToPeers : undefined, cohort, })); } @@ -142,7 +137,7 @@ function PostEditor({ useEffect(() => { dispatch(fetchCourseTopics(courseId)); - if (canSelectCohort) { + if (userIsPrivileged) { dispatch(fetchCourseCohorts(courseId)); } if (editExisting) { @@ -150,6 +145,34 @@ function PostEditor({ } }, [courseId, editExisting]); + if (editExisting && !post) { + return ( +