Compare commits
1 Commits
jenkins/ve
...
kshitij/th
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40ab53d793 |
1
.env
1
.env
@@ -20,3 +20,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
THEME_LOADER_URL=''
|
||||
|
||||
@@ -21,3 +21,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
THEME_LOADER_URL='https://xitij2000.github.io/frontend-theme-prototype/themes.js'
|
||||
|
||||
13
.github/workflows/lockfileversion-check.yml
vendored
13
.github/workflows/lockfileversion-check.yml
vendored
@@ -1,13 +0,0 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: Lockfile Version check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { _extends } from './common';
|
||||
|
||||
export default function QuestionAnswer(props) {
|
||||
return /* #__PURE__ */React.createElement('svg', _extends({
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
}, props), /* #__PURE__ */React.createElement('path', {
|
||||
d: 'M16.7371 4.00002H14.2371V11.5H3.4038V14H13.4038L16.7371 17.3334V4.00002ZM12.5705 9.83335V0.666687H0.0704651V13.1667L3.4038 9.83335H12.5705Z',
|
||||
fill: 'currentColor',
|
||||
}));
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { _extends } from './common';
|
||||
|
||||
export default function QuestionAnswerOutline(props) {
|
||||
return /* #__PURE__ */React.createElement('svg', _extends({
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
}, props), /* #__PURE__ */React.createElement('path', {
|
||||
d: 'M 16.7371 4 H 14.2371 V 11.5 H 3.4038 V 14 H 13.4038 L 16.7371 17.3334 V 4 Z M 12.5705 9.8333 V 0.6667 H 0.0705 V 13.1667 L 3.4038 9.8333 H 12.5705 Z M 11.465 8.618 H 1.038 V 1.683 H 11.465Z',
|
||||
fill: 'currentColor',
|
||||
}));
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { _extends } from './common';
|
||||
|
||||
export default function StarFilled(props) {
|
||||
return /* #__PURE__ */React.createElement('svg', _extends({
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
}, props), /* #__PURE__ */React.createElement('path', {
|
||||
d: 'M8.4038 13.3917L13.5538 16.5L12.1871 10.6417L16.7371 6.70002L10.7455 6.19169L8.4038 0.666687L6.06213 6.19169L0.0704651 6.70002L4.62047 10.6417L3.2538 16.5L8.4038 13.3917Z',
|
||||
fill: 'currentColor',
|
||||
}));
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { _extends } from './common';
|
||||
|
||||
export default function StarOutline(props) {
|
||||
return /* #__PURE__ */React.createElement('svg', _extends({
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
}, props), /* #__PURE__ */React.createElement('path', {
|
||||
d: 'M16.7371 6.69999L10.7455 6.18332L8.4038 0.666656L6.06213 6.19166L0.0704651 6.69999L4.62047 10.6417L3.2538 16.5L8.4038 13.3917L13.5538 16.5L12.1955 10.6417L16.7371 6.69999ZM8.4038 11.8333L5.27047 13.725L6.1038 10.1583L3.33713 7.75832L6.98713 7.44166L8.4038 4.08332L9.8288 7.44999L13.4788 7.76666L10.7121 10.1667L11.5455 13.7333L8.4038 11.8333Z',
|
||||
fill: 'currentColor',
|
||||
}));
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { _extends } from './common';
|
||||
|
||||
export default function ThumbUpFilled(props) {
|
||||
return /* #__PURE__ */React.createElement('svg', _extends({
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
}, props), /* #__PURE__ */React.createElement('path', {
|
||||
d: 'M11.2122 0.833344L5.23715 6.81668V17.5H15.4955L18.5705 10.3333V6.66668H11.6455L12.5788 2.18334L11.2122 0.833344ZM0.237152 7.50001H3.57049V17.5H0.237152V7.50001Z',
|
||||
fill: 'currentColor',
|
||||
}));
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { _extends } from './common';
|
||||
|
||||
export default function ThumbUpOutline(props) {
|
||||
return /* #__PURE__ */React.createElement('svg', _extends({
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
}, props), /* #__PURE__ */React.createElement('path', {
|
||||
d: 'M18.5705 6.66668V10.3333L15.4955 17.5L5.23715 17.5L5.23715 6.81668L11.2122 0.833344L12.5788 2.18334L11.6455 6.66668L18.5705 6.66668ZM6.90382 7.50834L6.90382 15.8333L14.3955 15.8333L16.9038 9.99168V8.33334L9.59548 8.33334L10.5205 3.88334L6.90382 7.50834Z M3.57049 17.5H0.237152L0.237152 7.50001H3.57049L3.57049 17.5Z',
|
||||
fill: 'currentColor',
|
||||
}));
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-func-assign */
|
||||
/* eslint-disable prefer-object-spread */
|
||||
/* eslint-disable prefer-rest-params */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function _extends() {
|
||||
_extends = Object.assign || function (target) {
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
const source = arguments[i];
|
||||
for (const key in source) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
return _extends.apply(this, arguments);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export { default as QuestionAnswer } from './QuestionAnswer';
|
||||
export { default as QuestionAnswerOutline } from './QuestionAnswerOutline';
|
||||
export { default as StarFilled } from './StarFilled';
|
||||
export { default as StarOutline } from './StarOutline';
|
||||
export { default as ThumbUpFilled } from './ThumbUpFilled';
|
||||
export { default as ThumbUpOutline } from './ThumbUpOutline';
|
||||
@@ -58,21 +58,19 @@ function normaliseCourseBlocks({
|
||||
} else {
|
||||
blocks[verticalId].children?.forEach(discussionId => {
|
||||
const discussion = camelCaseObject(blocks[discussionId]);
|
||||
const { topicId } = discussion.studentViewData || {};
|
||||
if (topicId) {
|
||||
blockData[discussionId] = discussion;
|
||||
// Add this topic id to the list of topics for the current chapter, sequential, and vertical
|
||||
chapterData.topics.push(topicId);
|
||||
blockData[sequentialId].topics.push(topicId);
|
||||
blockData[verticalId].topics.push(topicId);
|
||||
// Store the topic's context in the course in a map
|
||||
topics[topicId] = {
|
||||
chapterName: blockData[chapterId].displayName,
|
||||
verticalName: blockData[sequentialId].displayName,
|
||||
unitName: blockData[verticalId].displayName,
|
||||
unitLink: blockData[verticalId].lmsWebUrl,
|
||||
};
|
||||
}
|
||||
const { topicId } = discussion.studentViewData;
|
||||
blockData[discussionId] = discussion;
|
||||
// Add this topic id to the list of topics for the current chapter, sequential, and vertical
|
||||
chapterData.topics.push(topicId);
|
||||
blockData[sequentialId].topics.push(topicId);
|
||||
blockData[verticalId].topics.push(topicId);
|
||||
// Store the topic's context in the course in a map
|
||||
topics[topicId] = {
|
||||
chapterName: blockData[chapterId].displayName,
|
||||
verticalName: blockData[sequentialId].displayName,
|
||||
unitName: blockData[verticalId].displayName,
|
||||
unitLink: blockData[verticalId].lmsWebUrl,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ function Comment({
|
||||
<CommentEditor comment={comment} onCloseEditor={() => setEditing(false)} />
|
||||
)
|
||||
// eslint-disable-next-line react/no-danger
|
||||
: <div className="comment-body px-2" id="comment" dangerouslySetInnerHTML={{ __html: comment.renderedBody }} />}
|
||||
: <div className="comment-body px-2" dangerouslySetInnerHTML={{ __html: comment.renderedBody }} />}
|
||||
<CommentIcons
|
||||
comment={comment}
|
||||
following={comment.following}
|
||||
|
||||
@@ -10,7 +10,6 @@ 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';
|
||||
@@ -23,38 +22,14 @@ 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 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 editorRef = useRef(null);
|
||||
const saveUpdatedComment = async (values) => {
|
||||
if (comment.id) {
|
||||
const payload = {
|
||||
...values,
|
||||
editReasonCode: values.editReasonCode || undefined,
|
||||
};
|
||||
await dispatch(editComment(comment.id, payload));
|
||||
await dispatch(editComment(comment.id, values));
|
||||
} else {
|
||||
await dispatch(addComment(values.comment, comment.threadId, comment.parentId));
|
||||
}
|
||||
@@ -67,11 +42,17 @@ 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={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
initialValues={{ comment: comment.rawBody }}
|
||||
validationSchema={Yup.object()
|
||||
.shape({
|
||||
comment: Yup.string()
|
||||
.required(),
|
||||
editReasonCode: Yup.string()
|
||||
.nullable()
|
||||
.default(undefined),
|
||||
})}
|
||||
onSubmit={saveUpdatedComment}
|
||||
>
|
||||
{({
|
||||
@@ -83,13 +64,12 @@ function CommentEditor({
|
||||
handleChange,
|
||||
}) => (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
{canDisplayEditReason && (
|
||||
<Form.Group
|
||||
isInvalid={isFormikFieldInvalid('editReasonCode', {
|
||||
errors,
|
||||
touched,
|
||||
})}
|
||||
>
|
||||
{(reasonCodesEnabled
|
||||
&& userIsPrivileged
|
||||
&& comment.author !== authenticatedUser.username
|
||||
&& edit
|
||||
) && (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
name="editReasonCode"
|
||||
className="mt-2"
|
||||
@@ -108,7 +88,6 @@ function CommentEditor({
|
||||
<option key={code} value={code}>{label}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
<FormikErrorFeedback name="editReasonCode" />
|
||||
</Form.Group>
|
||||
)}
|
||||
<TinyMCEEditor
|
||||
@@ -163,7 +142,6 @@ CommentEditor.propTypes = {
|
||||
parentId: PropTypes.string,
|
||||
rawBody: PropTypes.string,
|
||||
author: PropTypes.string,
|
||||
lastEdit: PropTypes.object,
|
||||
}).isRequired,
|
||||
onCloseEditor: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -45,17 +45,11 @@ function Reply({
|
||||
hideDeleteConfirmation();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="d-flex">
|
||||
<div className="d-flex mx-3 invisible">
|
||||
<Avatar className="m-2" />
|
||||
</div>
|
||||
<div className="w-100">
|
||||
<AlertBanner postType={null} content={reply} intl={intl} />
|
||||
</div>
|
||||
<div className="d-flex flex-fill ml-6">
|
||||
<AlertBanner postType={null} content={reply} intl={intl} />
|
||||
</div>
|
||||
|
||||
<div className="d-flex">
|
||||
|
||||
<div className="d-flex m-3">
|
||||
<Avatar
|
||||
className={`m-2 ${colorClass && `border-${colorClass}`}`}
|
||||
@@ -78,8 +72,9 @@ function Reply({
|
||||
{isEditing
|
||||
? <CommentEditor comment={reply} onCloseEditor={() => setEditing(false)} />
|
||||
// eslint-disable-next-line react/no-danger
|
||||
: <div id="reply" dangerouslySetInnerHTML={{ __html: reply.renderedBody }} />}
|
||||
: <div dangerouslySetInnerHTML={{ __html: reply.renderedBody }} />}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="text-gray-500 align-self-end mt-2" title={reply.createdAt}>
|
||||
{timeago.format(reply.createdAt, intl.locale)}
|
||||
|
||||
@@ -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,11 +147,6 @@ 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',
|
||||
|
||||
@@ -56,7 +56,7 @@ function AlertBanner({
|
||||
</Alert>
|
||||
)}
|
||||
{content.abuseFlagged && (
|
||||
<Alert icon={Error} variant="danger" className="p-3 m-0 shadow-none my-1 flex-fill">
|
||||
<Alert icon={Error} variant="danger" className="p-3 m-0 shadow-none mb-1 flex-fill">
|
||||
{intl.formatMessage(messages.abuseFlaggedMessage)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
|
||||
import { PostActionsBar } from '../../components';
|
||||
import { ALL_ROUTES, DiscussionProvider, Routes } from '../../data/constants';
|
||||
import { useTheme } from '../../theme-hooks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import {
|
||||
useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useSidebarVisible,
|
||||
@@ -21,6 +22,7 @@ import DiscussionSidebar from './DiscussionSidebar';
|
||||
|
||||
export default function DiscussionsHome() {
|
||||
const location = useLocation();
|
||||
const ready = useTheme('red_theme');
|
||||
const postEditorVisible = useSelector(
|
||||
(state) => state.threads.postEditorVisible,
|
||||
);
|
||||
@@ -59,6 +61,9 @@ export default function DiscussionsHome() {
|
||||
postMessageToParent('discussions.navigate', { path });
|
||||
}
|
||||
}, [path]);
|
||||
if (!ready) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DiscussionContext.Provider value={{
|
||||
|
||||
@@ -64,7 +64,6 @@ const learnersSlice = createSlice({
|
||||
nextPage: (payload.page < payload.pagination.numPages) ? payload.page + 1 : null,
|
||||
totalPages: payload.pagination.numPages,
|
||||
};
|
||||
state.status = RequestStatus.SUCCESSFUL;
|
||||
},
|
||||
fetchUserCommentsDenied: (state) => {
|
||||
state.status = RequestStatus.DENIED;
|
||||
|
||||
@@ -58,7 +58,7 @@ function PostsList({ posts, topics }) {
|
||||
// Add a spacing after the group of pinned posts
|
||||
return (
|
||||
<React.Fragment key={post.id}>
|
||||
<div className="p-1 bg-light-400" />
|
||||
<div className="p-1 bg-light-300" />
|
||||
<PostLink post={post} key={post.id} />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -85,7 +85,6 @@ describe('PostsView', () => {
|
||||
|
||||
store = initializeStore({
|
||||
blocks: { blocks: { 'test-usage-key': { topics: ['some-topic-2', 'some-topic-0'] } } },
|
||||
config: { userIsPrivileged: true },
|
||||
});
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -84,21 +83,14 @@ 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.
|
||||
@@ -172,48 +164,60 @@ function PostEditor({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 || '',
|
||||
let initialValues = {
|
||||
postType: 'discussion',
|
||||
topic: topicId || nonCoursewareTopics?.[0]?.id,
|
||||
title: '',
|
||||
comment: '',
|
||||
follow: true,
|
||||
anonymous: false,
|
||||
anonymousToPeers: false,
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
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 postEditorId = `post-editor-${editExisting ? postId : 'new'}`;
|
||||
|
||||
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
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),
|
||||
})}
|
||||
onSubmit={submitForm}
|
||||
>{
|
||||
({
|
||||
@@ -300,7 +304,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="w-100"
|
||||
className="d-flex flex-fill"
|
||||
isInvalid={isFormikFieldInvalid('title', {
|
||||
errors,
|
||||
touched,
|
||||
@@ -317,31 +321,27 @@ function PostEditor({
|
||||
/>
|
||||
<FormikErrorFeedback name="title" />
|
||||
</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>
|
||||
{(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>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-2">
|
||||
|
||||
@@ -106,11 +106,6 @@ 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;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Collapsible, Form, Icon } from '@edx/paragon';
|
||||
import { Check, Sort } from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
PostsStatusFilter, ThreadOrdering, ThreadType,
|
||||
} from '../../../data/constants';
|
||||
import { selectUserIsPrivileged } from '../../data/selectors';
|
||||
import { setPostsTypeFilter, setSortedBy, setStatusFilter } from '../data';
|
||||
import { selectThreadFilters, selectThreadSorting } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
@@ -44,8 +44,8 @@ function PostFilterBar({
|
||||
filterSelfPosts,
|
||||
intl,
|
||||
}) {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const dispatch = useDispatch();
|
||||
const userIsPrivileged = useSelector(selectUserIsPrivileged);
|
||||
const currentSorting = useSelector(selectThreadSorting());
|
||||
const currentFilters = useSelector(selectThreadFilters());
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
@@ -147,7 +147,7 @@ function PostFilterBar({
|
||||
value={PostsStatusFilter.FOLLOWING}
|
||||
selected={currentFilters.status}
|
||||
/>
|
||||
{userIsPrivileged
|
||||
{authenticatedUser.administrator
|
||||
&& (
|
||||
<ActionItem
|
||||
id="status-reported"
|
||||
|
||||
@@ -5,8 +5,8 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon, IconButton, OverlayTrigger, Tooltip,
|
||||
} from '@edx/paragon';
|
||||
import { ThumbUpFilled, ThumbUpOutline } from '@edx/paragon/icons';
|
||||
|
||||
import { ThumbUpFilled, ThumbUpOutline } from '../../../components/icons';
|
||||
import messages from './messages';
|
||||
|
||||
function LikeButton({
|
||||
@@ -34,7 +34,7 @@ function LikeButton({
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleClick}
|
||||
className="p-3 mr-2 mt-1"
|
||||
className="p-3 mr-2"
|
||||
alt="Like"
|
||||
iconAs={Icon}
|
||||
size="inline"
|
||||
|
||||
@@ -75,7 +75,7 @@ function Post({
|
||||
<PostHeader post={post} actionHandlers={actionHandlers} />
|
||||
<div className="d-flex my-2 text-break">
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<div id="post" dangerouslySetInnerHTML={{ __html: post.renderedBody }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: post.renderedBody }} />
|
||||
</div>
|
||||
{topicContext && topic && (
|
||||
<div className="border p-3 rounded mb-3 mt-2 align-self-start">
|
||||
|
||||
@@ -9,15 +9,9 @@ import {
|
||||
Badge, Icon, IconButton, OverlayTrigger, Tooltip,
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
Locked, People,
|
||||
Locked, People, QuestionAnswer, QuestionAnswerOutline, StarFilled, StarOutline,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
QuestionAnswer,
|
||||
QuestionAnswerOutline,
|
||||
StarFilled,
|
||||
StarOutline,
|
||||
} from '../../../components/icons';
|
||||
import { updateExistingThread } from '../data/thunks';
|
||||
import LikeButton from './LikeButton';
|
||||
import messages from './messages';
|
||||
@@ -51,14 +45,14 @@ function PostFooter({
|
||||
alt="Follow"
|
||||
iconAs={Icon}
|
||||
size="inline"
|
||||
className="mx-2.5 my-0 mt-1.5"
|
||||
className="mx-2.5 my-0"
|
||||
src={post.following ? StarFilled : StarOutline}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
{preview && post.commentCount > 1
|
||||
&& (
|
||||
<>
|
||||
<Icon src={post.unreadCommentCount ? QuestionAnswer : QuestionAnswerOutline} className="mx-2 my-0 mt-2" />
|
||||
<Icon src={post.unreadCommentCount ? QuestionAnswer : QuestionAnswerOutline} className="mx-2 my-0" />
|
||||
<span style={{ minWidth: '2rem' }}>
|
||||
{post.commentCount}
|
||||
</span>
|
||||
|
||||
@@ -47,7 +47,7 @@ function PostLink({
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames('d-flex flex-row flex-fill mw-100 p-2.5 border-primary-500', { 'bg-light-300': post.read })}
|
||||
className={classNames('d-flex flex-row flex-fill mw-100 p-1 border-primary-500', { 'bg-light-300': post.read })}
|
||||
style={post.id === postId ? {
|
||||
borderRightWidth: '4px',
|
||||
borderRightStyle: 'solid',
|
||||
|
||||
@@ -14,7 +14,6 @@ import appMessages from './i18n';
|
||||
import store from './store';
|
||||
|
||||
import './assets/favicon.ico';
|
||||
import './index.scss';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
@@ -38,6 +37,7 @@ initialize({
|
||||
config() {
|
||||
mergeConfig({
|
||||
POST_MARK_AS_READ_DELAY: process.env.POST_MARK_AS_READ_DELAY || 2000,
|
||||
THEME_LOADER_URL: process.env.THEME_LOADER_URL || 'http://localhost:8111/themes.js',
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,11 +5,3 @@
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
|
||||
#post, #comment, #reply {
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
94
src/theme-hooks.js
Normal file
94
src/theme-hooks.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export async function loadComponent(scope, module) {
|
||||
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
|
||||
// Webpack module federation allows sharing common dependencies, like `react`, `react-dom` etc
|
||||
// between modules so that they are no loaded multiple times. The following line will initialise
|
||||
// the system for sharing common modules.
|
||||
// Since there are no shared module here, you can safely comment out the next three lines and this
|
||||
// will still work.
|
||||
// eslint-disable-next-line no-undef
|
||||
await __webpack_init_sharing__('default');
|
||||
const container = window[scope];
|
||||
// eslint-disable-next-line no-undef
|
||||
await container.init(__webpack_share_scopes__.default);
|
||||
const factory = await window[scope].get(module);
|
||||
return factory();
|
||||
} // The hook loads the supplied theme, and if the current theme changes it will
|
||||
// Given a script URL this hook will add the script to the body. When the url
|
||||
// changes it will unload the previous script.
|
||||
// For theming, if we know where the script will come from in advance we can just
|
||||
// include it in the HTML and not load it at runtime. However, that would
|
||||
// require supplying that as a build-time value. Keeping this dynamic allows us
|
||||
// to use it
|
||||
function useDynamicScript(url) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const element = document.createElement('script');
|
||||
|
||||
element.src = url;
|
||||
element.type = 'text/javascript';
|
||||
element.async = true;
|
||||
|
||||
setReady(false);
|
||||
setFailed(false);
|
||||
|
||||
element.onload = () => {
|
||||
console.log(`Dynamic Script Loaded: ${url}`);
|
||||
setReady(true);
|
||||
};
|
||||
|
||||
element.onerror = () => {
|
||||
console.error(`Dynamic Script Error: ${url}`);
|
||||
setReady(false);
|
||||
setFailed(true);
|
||||
};
|
||||
|
||||
document.head.appendChild(element);
|
||||
|
||||
return () => {
|
||||
console.log(`Dynamic Script Removed: ${url}`);
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return {
|
||||
ready,
|
||||
failed,
|
||||
};
|
||||
}
|
||||
|
||||
// unload the previous theme and load the new one.
|
||||
export function useTheme(theme) {
|
||||
const { ready } = useDynamicScript(getConfig().THEME_LOADER_URL);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!ready) {
|
||||
return undefined;
|
||||
}
|
||||
let styles = null;
|
||||
(async () => {
|
||||
const themeComponent = await loadComponent(theme, './theme');
|
||||
styles = themeComponent.styles;
|
||||
|
||||
themeComponent.styles.use();
|
||||
setLoaded(true);
|
||||
})();
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
return () => {
|
||||
if (styles) {
|
||||
styles.unuse();
|
||||
}
|
||||
setLoaded(false);
|
||||
};
|
||||
}, [ready]);
|
||||
return loaded;
|
||||
}
|
||||
Reference in New Issue
Block a user