Compare commits
8 Commits
Ayesha/INF
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a2fa404b | ||
|
|
472bbe2d96 | ||
|
|
dc5f097736 | ||
|
|
5e8c8254b4 | ||
|
|
0d6692cf8c | ||
|
|
3391e966f3 | ||
|
|
4297a96102 | ||
|
|
db883ca7cd |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -9,17 +9,19 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Validate package-lock.json changes
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
15501
package-lock.json
generated
15501
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,8 +34,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-header": "^5.0.2",
|
||||
"@edx/frontend-platform": "^7.1.0",
|
||||
"@edx/frontend-component-header": "^5.6.0",
|
||||
"@edx/frontend-platform": "8.0.0",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.1.1",
|
||||
@@ -64,7 +64,7 @@
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@openedx/frontend-build": "^13.0.28",
|
||||
"@openedx/frontend-build": "14.0.3",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
@@ -74,7 +74,7 @@
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"glob": "7.2.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"jest": "29.7.0",
|
||||
"rosie": "2.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
74
src/components/PostHelpPanel.jsx
Normal file
74
src/components/PostHelpPanel.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Hyperlink, Icon, IconButton, IconButtonWithTooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { Close, HelpOutline } from '@openedx/paragon/icons';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../discussions/posts/post-editor/messages';
|
||||
|
||||
const PostHelpPanel = () => {
|
||||
const intl = useIntl();
|
||||
const [showHelpPane, setShowHelpPane] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex justify-content-end">
|
||||
<IconButtonWithTooltip
|
||||
onClick={() => setShowHelpPane(true)}
|
||||
alt={intl.formatMessage(messages.showHelpIcon)}
|
||||
tooltipContent={<div>{intl.formatMessage(messages.discussionHelpTooltip)}</div>}
|
||||
src={HelpOutline}
|
||||
iconAs={Icon}
|
||||
size="inline"
|
||||
className="float-right p-3 help-icon"
|
||||
iconClassNames="help-icon-size"
|
||||
data-testid="help-button"
|
||||
invertColors
|
||||
isActive
|
||||
/>
|
||||
</div>
|
||||
{showHelpPane && (
|
||||
<div
|
||||
className="w-100 p-2 bg-light-200 rounded box-shadow-down-1 post-preview overflow-auto my-3"
|
||||
style={{ minHeight: '200px', wordBreak: 'break-word' }}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => setShowHelpPane(false)}
|
||||
alt={intl.formatMessage(messages.actionsAlt)}
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
size="inline"
|
||||
className="float-right p-3"
|
||||
iconClassNames="icon-size"
|
||||
data-testid="hide-help-button"
|
||||
/>
|
||||
<div className="pt-2 px-3">
|
||||
<h4 className="font-weight-bold">{intl.formatMessage(messages.discussionHelpHeader)}</h4>
|
||||
<p className="pt-2">{intl.formatMessage(messages.discussionHelpDescription)}</p>
|
||||
<Hyperlink
|
||||
target="_blank"
|
||||
className="w-100"
|
||||
destination="https://support.edx.org/hc/en-us/sections/115004169687-Participating-in-Course-Discussions"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{intl.formatMessage(messages.discussionHelpCourseParticipation)}
|
||||
</Hyperlink>
|
||||
<Hyperlink
|
||||
target="_blank"
|
||||
className="w-100"
|
||||
destination="https://support.edx.org/hc/en-us/articles/360000035267-Entering-math-expressions-in-course-discussions"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{intl.formatMessage(messages.discussionHelpMathExpressions)}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PostHelpPanel);
|
||||
@@ -94,6 +94,7 @@ const ActionsDropdown = ({
|
||||
handleActions(action.action);
|
||||
}}
|
||||
className="d-flex justify-content-start actions-dropdown-item"
|
||||
data-testId={action.id}
|
||||
>
|
||||
<Icon
|
||||
src={action.icon}
|
||||
|
||||
@@ -671,6 +671,151 @@ describe('ThreadView', () => {
|
||||
expect(screen.queryByTestId('reply-comment-3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('successfully added comment in the draft.', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add comment'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment!' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Cancel'));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add comment'));
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Draft comment!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('successfully updated comment in the draft.', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const comment = screen.queryByTestId('reply-comment-2');
|
||||
const actionBtn = comment.querySelector('button[aria-label="Actions menu"]');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(actionBtn);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByTestId('edit'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment!' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Cancel'));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(actionBtn);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByTestId('edit'));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Submit'));
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('Draft comment!')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('successfully removed comment from the draft.', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add comment'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment 123!' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Submit'));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryAllByText('Add comment')[0]);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('tinymce-editor').value).toBe('');
|
||||
});
|
||||
|
||||
it('successfully added response in the draft.', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add response'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft Response!' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Cancel'));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add response'));
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Draft Response!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('successfully removed response from the draft.', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add response'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft Response!' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Submit'));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add response'));
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('tinymce-editor').value).toBe('');
|
||||
});
|
||||
|
||||
it('successfully maintain response for the specific post in the draft.', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add response'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Hello, world!' } });
|
||||
});
|
||||
|
||||
await waitFor(() => renderComponent('thread-2'));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryAllByText('Add response')[0]);
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Hello, world!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of replies', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useRef,
|
||||
useCallback, useContext, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -22,7 +22,9 @@ import {
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsStaff,
|
||||
} from '../../../data/selectors';
|
||||
import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
|
||||
import { extractContent, formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
|
||||
import { useDraftContent } from '../../data/hooks';
|
||||
import { setDraftComments, setDraftResponses } from '../../data/slices';
|
||||
import { addComment, editComment } from '../../data/thunks';
|
||||
import messages from '../../messages';
|
||||
|
||||
@@ -45,6 +47,8 @@ const CommentEditor = ({
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const { editReasons } = useSelector(selectModerationSettings);
|
||||
const [submitting, dispatch] = useDispatchWithState();
|
||||
const [editorContent, setEditorContent] = useState();
|
||||
const { addDraftContent, getDraftContent, removeDraftContent } = useDraftContent();
|
||||
|
||||
const canDisplayEditReason = (edit
|
||||
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
|
||||
@@ -62,7 +66,7 @@ const CommentEditor = ({
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
comment: rawBody,
|
||||
comment: editorContent,
|
||||
editReasonCode: lastEdit?.reasonCode || (userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined),
|
||||
};
|
||||
|
||||
@@ -71,6 +75,15 @@ const CommentEditor = ({
|
||||
onCloseEditor();
|
||||
}, [onCloseEditor, initialValues]);
|
||||
|
||||
const deleteEditorContent = useCallback(async () => {
|
||||
const { updatedResponses, updatedComments } = removeDraftContent(parentId, id, threadId);
|
||||
if (parentId) {
|
||||
await dispatch(setDraftComments(updatedComments));
|
||||
} else {
|
||||
await dispatch(setDraftResponses(updatedResponses));
|
||||
}
|
||||
}, [parentId, id, threadId, setDraftComments, setDraftResponses]);
|
||||
|
||||
const saveUpdatedComment = useCallback(async (values, { resetForm }) => {
|
||||
if (id) {
|
||||
const payload = {
|
||||
@@ -86,6 +99,7 @@ const CommentEditor = ({
|
||||
editorRef.current.plugins.autosave.removeDraft();
|
||||
}
|
||||
handleCloseEditor(resetForm);
|
||||
deleteEditorContent();
|
||||
}, [id, threadId, parentId, enableInContextSidebar, handleCloseEditor]);
|
||||
// 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.
|
||||
@@ -97,11 +111,33 @@ const CommentEditor = ({
|
||||
}
|
||||
}, [formRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const draftHtml = getDraftContent(parentId, threadId, id) || rawBody;
|
||||
setEditorContent(draftHtml);
|
||||
}, [parentId, threadId, id]);
|
||||
|
||||
const saveDraftContent = async (content) => {
|
||||
const draftDataContent = extractContent(content);
|
||||
|
||||
const { updatedResponses, updatedComments } = addDraftContent(
|
||||
draftDataContent,
|
||||
parentId,
|
||||
id,
|
||||
threadId,
|
||||
);
|
||||
if (parentId) {
|
||||
await dispatch(setDraftComments(updatedComments));
|
||||
} else {
|
||||
await dispatch(setDraftResponses(updatedResponses));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={saveUpdatedComment}
|
||||
enableReinitialize
|
||||
>
|
||||
{({
|
||||
values,
|
||||
@@ -151,7 +187,10 @@ const CommentEditor = ({
|
||||
id={editorId}
|
||||
value={values.comment}
|
||||
onEditorChange={formikCompatibleHandler(handleChange, 'comment')}
|
||||
onBlur={formikCompatibleHandler(handleBlur, 'comment')}
|
||||
onBlur={(content) => {
|
||||
formikCompatibleHandler(handleChange, 'comment');
|
||||
saveDraftContent(content);
|
||||
}}
|
||||
/>
|
||||
{isFormikFieldInvalid('comment', {
|
||||
errors,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
|
||||
@@ -13,7 +14,8 @@ import { selectThread } from '../../posts/data/selectors';
|
||||
import { markThreadAsRead } from '../../posts/data/thunks';
|
||||
import { filterPosts } from '../../utils';
|
||||
import {
|
||||
selectCommentSortOrder, selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages,
|
||||
selectCommentSortOrder, selectDraftComments, selectDraftResponses,
|
||||
selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages,
|
||||
} from './selectors';
|
||||
import { fetchThreadComments } from './thunks';
|
||||
|
||||
@@ -102,3 +104,73 @@ export function useCommentsCount(postId) {
|
||||
|
||||
return commentsLength;
|
||||
}
|
||||
|
||||
export const useDraftContent = () => {
|
||||
const comments = useSelector(selectDraftComments);
|
||||
const responses = useSelector(selectDraftResponses);
|
||||
|
||||
const getObjectByParentId = (data, parentId, isComment, id) => Object.values(data)
|
||||
.find(draft => (isComment ? draft.parentId === parentId && (id ? draft.id === id : draft.isNewContent === true)
|
||||
: draft.threadId === parentId && (id ? draft.id === id : draft.isNewContent === true)));
|
||||
|
||||
const updateDraftData = (draftData, newDraftObject) => ({
|
||||
...draftData,
|
||||
[newDraftObject.id]: newDraftObject,
|
||||
});
|
||||
|
||||
const addDraftContent = (content, parentId, id, threadId) => {
|
||||
const data = parentId ? comments : responses;
|
||||
const draftParentId = parentId || threadId;
|
||||
const isComment = !!parentId;
|
||||
const existingObj = getObjectByParentId(data, draftParentId, isComment, id);
|
||||
const newObject = existingObj
|
||||
? { ...existingObj, content }
|
||||
: {
|
||||
threadId,
|
||||
content,
|
||||
parentId,
|
||||
id: id || uuidv4(),
|
||||
isNewContent: !id,
|
||||
};
|
||||
|
||||
const updatedComments = parentId ? updateDraftData(comments, newObject) : comments;
|
||||
const updatedResponses = !parentId ? updateDraftData(responses, newObject) : responses;
|
||||
|
||||
return { updatedComments, updatedResponses };
|
||||
};
|
||||
|
||||
const getDraftContent = (parentId, threadId, id) => {
|
||||
if (id) {
|
||||
return parentId ? comments?.[id]?.content : responses?.[id]?.content;
|
||||
}
|
||||
|
||||
const data = parentId ? comments : responses;
|
||||
const draftParentId = parentId || threadId;
|
||||
const isComment = !!parentId;
|
||||
|
||||
return getObjectByParentId(data, draftParentId, isComment, id)?.content;
|
||||
};
|
||||
|
||||
const removeItem = (draftData, objId) => {
|
||||
const { [objId]: _, ...newDraftData } = draftData;
|
||||
return newDraftData;
|
||||
};
|
||||
|
||||
const updateContent = (items, itemId, parentId, isComment) => {
|
||||
const itemObj = itemId ? items[itemId] : getObjectByParentId(items, parentId, isComment, itemId);
|
||||
return itemObj ? removeItem(items, itemObj.id) : items;
|
||||
};
|
||||
|
||||
const removeDraftContent = (parentId, id, threadId) => {
|
||||
const updatedResponses = !parentId ? updateContent(responses, id, threadId, false) : responses;
|
||||
const updatedComments = parentId ? updateContent(comments, id, parentId, true) : comments;
|
||||
|
||||
return { updatedResponses, updatedComments };
|
||||
};
|
||||
|
||||
return {
|
||||
addDraftContent,
|
||||
getDraftContent,
|
||||
removeDraftContent,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -47,3 +47,7 @@ export const selectCommentCurrentPage = commentId => (
|
||||
export const selectCommentsStatus = state => state.comments.status;
|
||||
|
||||
export const selectCommentSortOrder = state => state.comments.sortOrder;
|
||||
|
||||
export const selectDraftComments = state => state.comments.draftComments;
|
||||
|
||||
export const selectDraftResponses = state => state.comments.draftResponses;
|
||||
|
||||
@@ -22,6 +22,8 @@ const commentsSlice = createSlice({
|
||||
pagination: {},
|
||||
responsesPagination: {},
|
||||
sortOrder: true,
|
||||
draftResponses: {},
|
||||
draftComments: {},
|
||||
},
|
||||
reducers: {
|
||||
fetchCommentsRequest: (state) => (
|
||||
@@ -257,6 +259,18 @@ const commentsSlice = createSlice({
|
||||
sortOrder: payload,
|
||||
}
|
||||
),
|
||||
setDraftComments: (state, { payload }) => (
|
||||
{
|
||||
...state,
|
||||
draftComments: payload,
|
||||
}
|
||||
),
|
||||
setDraftResponses: (state, { payload }) => (
|
||||
{
|
||||
...state,
|
||||
draftResponses: payload,
|
||||
}
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -282,6 +296,8 @@ export const {
|
||||
deleteCommentRequest,
|
||||
deleteCommentSuccess,
|
||||
setCommentSortOrder,
|
||||
setDraftComments,
|
||||
setDraftResponses,
|
||||
} = commentsSlice.actions;
|
||||
|
||||
export const commentsReducer = commentsSlice.reducer;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { TinyMCEEditor } from '../../../components';
|
||||
import FormikErrorFeedback from '../../../components/FormikErrorFeedback';
|
||||
import PostHelpPanel from '../../../components/PostHelpPanel';
|
||||
import PostPreviewPanel from '../../../components/PostPreviewPanel';
|
||||
import useDispatchWithState from '../../../data/hooks';
|
||||
import selectCourseCohorts from '../../cohorts/data/selectors';
|
||||
@@ -409,6 +410,7 @@ const PostEditor = ({
|
||||
onEditorChange={formikCompatibleHandler(handleChange, 'comment')}
|
||||
onBlur={formikCompatibleHandler(handleBlur, 'comment')}
|
||||
/>
|
||||
<PostHelpPanel />
|
||||
<FormikErrorFeedback name="comment" />
|
||||
</div>
|
||||
<PostPreviewPanel htmlNode={values.comment} isPost editExisting={editExisting} />
|
||||
|
||||
@@ -368,5 +368,34 @@ describe('PostEditor', () => {
|
||||
expect(container.querySelector('[data-testid="hide-preview-button"]')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show Help Panel', async () => {
|
||||
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(container.querySelector('[data-testid="help-button"]'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('[data-testid="hide-help-button"]')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide Help Panel', async () => {
|
||||
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(container.querySelector('[data-testid="help-button"]'));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(container.querySelector('[data-testid="hide-help-button"]'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('[data-testid="help-button"]')).toBeInTheDocument();
|
||||
expect(container.querySelector('[data-testid="hide-help-button"]')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,6 +116,36 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Show preview',
|
||||
description: 'show preview button text to allow user to see their post content.',
|
||||
},
|
||||
showHelpIcon: {
|
||||
id: 'discussions.editor.posts.showHelp.icon',
|
||||
defaultMessage: 'Show Help',
|
||||
description: 'show help icon to allow user to see important documentation.',
|
||||
},
|
||||
discussionHelpHeader: {
|
||||
id: 'discussions.editor.posts.discussionHelpHeader',
|
||||
defaultMessage: 'Discussions help',
|
||||
description: 'header text for post help section.',
|
||||
},
|
||||
discussionHelpDescription: {
|
||||
id: 'discussions.editor.posts.discussionHelpDescription',
|
||||
defaultMessage: 'Course discussions give you the opportunity to start conversations, ask questions, and interact with other learners. See the links below to learn more:',
|
||||
description: 'description message for post help section.',
|
||||
},
|
||||
discussionHelpCourseParticipation: {
|
||||
id: 'discussions.editor.posts.discussionHelpCourseParticipation',
|
||||
defaultMessage: 'Participating in course discussions',
|
||||
description: 'Documentation link title for participating in course discussions.',
|
||||
},
|
||||
discussionHelpMathExpressions: {
|
||||
id: 'discussions.editor.posts.discussionHelpMathExpressions',
|
||||
defaultMessage: 'Entering math expressions in course discussions',
|
||||
description: 'Documentation link title for entering math expressions in course discussions.',
|
||||
},
|
||||
discussionHelpTooltip: {
|
||||
id: 'discussions.editor.posts.discussionHelpTooltip',
|
||||
defaultMessage: 'Learn more about MathJax & LaTeX',
|
||||
description: 'Tooltip help message for documentation help.',
|
||||
},
|
||||
actionsAlt: {
|
||||
id: 'discussions.actions.label',
|
||||
defaultMessage: 'Actions menu',
|
||||
|
||||
@@ -316,3 +316,10 @@ export function getAuthorLabel(intl, authorLabel) {
|
||||
}
|
||||
|
||||
export const isCourseStatusValid = (courseStatus) => [DENIED, LOADED].includes(courseStatus);
|
||||
|
||||
export const extractContent = (content) => {
|
||||
if (typeof content === 'object') {
|
||||
return content.target.getContent();
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
@@ -593,6 +593,15 @@ th, td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
margin: -47px -3px 0px 0px;
|
||||
}
|
||||
|
||||
.help-icon-size {
|
||||
height: 16px !important;
|
||||
width: 16px !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 367px) {
|
||||
|
||||
.discussion-comments h5,
|
||||
@@ -603,7 +612,8 @@ th, td {
|
||||
.pgn__modal,
|
||||
.pgn__form-label,
|
||||
.dropdown-menu,
|
||||
.tox-tbtn {
|
||||
.tox-tbtn,
|
||||
.tooltip {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
@@ -640,7 +650,8 @@ th, td {
|
||||
.pgn__form-label,
|
||||
.pgn__modal,
|
||||
.dropdown-menu,
|
||||
.tox-tbtn {
|
||||
.tox-tbtn,
|
||||
.tooltip {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
@@ -659,7 +670,8 @@ th, td {
|
||||
@media only screen and (min-width: 769px) {
|
||||
|
||||
body,
|
||||
#main {
|
||||
#main,
|
||||
.tooltip {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,25 +19,35 @@ Object.defineProperty(window, 'matchMedia', {
|
||||
})),
|
||||
});
|
||||
|
||||
global.MathJax = {
|
||||
typeset: jest.fn(callback => {
|
||||
if (callback) { callback(); }
|
||||
}),
|
||||
startup: {
|
||||
defaultPageReady: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
};
|
||||
|
||||
// Provides a mock editor component that functions like tinyMCE without the overhead
|
||||
const MockEditor = ({
|
||||
onBlur,
|
||||
onEditorChange,
|
||||
value,
|
||||
}) => (
|
||||
<textarea
|
||||
data-testid="tinymce-editor"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onEditorChange(event.currentTarget.value);
|
||||
}}
|
||||
onBlur={event => {
|
||||
onBlur(event.currentTarget.value);
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
);
|
||||
|
||||
MockEditor.propTypes = {
|
||||
onBlur: PropTypes.func.isRequired,
|
||||
onEditorChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
jest.mock('@tinymce/tinymce-react', () => {
|
||||
const originalModule = jest.requireActual('@tinymce/tinymce-react');
|
||||
|
||||
Reference in New Issue
Block a user