Merge pull request #143 from openedx/aansari/INF-156

fix: add validation for edit reason code dropdown
This commit is contained in:
Muhammad Adeel Tajamul
2022-04-22 11:15:00 +05:00
committed by GitHub
4 changed files with 122 additions and 90 deletions

View File

@@ -10,6 +10,7 @@ import { AppContext } from '@edx/frontend-platform/react';
import { Button, Form, StatefulButton } from '@edx/paragon';
import { TinyMCEEditor } from '../../../components';
import FormikErrorFeedback from '../../../components/FormikErrorFeedback';
import { useDispatchWithState } from '../../../data/hooks';
import { selectModerationSettings, selectUserIsPrivileged } from '../../data/selectors';
import { formikCompatibleHandler, isFormikFieldInvalid } from '../../utils';
@@ -22,14 +23,38 @@ function CommentEditor({
onCloseEditor,
edit,
}) {
const editorRef = useRef(null);
const { authenticatedUser } = useContext(AppContext);
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
const [submitting, dispatch] = useDispatchWithState();
const editorRef = useRef(null);
const canDisplayEditReason = (reasonCodesEnabled && userIsPrivileged
&& edit && comment.author !== authenticatedUser.username
);
const editReasonCodeValidation = canDisplayEditReason && {
editReasonCode: Yup.string().required(intl.formatMessage(messages.editReasonCodeError)),
};
const validationSchema = Yup.object().shape({
comment: Yup.string()
.required(),
...editReasonCodeValidation,
});
const initialValues = {
comment: comment.rawBody,
editReasonCode: comment?.lastEdit?.reasonCode || '',
};
const saveUpdatedComment = async (values) => {
if (comment.id) {
await dispatch(editComment(comment.id, values));
const payload = {
...values,
editReasonCode: values.editReasonCode || undefined,
};
await dispatch(editComment(comment.id, payload));
} else {
await dispatch(addComment(values.comment, comment.threadId, comment.parentId));
}
@@ -42,17 +67,11 @@ function CommentEditor({
// The editorId is used to autosave contents to localstorage. This format means that the autosave is scoped to
// the current comment id, or the current comment parent or the curren thread.
const editorId = `comment-editor-${comment.id || comment.parentId || comment.threadId}`;
return (
<Formik
initialValues={{ comment: comment.rawBody }}
validationSchema={Yup.object()
.shape({
comment: Yup.string()
.required(),
editReasonCode: Yup.string()
.nullable()
.default(undefined),
})}
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={saveUpdatedComment}
>
{({
@@ -64,12 +83,13 @@ function CommentEditor({
handleChange,
}) => (
<Form onSubmit={handleSubmit}>
{(reasonCodesEnabled
&& userIsPrivileged
&& comment.author !== authenticatedUser.username
&& edit
) && (
<Form.Group>
{canDisplayEditReason && (
<Form.Group
isInvalid={isFormikFieldInvalid('editReasonCode', {
errors,
touched,
})}
>
<Form.Control
name="editReasonCode"
className="mt-2"
@@ -88,6 +108,7 @@ function CommentEditor({
<option key={code} value={code}>{label}</option>
))}
</Form.Control>
<FormikErrorFeedback name="editReasonCode" />
</Form.Group>
)}
<TinyMCEEditor
@@ -142,6 +163,7 @@ CommentEditor.propTypes = {
parentId: PropTypes.string,
rawBody: PropTypes.string,
author: PropTypes.string,
lastEdit: PropTypes.object,
}).isRequired,
onCloseEditor: PropTypes.func.isRequired,
intl: intlShape.isRequired,

View File

@@ -26,7 +26,7 @@ const messages = defineMessages({
},
endorsedResponseCount: {
id: 'discussions.comments.comment.endorsedResponseCount',
defaultMessage: `{num, plural,
defaultMessage: `{num, plural,
=0 {No endorsed responses}
one {Showing # endorsed response}
other {Showing # endorsed responses}
@@ -147,6 +147,11 @@ const messages = defineMessages({
defaultMessage: 'Reason for editing',
description: 'Label for field visible to moderators that allows them to select a reason for editing another user\'s response',
},
editReasonCodeError: {
id: 'discussions.editor.posts.editReasonCode.error',
defaultMessage: 'Select reason for editing',
description: 'Error message visible to moderators when they submit the post/response/comment without select reason for editing',
},
editedBy: {
id: 'discussions.comment.comments.editedBy',
defaultMessage: 'Edited by',

View File

@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { Formik } from 'formik';
import { isEmpty } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import * as Yup from 'yup';
@@ -83,14 +84,21 @@ function PostEditor({
const nonCoursewareTopics = useSelector(selectNonCoursewareTopics);
const nonCoursewareIds = useSelector(selectNonCoursewareIds);
const coursewareTopics = useSelector(selectCoursewareTopics);
const {
allowAnonymous,
allowAnonymousToPeers,
} = useSelector(selectAnonymousPostingConfig);
const cohorts = useSelector(selectCourseCohorts);
const post = useSelector(selectThread(postId));
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const settings = useSelector(selectDivisionSettings);
const { allowAnonymous, allowAnonymousToPeers } = useSelector(selectAnonymousPostingConfig);
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
const canDisplayEditReason = (reasonCodesEnabled && editExisting
&& userIsPrivileged && post.author !== authenticatedUser.username
);
const editReasonCodeValidation = canDisplayEditReason && {
editReasonCode: Yup.string().required(intl.formatMessage(messages.editReasonCodeError)),
};
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.
@@ -164,60 +172,48 @@ function PostEditor({
</div>
);
}
let initialValues = {
postType: 'discussion',
topic: topicId || nonCoursewareTopics?.[0]?.id,
title: '',
comment: '',
follow: true,
anonymous: false,
anonymousToPeers: false,
const initialValues = {
postType: post?.type || 'discussion',
topic: post?.topicId || topicId || nonCoursewareTopics?.[0]?.id,
title: post?.title || '',
comment: post?.rawBody || '',
follow: isEmpty(post?.following) ? true : post?.following,
anonymous: allowAnonymous ? false : undefined,
anonymousToPeers: allowAnonymousToPeers ? false : undefined,
editReasonCode: post?.lastEdit?.reasonCode || '',
};
if (editExisting) {
initialValues = {
postType: post.type,
topic: post.topicId,
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 validationSchema = Yup.object().shape({
postType: Yup.mixed()
.oneOf(['discussion', 'question']),
topic: Yup.string()
.required(),
title: Yup.string()
.required(intl.formatMessage(messages.titleError)),
comment: Yup.string()
.required(intl.formatMessage(messages.commentError)),
follow: Yup.bool()
.default(true),
anonymous: Yup.bool()
.default(false)
.nullable(),
anonymousToPeers: Yup.bool()
.default(false)
.nullable(),
cohort: Yup.string()
.nullable()
.default(null),
...editReasonCodeValidation,
});
const postEditorId = `post-editor-${editExisting ? postId : 'new'}`;
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
return (
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={Yup.object()
.shape({
postType: Yup.mixed()
.oneOf(['discussion', 'question']),
topic: Yup.string()
.required(),
title: Yup.string()
.required(intl.formatMessage(messages.titleError)),
comment: Yup.string()
.required(intl.formatMessage(messages.commentError)),
follow: Yup.bool()
.default(true),
anonymous: Yup.bool()
.default(false)
.nullable(),
anonymousToPeers: Yup.bool()
.default(false)
.nullable(),
cohort: Yup.string()
.nullable()
.default(null),
editReasonCode: Yup.string()
.nullable()
.default(null),
})}
validationSchema={validationSchema}
onSubmit={submitForm}
>{
({
@@ -304,7 +300,7 @@ function PostEditor({
<div className="border-bottom my-1" />
<div className="d-flex flex-row py-2 mt-4 justify-content-between">
<Form.Group
className="d-flex flex-fill"
className="w-100"
isInvalid={isFormikFieldInvalid('title', {
errors,
touched,
@@ -321,27 +317,31 @@ function PostEditor({
/>
<FormikErrorFeedback name="title" />
</Form.Group>
{(reasonCodesEnabled
&& editExisting
&& userIsPrivileged
&& post.author !== authenticatedUser.username) && (
<Form.Group className="d-flex flex-fill">
<Form.Control
name="editReasonCode"
className="ml-4"
as="select"
value={values.editReasonCode}
onChange={handleChange}
onBlur={handleBlur}
aria-describedby="editReasonCodeInput"
floatingLabel={intl.formatMessage(messages.editReasonCode)}
>
<option key="empty" value="">---</option>
{editReasons.map(({ code, label }) => (
<option key={code} value={code}>{label}</option>
))}
</Form.Control>
</Form.Group>
{canDisplayEditReason && (
<Form.Group
className="w-100"
isInvalid={isFormikFieldInvalid('editReasonCode', {
errors,
touched,
})}
>
<Form.Control
name="editReasonCode"
className="m-0"
as="select"
value={values.editReasonCode}
onChange={handleChange}
onBlur={handleBlur}
aria-describedby="editReasonCodeInput"
floatingLabel={intl.formatMessage(messages.editReasonCode)}
>
<option key="empty" value="">---</option>
{editReasons.map(({ code, label }) => (
<option key={code} value={code}>{label}</option>
))}
</Form.Control>
<FormikErrorFeedback name="editReasonCode" />
</Form.Group>
)}
</div>
<div className="py-2">

View File

@@ -106,6 +106,11 @@ const messages = defineMessages({
defaultMessage: 'Reason for editing',
description: 'Label for field visible to moderators that allows them to select a reason for editing another user\'s post',
},
editReasonCodeError: {
id: 'discussions.editor.posts.editReasonCode.error',
defaultMessage: 'Select reason for editing',
description: 'Error message visible to moderators when they submit the post/response/comment without select reason for editing',
},
});
export default messages;