Compare commits
33 Commits
open-relea
...
frontend-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac471e2dd7 | ||
|
|
f04429f6f7 | ||
|
|
bad12462f5 | ||
|
|
ec915f622b | ||
|
|
60da5eafc4 | ||
|
|
05cf174335 | ||
|
|
ff72dab001 | ||
|
|
c38887ec2b | ||
|
|
58aa512f47 | ||
|
|
62a5c11f52 | ||
|
|
3ef8515891 | ||
|
|
3cc39d83c4 | ||
|
|
af6cd1853c | ||
|
|
79a2fa404b | ||
|
|
472bbe2d96 | ||
|
|
dc5f097736 | ||
|
|
5e8c8254b4 | ||
|
|
0d6692cf8c | ||
|
|
3391e966f3 | ||
|
|
4297a96102 | ||
|
|
db883ca7cd | ||
|
|
422fbf6173 | ||
|
|
e862ee6fb1 | ||
|
|
eeae6d45ce | ||
|
|
71b88bcea3 | ||
|
|
c808069fe1 | ||
|
|
b9543c6d9c | ||
|
|
a545d0b9f6 | ||
|
|
8d86e6dcc0 | ||
|
|
37781566f5 | ||
|
|
50948acfeb | ||
|
|
4de1011780 | ||
|
|
d7474782b4 |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -9,17 +9,16 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Validate package-lock.json changes
|
||||
@@ -33,4 +32,7 @@ jobs:
|
||||
- name: i18n_extract
|
||||
run: npm run i18n_extract
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
module.config.js
|
||||
env.config.*
|
||||
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
|
||||
@@ -76,7 +76,7 @@ How to Contribute
|
||||
|
||||
Details about how to become a contributor to the Open edX project may be found in the wiki at `How to contribute`_
|
||||
|
||||
.. _How to contribute: https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/process/index.html
|
||||
.. _How to contribute: https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html
|
||||
|
||||
PR description template should be automatically applied if you are sending PR from github interface; otherwise you
|
||||
can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend-app-discussions/blob/master/.github/pull_request_template.md>`_
|
||||
@@ -125,4 +125,4 @@ Please see `edx/frontend-platform's i18n module <https://edx.github.io/frontend-
|
||||
Reporting Security Issues
|
||||
=========================
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
icon: "Web"
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:edx-infinity
|
||||
type: 'website'
|
||||
|
||||
11
openedx.yaml
11
openedx.yaml
@@ -1,11 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
nick: tmpa
|
||||
oeps: {}
|
||||
owner: edx/arch-team
|
||||
openedx-release:
|
||||
# The openedx-release key is described in OEP-10:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html
|
||||
# The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ
|
||||
ref: master
|
||||
18730
package-lock.json
generated
18730
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -16,13 +16,9 @@
|
||||
"lint:fix": "fedx-scripts eslint --ext .js --ext .jsx . --fix",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/discussions/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/openedx/frontend-app-discussions#readme",
|
||||
@@ -34,13 +30,13 @@
|
||||
},
|
||||
"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-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-platform": "^8.3.3",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.1.1",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
"@tinymce/tinymce-react": "3.13.1",
|
||||
"@tinymce/tinymce-react": "5.1.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.21.1",
|
||||
@@ -49,8 +45,8 @@
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"raw-loader": "4.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "6.18.0",
|
||||
@@ -64,17 +60,16 @@
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@openedx/frontend-build": "^13.0.28",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"axios": "^0.28.0",
|
||||
"axios-mock-adapter": "1.22.0",
|
||||
"babel-plugin-react-intl": "8.2.25",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useDebounce } from '../discussions/data/hooks';
|
||||
|
||||
const defaultSanitizeOptions = {
|
||||
USE_PROFILES: { html: true },
|
||||
ADD_ATTR: ['columnalign'],
|
||||
ADD_ATTR: ['columnalign', 'target'],
|
||||
};
|
||||
|
||||
const HTMLLoader = ({
|
||||
|
||||
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);
|
||||
@@ -79,7 +79,7 @@ const ActionsDropdown = ({
|
||||
placement="bottom-end"
|
||||
>
|
||||
<div
|
||||
className="bg-white shadow d-flex flex-column"
|
||||
className="bg-white shadow d-flex flex-column mt-1"
|
||||
data-testid="actions-dropdown-modal-popup"
|
||||
>
|
||||
{actions.map(action => (
|
||||
@@ -94,12 +94,13 @@ const ActionsDropdown = ({
|
||||
handleActions(action.action);
|
||||
}}
|
||||
className="d-flex justify-content-start actions-dropdown-item"
|
||||
data-testId={action.id}
|
||||
>
|
||||
<Icon
|
||||
src={action.icon}
|
||||
className="icon-size-24"
|
||||
/>
|
||||
<span className="font-weight-normal font-xl ml-2">
|
||||
<span className="font-weight-normal ml-2">
|
||||
{intl.formatMessage(action.label)}
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
|
||||
@@ -38,7 +38,7 @@ const AuthorLabel = ({
|
||||
|
||||
const authorName = useMemo(() => (
|
||||
<span
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500 author-name', {
|
||||
className={classNames('mr-1.5 font-style font-weight-500 author-name', {
|
||||
'text-gray-700': isRetiredUser,
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser,
|
||||
})}
|
||||
@@ -71,7 +71,7 @@ const AuthorLabel = ({
|
||||
/>
|
||||
{authorLabelMessage && (
|
||||
<span
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
|
||||
className={classNames('mr-1.5 font-style font-weight-500', {
|
||||
'text-primary-500': showTextPrimary,
|
||||
'text-gray-700': isRetiredUser,
|
||||
})}
|
||||
@@ -85,7 +85,7 @@ const AuthorLabel = ({
|
||||
{postCreatedAt && (
|
||||
<span
|
||||
title={postCreatedAt}
|
||||
className={classNames('align-content-center', {
|
||||
className={classNames('align-content-center post-summary-timestamp', {
|
||||
'text-white': alert,
|
||||
'text-gray-500': !alert,
|
||||
})}
|
||||
|
||||
@@ -46,7 +46,7 @@ const HoverCard = ({
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className={classNames(
|
||||
'px-2.5 py-2 border-0 font-style text-gray-700 font-size-12',
|
||||
'px-2.5 py-2 border-0 font-style text-gray-700',
|
||||
{ 'w-100': enableInContextSidebar },
|
||||
)}
|
||||
onClick={() => handleResponseCommentButton()}
|
||||
|
||||
@@ -106,8 +106,8 @@ describe('HoverCard', () => {
|
||||
});
|
||||
|
||||
test('it should have hover card on post', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
const post = screen.getByTestId('post-thread-1');
|
||||
renderComponent(discussionPostId);
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
expect(within(post).getByTestId('hover-card-thread-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const ContentUnavailable = ({ subTitleMessage }) => {
|
||||
{intl.formatMessage(messages.contentUnavailableTitle)}
|
||||
</h3>
|
||||
<p className="pb-2 text-gray-500 text-center">{intl.formatMessage(subTitleMessage)}</p>
|
||||
<Button onClick={redirectToDashboard} variant="outline-dark" className="font-size-14 py-2 px-2.5">
|
||||
<Button onClick={redirectToDashboard} variant="outline-dark" className="py-2 px-2.5">
|
||||
{intl.formatMessage(messages.contentUnavailableAction)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,10 @@ ensureConfig([
|
||||
'LMS_BASE_URL',
|
||||
], 'Posts API service');
|
||||
|
||||
export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
|
||||
export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v2/courses/`;
|
||||
export const getCourseSettingsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
|
||||
export const getDiscussionsConfigUrl = (courseId) => `${getCourseConfigApiUrl()}${courseId}/`;
|
||||
export const getDiscussionsSettingsUrl = (courseId) => `${getCourseSettingsApiUrl()}${courseId}/settings`;
|
||||
/**
|
||||
* Get discussions course config
|
||||
* @param {string} courseId
|
||||
@@ -21,7 +23,7 @@ export async function getDiscussionsConfig(courseId) {
|
||||
* @param {string} courseId
|
||||
*/
|
||||
export async function getDiscussionsSettings(courseId) {
|
||||
const url = `${getDiscussionsConfigUrl(courseId)}settings`;
|
||||
const url = `${getDiscussionsSettingsUrl(courseId)}`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -33,14 +33,11 @@ import { ContentSelectors } from './constants';
|
||||
import {
|
||||
selectAreThreadsFiltered,
|
||||
selectEnableInContext,
|
||||
selectIsCourseAdmin,
|
||||
selectIsCourseStaff,
|
||||
selectIsPostingEnabled,
|
||||
selectIsUserLearner,
|
||||
selectPostThreadCount,
|
||||
selectUserHasModerationPrivileges,
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsStaff,
|
||||
} from './selectors';
|
||||
import fetchCourseConfig from './thunks';
|
||||
|
||||
@@ -220,12 +217,9 @@ export const useCurrentDiscussionTopic = () => {
|
||||
|
||||
export const useUserPostingEnabled = () => {
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const isUserAdmin = useSelector(selectUserIsStaff);
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const isUserGroupTA = useSelector(selectUserIsGroupTa);
|
||||
const isCourseAdmin = useSelector(selectIsCourseAdmin);
|
||||
const isCourseStaff = useSelector(selectIsCourseStaff);
|
||||
const isPrivileged = isUserAdmin || userHasModerationPrivileges || isUserGroupTA || isCourseAdmin || isCourseStaff;
|
||||
const isPrivileged = userHasModerationPrivileges || isUserGroupTA;
|
||||
|
||||
return (isPostingEnabled || isPrivileged);
|
||||
};
|
||||
|
||||
@@ -19,11 +19,11 @@ const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
const generateApiResponse = (isPostingEnabled, isCourseAdmin = false) => ({
|
||||
const generateApiResponse = (isPostingEnabled, hasModerationPrivileges = false) => ({
|
||||
isPostingEnabled,
|
||||
hasModerationPrivileges: false,
|
||||
hasModerationPrivileges,
|
||||
isGroupTa: false,
|
||||
isCourseAdmin,
|
||||
isCourseAdmin: false,
|
||||
isCourseStaff: false,
|
||||
isUserAdmin: false,
|
||||
});
|
||||
@@ -160,7 +160,7 @@ describe('Hooks', () => {
|
||||
expect(queryByText('false')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when posting is not disabled and Role is not Learner return true', async () => {
|
||||
test('when posting is disabled and Role is not Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse(false, true));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -85,7 +84,7 @@ describe('DiscussionSidebar', () => {
|
||||
},
|
||||
})]);
|
||||
renderComponent();
|
||||
await act(async () => expect(await screen.findAllByText('Thread by other users')).toBeTruthy());
|
||||
await screen.findAllByText('Thread by other users');
|
||||
expect(screen.queryByText('Thread by abc123')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -100,7 +99,7 @@ describe('DiscussionSidebar', () => {
|
||||
},
|
||||
})]);
|
||||
renderComponent();
|
||||
await act(async () => expect(await screen.findAllByText('Thread by other users')).toBeTruthy());
|
||||
await screen.findAllByText('Thread by other users');
|
||||
expect(screen.queryByText('Thread by abc123')).not.toBeInTheDocument();
|
||||
expect(container.querySelectorAll('.discussion-post')).toHaveLength(postCount);
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ import { selectPostEditorVisible } from '../posts/data/selectors';
|
||||
import { isCourseStatusValid } from '../utils';
|
||||
import useFeedbackWrapper from './FeedbackWrapper';
|
||||
|
||||
const FooterSlot = lazy(() => import('@openedx/frontend-slot-footer'));
|
||||
const FooterSlot = lazy(() => import('@edx/frontend-component-footer').then(module => ({ default: module.FooterSlot })));
|
||||
const PostActionsBar = lazy(() => import('../posts/post-actions-bar/PostActionsBar'));
|
||||
const CourseTabsNavigation = lazy(() => import('../../components/NavigationBar/CourseTabsNavigation'));
|
||||
const LegacyBreadcrumbMenu = lazy(() => import('../navigation/breadcrumb-menu/LegacyBreadcrumbMenu'));
|
||||
@@ -82,7 +82,7 @@ const DiscussionsHome = () => {
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
<DiscussionContext.Provider value={discussionContextValue}>
|
||||
{!enableInContextSidebar && (<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />)}
|
||||
<main className="container-fluid d-flex flex-column p-0 w-100" id="main" tabIndex="-1">
|
||||
<main className="container-fluid d-flex flex-column p-0 w-100 font-size" id="main" tabIndex="-1">
|
||||
{!enableInContextSidebar && <CourseTabsNavigation />}
|
||||
{(isEnrolled || !isUserLearner) && (
|
||||
<div
|
||||
|
||||
@@ -90,8 +90,7 @@ describe('DiscussionsHome', () => {
|
||||
|
||||
test('full view should hide close button', async () => {
|
||||
renderComponent(`/${courseId}/topics`);
|
||||
expect(screen.queryByText(navigationBarMessages.allTopics.defaultMessage))
|
||||
.toBeInTheDocument();
|
||||
await screen.findByText(navigationBarMessages.allTopics.defaultMessage);
|
||||
expect(screen.queryByRole('button', { name: 'Close' }))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
@@ -144,9 +143,7 @@ describe('DiscussionsHome', () => {
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await renderComponent(`/${courseId}/${searchByEndPoint}`);
|
||||
|
||||
waitFor(() => {
|
||||
expect(screen.queryByText('Add a post')).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByText('Add a post');
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -170,9 +167,7 @@ describe('DiscussionsHome', () => {
|
||||
await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||
await renderComponent(`/${courseId}/${searchByEndPoint}`);
|
||||
|
||||
waitFor(() => {
|
||||
expect(screen.queryByText(result)).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByText(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -199,9 +194,7 @@ describe('DiscussionsHome', () => {
|
||||
await executeThunk(fetchCourseTopicsV3(courseId), store.dispatch, store.getState);
|
||||
await renderComponent(`/${courseId}/${searchByEndPoint}`);
|
||||
|
||||
waitFor(() => {
|
||||
expect(screen.queryByText('No topic selected')).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByText('No topic selected');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -210,9 +203,7 @@ describe('DiscussionsHome', () => {
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await renderComponent(`/${courseId}/learners`);
|
||||
|
||||
waitFor(() => {
|
||||
expect(screen.queryByText('Nothing here yet')).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByText('Nothing here yet');
|
||||
});
|
||||
|
||||
it('should display post editor form when click on add a post button for posts', async () => {
|
||||
@@ -235,10 +226,10 @@ describe('DiscussionsHome', () => {
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await renderComponent(`/${courseId}/topics`);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('Nothing here yet')).toBeInTheDocument());
|
||||
await screen.findByText('Nothing here yet');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByText('Add a post'));
|
||||
fireEvent.click((await screen.findAllByText('Add a post'))[0]);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(container.querySelector('.post-form')).toBeInTheDocument());
|
||||
@@ -247,27 +238,27 @@ describe('DiscussionsHome', () => {
|
||||
it('should display Add a post button for legacy topics view', async () => {
|
||||
await renderComponent(`/${courseId}/topics/topic-1`);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('Add a post')).toBeInTheDocument());
|
||||
await screen.findByText('Add a post');
|
||||
});
|
||||
|
||||
it('should display No post selected for legacy topics view', async () => {
|
||||
await setUpV1TopicsMockResponse();
|
||||
await renderComponent(`/${courseId}/topics/category-1-topic-1`);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('No post selected')).toBeInTheDocument());
|
||||
await screen.findByText('No post selected');
|
||||
});
|
||||
|
||||
it('should display No topic selected for legacy topics view', async () => {
|
||||
await setUpV1TopicsMockResponse();
|
||||
await renderComponent(`/${courseId}/topics`);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('No topic selected')).toBeInTheDocument());
|
||||
await screen.findByText('No topic selected');
|
||||
});
|
||||
|
||||
it('should display navigation tabs', async () => {
|
||||
renderComponent(`/${courseId}/topics`);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('Discussion')).toBeInTheDocument());
|
||||
await screen.findByText('Discussion');
|
||||
});
|
||||
|
||||
it('should display content unavailable message when the user is not enrolled in the course.', async () => {
|
||||
@@ -276,7 +267,7 @@ describe('DiscussionsHome', () => {
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('Content unavailable')).toBeInTheDocument());
|
||||
await screen.findByText('Content unavailable');
|
||||
});
|
||||
|
||||
it('should redirect to dashboard when the user clicks on the Enroll button.', async () => {
|
||||
|
||||
@@ -66,15 +66,13 @@ describe('EmptyTopics', () => {
|
||||
|
||||
test('"no topic selected" text shown when viewing topics page', async () => {
|
||||
renderComponent(`/${courseId}/topics/`);
|
||||
expect(screen.queryByText(messages.emptyTitle.defaultMessage))
|
||||
.toBeInTheDocument();
|
||||
await screen.findByText(messages.emptyTitle.defaultMessage);
|
||||
});
|
||||
|
||||
test('"no post selected" text shown when viewing a specific topic', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent(`/${courseId}/topics/ncwtopic-3/`);
|
||||
|
||||
expect(screen.queryByText(messages.noPostSelected.defaultMessage))
|
||||
.toBeInTheDocument();
|
||||
await screen.findByText(messages.noPostSelected.defaultMessage);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -283,9 +283,9 @@ describe('InContext Topic Posts View', () => {
|
||||
await setupTopicsMockResponse(0, 0, 0);
|
||||
await renderComponent({ topicId: 'test-topic', category: 'test-category' });
|
||||
|
||||
await waitFor(() => expect(within(container).queryByText('Nothing here yet')).toBeInTheDocument());
|
||||
expect(within(container).queryByText('No topic exists')).toBeInTheDocument();
|
||||
expect(within(container).queryByText('Unnamed Topic')).toBeInTheDocument();
|
||||
await within(container).findByText('Nothing here yet');
|
||||
await within(container).findByText('No topic exists');
|
||||
await within(container).findByText('Unnamed Topic');
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
fireEvent, render, screen, waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
@@ -165,9 +166,10 @@ describe('InContext Topics View', () => {
|
||||
it('The subsection should have a title name, be clickable, and have the stats', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
await screen.findByTestId('redux-provider');
|
||||
const subsectionObject = coursewareTopics[0].children[0];
|
||||
const subSection = await container.querySelector(`[data-subsection-id=${subsectionObject.id}]`);
|
||||
const subSectionTitle = await within(subSection).queryByText(subsectionObject.displayName);
|
||||
const subSectionTitle = await within(subSection).findByText(subsectionObject.displayName);
|
||||
const statsList = await subSection.querySelectorAll('.icon-size');
|
||||
|
||||
expect(subSectionTitle).toBeInTheDocument();
|
||||
@@ -177,11 +179,12 @@ describe('InContext Topics View', () => {
|
||||
it('Subsection names should be clickable and redirected to the units lists', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
await screen.findByTestId('redux-provider');
|
||||
|
||||
const subsectionObject = coursewareTopics[0].children[0];
|
||||
const subSection = await container.querySelector(`[data-subsection-id=${subsectionObject.id}]`);
|
||||
|
||||
await act(async () => { fireEvent.click(subSection); });
|
||||
await userEvent.click(subSection);
|
||||
await waitFor(async () => {
|
||||
const backButton = await screen.getByLabelText('Back to topics list');
|
||||
const topicsList = await screen.getByRole('list');
|
||||
@@ -198,9 +201,11 @@ describe('InContext Topics View', () => {
|
||||
it('The number of units should be matched with the actual unit length.', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
await screen.findByTestId('redux-provider');
|
||||
|
||||
const subSection = await container.querySelector(`[data-subsection-id=${coursewareTopics[0].children[0].id}]`);
|
||||
|
||||
await act(async () => { fireEvent.click(subSection); });
|
||||
await userEvent.click(subSection);
|
||||
await waitFor(async () => {
|
||||
const units = await container.querySelectorAll('.discussion-topic');
|
||||
|
||||
@@ -211,12 +216,14 @@ describe('InContext Topics View', () => {
|
||||
it('A unit should have a title and should be clickable', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
await screen.findByTestId('redux-provider');
|
||||
|
||||
const subSectionObject = coursewareTopics[0].children[0];
|
||||
const unitObject = subSectionObject.children[0];
|
||||
|
||||
const subSection = await container.querySelector(`[data-subsection-id=${subSectionObject.id}]`);
|
||||
|
||||
await act(async () => { fireEvent.click(subSection); });
|
||||
await userEvent.click(subSection);
|
||||
await waitFor(async () => {
|
||||
const unitElement = await screen.findByText(unitObject.name);
|
||||
const unitContainer = await container.querySelector(`[data-topic-id=${unitObject.id}]`);
|
||||
|
||||
@@ -37,7 +37,7 @@ const LearnerCard = ({ learner }) => {
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill">
|
||||
<div className="d-flex align-items-center flex-fill">
|
||||
<div
|
||||
className="text-truncate font-weight-500 font-size-14 text-primary-500 font-style"
|
||||
className="text-truncate font-weight-500 text-primary-500 font-style"
|
||||
>
|
||||
{username}
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ import executeThunk from '../../test-utils';
|
||||
import { getCohortsApiUrl } from '../cohorts/data/api';
|
||||
import fetchCourseCohorts from '../cohorts/data/thunks';
|
||||
import DiscussionContext from '../common/context';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { getCourseConfigApiUrl, getCourseSettingsApiUrl } from '../data/api';
|
||||
import fetchCourseConfig from '../data/thunks';
|
||||
import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
@@ -37,6 +37,7 @@ import '../topics/data/__factories__';
|
||||
import '../cohorts/data/__factories__';
|
||||
|
||||
const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
const courseSettingsApiUrl = getCourseSettingsApiUrl();
|
||||
const commentsApiUrl = getCommentsApiUrl();
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const discussionPostId = 'thread-1';
|
||||
@@ -105,7 +106,7 @@ async function setupCourseConfig() {
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
axiosMock.onGet(`${courseSettingsApiUrl}${courseId}/settings`).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
@@ -670,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));
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ const CommentsView = ({ threadType }) => {
|
||||
onClick={handleLoadMoreResponses}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="px-4 mt-3 border-0 line-height-24 py-0 mb-2 font-style font-weight-500 font-size-14"
|
||||
className="px-4 mt-3 border-0 line-height-24 py-0 mb-2 font-style font-weight-500"
|
||||
data-testid="load-more-comments"
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreResponses)}
|
||||
@@ -92,7 +92,7 @@ const CommentsView = ({ threadType }) => {
|
||||
variant="plain"
|
||||
block="true"
|
||||
className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
|
||||
line-height-24 font-size-14 text-primary-500"
|
||||
line-height-24 text-primary-500"
|
||||
onClick={handleAddResponse}
|
||||
data-testid="add-response"
|
||||
>
|
||||
|
||||
@@ -104,6 +104,10 @@ const Comment = ({
|
||||
hideReportConfirmation();
|
||||
}, [abuseFlagged, id, hideReportConfirmation]);
|
||||
|
||||
const handleCommentLike = useCallback(async () => {
|
||||
await dispatch(editComment(id, { voted: !voted }));
|
||||
}, [id, voted]);
|
||||
|
||||
const actionHandlers = useMemo(() => ({
|
||||
[ContentActions.EDIT_CONTENT]: handleEditContent,
|
||||
[ContentActions.ENDORSE]: handleCommentEndorse,
|
||||
@@ -124,10 +128,6 @@ const Comment = ({
|
||||
}
|
||||
}, [isUserPrivilegedInPostingRestriction]);
|
||||
|
||||
const handleCommentLike = useCallback(async () => {
|
||||
await dispatch(editComment(id, { voted: !voted }));
|
||||
}, [id, voted]);
|
||||
|
||||
const handleCloseEditor = useCallback(() => {
|
||||
setEditing(false);
|
||||
}, []);
|
||||
@@ -249,7 +249,7 @@ const Comment = ({
|
||||
onClick={handleLoadMoreComments}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="font-size-14 line-height-24 font-style pt-10px border-0 font-weight-500 pb-0"
|
||||
className="line-height-24 font-style pt-10px border-0 font-weight-500 pb-0"
|
||||
data-testid="load-more-comments-responses"
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreComments)}
|
||||
@@ -267,7 +267,7 @@ const Comment = ({
|
||||
) : (
|
||||
!isClosed && isUserPrivilegedInPostingRestriction && (inlineReplies.length >= 5) && (
|
||||
<Button
|
||||
className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500 add-comment-btn rounded-0"
|
||||
className="d-flex flex-grow mt-2 font-style font-weight-500 text-primary-500 add-comment-btn rounded-0"
|
||||
variant="plain"
|
||||
style={{ height: '36px' }}
|
||||
onClick={handleAddCommentReply}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,9 +11,9 @@ const usePostList = (ids) => {
|
||||
|
||||
const sortedIds = useMemo(() => {
|
||||
posts.forEach((post) => {
|
||||
if (post.pinned) {
|
||||
if (post && post.pinned) {
|
||||
pinnedPostsIds.push(post.id);
|
||||
} else {
|
||||
} else if (post) {
|
||||
unpinnedPostsIds.push(post.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
@@ -241,7 +242,7 @@ const PostEditor = ({
|
||||
resetForm,
|
||||
}) => (
|
||||
<Form className="m-4 card p-4 post-form" onSubmit={handleSubmit}>
|
||||
<h4 className="mb-4 font-style font-size-16" style={{ lineHeight: '16px' }}>
|
||||
<h4 className="mb-4 font-style" style={{ lineHeight: '16px' }}>
|
||||
{editExisting
|
||||
? intl.formatMessage(messages.editPostHeading)
|
||||
: intl.formatMessage(messages.addPostHeading)}
|
||||
@@ -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} />
|
||||
@@ -423,7 +425,7 @@ const PostEditor = ({
|
||||
onBlur={handleBlur}
|
||||
className="mr-4.5"
|
||||
>
|
||||
<span className="font-size-14">
|
||||
<span>
|
||||
{intl.formatMessage(messages.followPost)}
|
||||
</span>
|
||||
</Form.Checkbox>
|
||||
@@ -436,7 +438,7 @@ const PostEditor = ({
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<span className="font-size-14">
|
||||
<span>
|
||||
{intl.formatMessage(messages.anonymousToPeersPost)}
|
||||
</span>
|
||||
</Form.Checkbox>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ActionItem = React.memo(({
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={value === selected ? '0' : '-1'}
|
||||
>
|
||||
<Icon src={Check} className={classNames('text-success mr-2', { invisible: value !== selected })} />
|
||||
<Icon src={Check} className={classNames('text-success dropdown-icon-dimensions', { invisible: value !== selected })} />
|
||||
<Form.Radio id={id} className="sr-only sr-only-focusable" value={value} tabIndex="0">
|
||||
{label}
|
||||
</Form.Radio>
|
||||
|
||||
@@ -85,6 +85,10 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
updateExistingThread(postId, { pinned: !pinned }),
|
||||
), [postId, pinned]);
|
||||
|
||||
const handlePostLike = useCallback(() => {
|
||||
dispatch(updateExistingThread(postId, { voted: !voted }));
|
||||
}, [postId, voted]);
|
||||
|
||||
const handlePostReport = useCallback(() => {
|
||||
if (abuseFlagged) {
|
||||
dispatch(updateExistingThread(postId, { flagged: !abuseFlagged }));
|
||||
@@ -109,10 +113,6 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
hideClosePostModal();
|
||||
}, [postId, hideClosePostModal]);
|
||||
|
||||
const handlePostLike = useCallback(() => {
|
||||
dispatch(updateExistingThread(postId, { voted: !voted }));
|
||||
}, [postId, voted]);
|
||||
|
||||
const handlePostFollow = useCallback(() => {
|
||||
dispatch(updateExistingThread(postId, { following: !following }));
|
||||
}, [postId, following]);
|
||||
@@ -188,10 +188,7 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
</div>
|
||||
{(topicContext || topic) && (
|
||||
<div
|
||||
className={classNames(
|
||||
'mt-14px font-style font-size-12',
|
||||
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter },
|
||||
)}
|
||||
className={classNames('mt-14px font-style', { 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter })}
|
||||
style={{ lineHeight: '20px' }}
|
||||
>
|
||||
<span className="text-gray-500" style={{ lineHeight: '20px' }}>
|
||||
@@ -206,13 +203,9 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
)}
|
||||
>
|
||||
{(topicContext && !topic) ? (
|
||||
<>
|
||||
<span className="w-auto">{topicContext.chapterName}</span>
|
||||
<span className="mx-1">/</span>
|
||||
<span className="w-auto">{topicContext.verticalName}</span>
|
||||
<span className="mx-1">/</span>
|
||||
<span className="w-auto">{topicContext.unitName}</span>
|
||||
</>
|
||||
<span>
|
||||
{topicContext.chapterName} / {topicContext.verticalName} / {topicContext.unitName}
|
||||
</span>
|
||||
) : (
|
||||
getTopicInfo(topic)
|
||||
)}
|
||||
|
||||
@@ -89,13 +89,13 @@ const PostLink = ({
|
||||
<div className="d-flex align-items-center pb-0 mb-0 flex-fill">
|
||||
<div className="text-truncate mr-1">
|
||||
<span className={classNames(
|
||||
'font-weight-500 font-size-14 text-primary-500 font-style align-bottom mr-1',
|
||||
'font-weight-500 text-primary-500 font-style align-bottom mr-1',
|
||||
{ 'font-weight-bolder': !read },
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span className="text-gray-700 font-weight-normal font-size-14 font-style align-bottom">
|
||||
<span className="text-gray-700 font-weight-normal font-style align-bottom">
|
||||
{isPostPreviewAvailable(previewBody) ? previewBody : intl.formatMessage(messages.postWithoutPreview)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ const PostSummaryFooter = ({
|
||||
</OverlayTrigger>
|
||||
|
||||
{preview && commentCount > 1 && (
|
||||
<div className="d-flex align-items-center ml-4.5 text-gray-700 font-style font-size-12">
|
||||
<div className="d-flex align-items-center ml-4.5 text-gray-700 font-style">
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`follow-${postId}-tooltip`}>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, { StrictMode } from 'react';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import {
|
||||
APP_INIT_ERROR, APP_READY, initialize, mergeConfig,
|
||||
@@ -17,18 +19,20 @@ import store from './store';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const rootNode = createRoot(document.getElementById('root'));
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={store}>
|
||||
<Head />
|
||||
<DiscussionsHome />
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
rootNode.render(
|
||||
<StrictMode>
|
||||
<AppProvider store={store}>
|
||||
<Head />
|
||||
<DiscussionsHome />
|
||||
</AppProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
subscribe(APP_INIT_ERROR, (error) => {
|
||||
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
|
||||
rootNode.render(<ErrorPage message={error.message} />);
|
||||
});
|
||||
|
||||
initialize({
|
||||
|
||||
140
src/index.scss
140
src/index.scss
@@ -9,6 +9,25 @@
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
body,
|
||||
#main
|
||||
{
|
||||
font-size: 14px ;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.form-control,
|
||||
.dropdown-menu,
|
||||
.pgn__modal,
|
||||
.pgn__form-control-floating-label-content,
|
||||
.pgn__form-label {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
#post,
|
||||
#comment,
|
||||
#reply,
|
||||
@@ -41,18 +60,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
outline: $light-400 solid 2px;
|
||||
}
|
||||
|
||||
.font-size-16 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.font-size-14 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.font-size-12 {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.font-size-8 {
|
||||
font-size: 8px !important;
|
||||
}
|
||||
@@ -233,11 +240,11 @@ header {
|
||||
}
|
||||
|
||||
.sidebar-tablet-width {
|
||||
max-width: 20rem !important;
|
||||
max-width: 21rem !important;
|
||||
}
|
||||
|
||||
.sidebar-XL-width {
|
||||
min-width: 35rem !important;
|
||||
min-width: 29rem !important;
|
||||
}
|
||||
|
||||
.filter-menu:focus-visible {
|
||||
@@ -259,6 +266,9 @@ header {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
h4 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pointer-cursor-hover :hover {
|
||||
@@ -390,6 +400,7 @@ header {
|
||||
|
||||
.badge {
|
||||
padding: 1px 5px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.pgn__checkpoint {
|
||||
@@ -542,7 +553,7 @@ code {
|
||||
.actions-dropdown-item {
|
||||
padding: 12px 16px;
|
||||
height: 48px !important;
|
||||
width: 304px !important;
|
||||
width: 195px !important
|
||||
}
|
||||
|
||||
.font-xl {
|
||||
@@ -581,3 +592,102 @@ th, td {
|
||||
padding: 0.4rem;
|
||||
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,
|
||||
.btn,
|
||||
.alert-message-content,
|
||||
.pgn__form-control-floating-label-content,
|
||||
.form-control,
|
||||
.pgn__modal,
|
||||
.pgn__form-label,
|
||||
.dropdown-menu,
|
||||
.tox-tbtn,
|
||||
.tooltip {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.badge,
|
||||
.post-summary-timestamp,
|
||||
.hover-card button {
|
||||
font-size: 8px !important;
|
||||
}
|
||||
|
||||
.header-action-bar button {
|
||||
height: 42px;
|
||||
text-wrap: nowrap;
|
||||
;
|
||||
}
|
||||
|
||||
.nav-item a {
|
||||
padding-left: 14px !important;
|
||||
padding-right: 14px !important;
|
||||
}
|
||||
|
||||
.post-form h4,
|
||||
.discussion-posts .btn-md {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 367px) and (max-width: 768px) {
|
||||
|
||||
.discussion-comments h5,
|
||||
.btn,
|
||||
.alert-message-content,
|
||||
.pgn__form-control-floating-label-content,
|
||||
.form-control,
|
||||
.pgn__form-label,
|
||||
.pgn__modal,
|
||||
.dropdown-menu,
|
||||
.tox-tbtn,
|
||||
.tooltip {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.badge,
|
||||
.post-summary-timestamp,
|
||||
.hover-card.btn-tertiary {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.post-form h4,
|
||||
.discussion-posts .btn-md {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) {
|
||||
|
||||
body,
|
||||
#main,
|
||||
.tooltip {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
|
||||
body,
|
||||
#main {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 366px) {
|
||||
|
||||
body,
|
||||
#main {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Footer Slot
|
||||
|
||||
### Slot ID: `footer_slot`
|
||||
### Slot ID: `org.openedx.frontend.layout.footer.v1`
|
||||
|
||||
## Description
|
||||
### Slot ID Aliases
|
||||
* `footer_slot`
|
||||
|
||||
This slot is used to replace/modify/hide the footer.
|
||||
|
||||
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-footer/).
|
||||
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
|
||||
|
||||
## Example
|
||||
|
||||
@@ -23,7 +24,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
footer_slot: {
|
||||
'org.openedx.frontend.layout.footer.v1': {
|
||||
plugins: [
|
||||
{
|
||||
// Hide the default footer
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# `frontend-app-discussions` Plugin Slots
|
||||
|
||||
* [`footer_slot`](./FooterSlot/)
|
||||
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
|
||||
|
||||
@@ -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