Compare commits

..

3 Commits

111 changed files with 27804 additions and 5243 deletions

View File

@@ -1,10 +1,9 @@
const { createConfig } = require('@edx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig( module.exports = createConfig('eslint',
'eslint', {
{ "plugins": ["simple-import-sort"],
plugins: ['simple-import-sort'], "rules": {
rules: {
'import/no-extraneous-dependencies': 'off', 'import/no-extraneous-dependencies': 'off',
'react-hooks/exhaustive-deps': 'off', 'react-hooks/exhaustive-deps': 'off',
'jsx-a11y/no-noninteractive-element-interactions': 'off', 'jsx-a11y/no-noninteractive-element-interactions': 'off',
@@ -26,6 +25,7 @@ module.exports = createConfig(
}, },
], ],
'simple-import-sort/exports': 'error', 'simple-import-sort/exports': 'error',
}, }
}, }
); );

View File

@@ -9,17 +9,18 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: ${{ env.NODE_VER }} node-version: ${{ matrix.node }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Validate package-lock.json changes - name: Validate package-lock.json changes

View File

@@ -10,4 +10,4 @@ on:
jobs: jobs:
version-check: version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master

2
.nvmrc
View File

@@ -1 +1 @@
18 16

31346
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,9 +34,9 @@
}, },
"dependencies": { "dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0", "@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "12.0.0", "@edx/frontend-component-footer": "11.2.0",
"@edx/frontend-component-header": "4.0.3", "@edx/frontend-component-header": "3.2.0",
"@edx/frontend-platform": "4.4.0", "@edx/frontend-platform": "2.6.1",
"@edx/paragon": "20.15.0", "@edx/paragon": "20.15.0",
"@reduxjs/toolkit": "1.8.0", "@reduxjs/toolkit": "1.8.0",
"@tinymce/tinymce-react": "3.13.1", "@tinymce/tinymce-react": "3.13.1",
@@ -61,7 +61,7 @@
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "1.1.0", "@edx/browserslist-config": "1.1.0",
"@edx/frontend-build": "12.8.38", "@edx/frontend-build": "11.0.1",
"@edx/reactifex": "1.0.3", "@edx/reactifex": "1.0.3",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.4", "@testing-library/react": "12.1.4",

View File

@@ -19,21 +19,19 @@ import { selectCourseCohorts } from '../discussions/cohorts/data/selectors';
import messages from '../discussions/posts/post-filter-bar/messages'; import messages from '../discussions/posts/post-filter-bar/messages';
import { ActionItem } from '../discussions/posts/post-filter-bar/PostFilterBar'; import { ActionItem } from '../discussions/posts/post-filter-bar/PostFilterBar';
const FilterBar = ({ function FilterBar({
intl, intl,
filters, filters,
selectedFilters, selectedFilters,
onFilterChange, onFilterChange,
showCohortsFilter, showCohortsFilter,
}) => { }) {
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const cohorts = useSelector(selectCourseCohorts); const cohorts = useSelector(selectCourseCohorts);
const { status } = useSelector(state => state.cohorts); const { status } = useSelector(state => state.cohorts);
const selectedCohort = useMemo( const selectedCohort = useMemo(() => cohorts.find(cohort => (
() => cohorts.find(cohort => ( toString(cohort.id) === selectedFilters.cohort)),
toString(cohort.id) === selectedFilters.cohort)), [selectedFilters.cohort]);
[selectedFilters.cohort],
);
const allFilters = [ const allFilters = [
{ {
@@ -185,7 +183,7 @@ const FilterBar = ({
</Collapsible.Body> </Collapsible.Body>
</Collapsible.Advanced> </Collapsible.Advanced>
); );
}; }
FilterBar.propTypes = { FilterBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -5,7 +5,7 @@ import { getIn, useFormikContext } from 'formik';
import { Form, TransitionReplace } from '@edx/paragon'; import { Form, TransitionReplace } from '@edx/paragon';
const FormikErrorFeedback = ({ name }) => { function FormikErrorFeedback({ name }) {
const { const {
touched, touched,
errors, errors,
@@ -26,7 +26,7 @@ const FormikErrorFeedback = ({ name }) => {
)} )}
</TransitionReplace> </TransitionReplace>
); );
}; }
FormikErrorFeedback.propTypes = { FormikErrorFeedback.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,

View File

@@ -12,9 +12,9 @@ const defaultSanitizeOptions = {
ADD_ATTR: ['columnalign'], ADD_ATTR: ['columnalign'],
}; };
const HTMLLoader = ({ function HTMLLoader({
htmlNode, componentId, cssClassName, testId, delay, htmlNode, componentId, cssClassName, testId, delay,
}) => { }) {
const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions }); const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions });
const previewRef = useRef(null); const previewRef = useRef(null);
const debouncedPostContent = useDebounce(htmlNode, delay); const debouncedPostContent = useDebounce(htmlNode, delay);
@@ -45,7 +45,7 @@ const HTMLLoader = ({
return ( return (
<div ref={previewRef} className={cssClassName} id={componentId} data-testid={testId} /> <div ref={previewRef} className={cssClassName} id={componentId} data-testid={testId} />
); );
}; }
HTMLLoader.propTypes = { HTMLLoader.propTypes = {
htmlNode: PropTypes.node, htmlNode: PropTypes.node,

View File

@@ -12,9 +12,9 @@ import messages from './messages';
import './navBar.scss'; import './navBar.scss';
const CourseTabsNavigation = ({ function CourseTabsNavigation({
activeTab, className, intl, courseId, rootSlug, activeTab, className, intl, courseId, rootSlug,
}) => { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const tabs = useSelector(state => state.courseTabs.tabs); const tabs = useSelector(state => state.courseTabs.tabs);
@@ -45,7 +45,7 @@ const CourseTabsNavigation = ({
</div> </div>
</div> </div>
); );
}; }
CourseTabsNavigation.propTypes = { CourseTabsNavigation.propTypes = {
activeTab: PropTypes.string, activeTab: PropTypes.string,

View File

@@ -56,10 +56,8 @@ describe('Navigation bar api tests', () => {
}); });
it('Denied to get navigation bar when user has no access on course', async () => { it('Denied to get navigation bar when user has no access on course', async () => {
axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply( axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200,
200, (Factory.build('navigationBar', 1, { hasCourseAccess: false })));
(Factory.build('navigationBar', 1, { hasCourseAccess: false })),
);
await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState); await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState);
expect(store.getState().courseTabs.courseStatus).toEqual('denied'); expect(store.getState().courseTabs.courseStatus).toEqual('denied');

View File

@@ -8,7 +8,7 @@ import { Dropdown } from '@edx/paragon';
import useIndexOfLastVisibleChild from './useIndexOfLastVisibleChild'; import useIndexOfLastVisibleChild from './useIndexOfLastVisibleChild';
const Tabs = ({ children, className, ...attrs }) => { export default function Tabs({ children, className, ...attrs }) {
const [ const [
indexOfLastVisibleChild, indexOfLastVisibleChild,
containerElementRef, containerElementRef,
@@ -31,28 +31,25 @@ const Tabs = ({ children, className, ...attrs }) => {
// Insert the overflow menu at the cut off index (even if it will be hidden // Insert the overflow menu at the cut off index (even if it will be hidden
// it so it can be part of measurements) // it so it can be part of measurements)
wrappedChildren.splice( wrappedChildren.splice(indexOfOverflowStart, 0, (
indexOfOverflowStart, <div
0, ( className="nav-item flex-shrink-0"
<div style={indexOfOverflowStart >= React.Children.count(children) ? invisibleStyle : null}
className="nav-item flex-shrink-0" ref={overflowElementRef}
style={indexOfOverflowStart >= React.Children.count(children) ? invisibleStyle : null} key="overflow"
ref={overflowElementRef} >
key="overflow" <Dropdown className="h-100">
> <Dropdown.Toggle variant="link" className="nav-link h-100" id="learn.course.tabs.navigation.overflow.menu">
<Dropdown className="h-100"> <FormattedMessage
<Dropdown.Toggle variant="link" className="nav-link h-100" id="learn.course.tabs.navigation.overflow.menu"> id="learn.course.tabs.navigation.overflow.menu"
<FormattedMessage description="The title of the overflow menu for course tabs"
id="learn.course.tabs.navigation.overflow.menu" defaultMessage="More..."
description="The title of the overflow menu for course tabs" />
defaultMessage="More..." </Dropdown.Toggle>
/> <Dropdown.Menu className="dropdown-menu-right">{overflowChildren}</Dropdown.Menu>
</Dropdown.Toggle> </Dropdown>
<Dropdown.Menu className="dropdown-menu-right">{overflowChildren}</Dropdown.Menu> </div>
</Dropdown> ));
</div>
),
);
return wrappedChildren; return wrappedChildren;
}, [children, indexOfLastVisibleChild]); }, [children, indexOfLastVisibleChild]);
@@ -65,7 +62,7 @@ const Tabs = ({ children, className, ...attrs }) => {
{tabChildren} {tabChildren}
</nav> </nav>
); );
}; }
Tabs.propTypes = { Tabs.propTypes = {
children: PropTypes.node, children: PropTypes.node,
@@ -76,5 +73,3 @@ Tabs.defaultProps = {
children: null, children: null,
className: undefined, className: undefined,
}; };
export default Tabs;

View File

@@ -8,9 +8,9 @@ import { Close } from '@edx/paragon/icons';
import messages from '../discussions/posts/post-editor/messages'; import messages from '../discussions/posts/post-editor/messages';
import HTMLLoader from './HTMLLoader'; import HTMLLoader from './HTMLLoader';
const PostPreviewPanel = ({ function PostPreviewPanel({
htmlNode, intl, isPost, editExisting, htmlNode, intl, isPost, editExisting,
}) => { }) {
const [showPreviewPane, setShowPreviewPane] = useState(false); const [showPreviewPane, setShowPreviewPane] = useState(false);
return ( return (
@@ -55,7 +55,7 @@ const PostPreviewPanel = ({
</div> </div>
</> </>
); );
}; }
PostPreviewPanel.propTypes = { PostPreviewPanel.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect } from 'react';
import camelCase from 'lodash/camelCase'; import camelCase from 'lodash/camelCase';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@@ -13,8 +13,7 @@ import { setSearchQuery } from '../discussions/posts/data';
import postsMessages from '../discussions/posts/post-actions-bar/messages'; import postsMessages from '../discussions/posts/post-actions-bar/messages';
import { setFilter as setTopicFilter } from '../discussions/topics/data/slices'; import { setFilter as setTopicFilter } from '../discussions/topics/data/slices';
const Search = ({ intl }) => { function Search({ intl }) {
const [previousSearchValue, setPreviousSearchValue] = useState('');
const dispatch = useDispatch(); const dispatch = useDispatch();
const { page } = useContext(DiscussionContext); const { page } = useContext(DiscussionContext);
const postSearch = useSelector(({ threads }) => threads.filters.search); const postSearch = useSelector(({ threads }) => threads.filters.search);
@@ -36,7 +35,6 @@ const Search = ({ intl }) => {
dispatch(setSearchQuery('')); dispatch(setSearchQuery(''));
dispatch(setTopicFilter('')); dispatch(setTopicFilter(''));
dispatch(setUsernameSearch('')); dispatch(setUsernameSearch(''));
setPreviousSearchValue('');
}; };
const onChange = (query) => { const onChange = (query) => {
@@ -44,7 +42,7 @@ const Search = ({ intl }) => {
}; };
const onSubmit = (query) => { const onSubmit = (query) => {
if (query === '' || query === previousSearchValue) { if (query === '') {
return; return;
} }
if (isPostSearch) { if (isPostSearch) {
@@ -54,32 +52,33 @@ const Search = ({ intl }) => {
} else if (page === 'learners') { } else if (page === 'learners') {
dispatch(setUsernameSearch(query)); dispatch(setUsernameSearch(query));
} }
setPreviousSearchValue(query);
}; };
useEffect(() => onClear(), [page]); useEffect(() => onClear(), [page]);
return ( return (
<SearchField.Advanced <>
onClear={onClear} <SearchField.Advanced
onChange={onChange} onClear={onClear}
onSubmit={onSubmit} onChange={onChange}
value={currentValue} onSubmit={onSubmit}
> value={currentValue}
<SearchField.Label /> >
<SearchField.Input <SearchField.Label />
style={{ paddingRight: '1rem' }} <SearchField.Input
placeholder={intl.formatMessage(postsMessages.search, { page: camelCase(page) })} style={{ paddingRight: '1rem' }}
/> placeholder={intl.formatMessage(postsMessages.search, { page: camelCase(page) })}
<span className="mt-auto mb-auto mr-2.5 pointer-cursor-hover">
<Icon
src={SearchIcon}
onClick={() => onSubmit(searchValue)}
data-testid="search-icon"
/> />
</span> <span className="mt-auto mb-auto mr-2.5 pointer-cursor-hover">
</SearchField.Advanced> <Icon
src={SearchIcon}
onClick={() => onSubmit(searchValue)}
data-testid="search-icon"
/>
</span>
</SearchField.Advanced>
</>
); );
}; }
Search.propTypes = { Search.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -8,32 +8,34 @@ import { Search } from '@edx/paragon/icons';
import { RequestStatus } from '../data/constants'; import { RequestStatus } from '../data/constants';
import messages from '../discussions/posts/post-actions-bar/messages'; import messages from '../discussions/posts/post-actions-bar/messages';
const SearchInfo = ({ function SearchInfo({
intl, intl,
count, count,
text, text,
loadingStatus, loadingStatus,
onClear, onClear,
textSearchRewrite, textSearchRewrite,
}) => ( }) {
<div className="d-flex flex-row border-bottom border-light-400"> return (
<Icon src={Search} className="justify-content-start ml-3.5 mr-2 mb-2 mt-2.5" /> <div className="d-flex flex-row border-bottom border-light-400">
<Button variant="" size="inline" className="text-justify p-2"> <Icon src={Search} className="justify-content-start ml-3.5 mr-2 mb-2 mt-2.5" />
{loadingStatus === RequestStatus.SUCCESSFUL && ( <Button variant="" size="inline" className="text-justify p-2">
textSearchRewrite ? intl.formatMessage(messages.searchRewriteInfo, { {loadingStatus === RequestStatus.SUCCESSFUL && (
searchString: text, textSearchRewrite ? intl.formatMessage(messages.searchRewriteInfo, {
count, searchString: text,
textSearchRewrite, count,
}) textSearchRewrite,
: intl.formatMessage(messages.searchInfo, { count, text }) })
)} : intl.formatMessage(messages.searchInfo, { count, text })
{loadingStatus !== RequestStatus.SUCCESSFUL && intl.formatMessage(messages.searchInfoSearching)} )}
</Button> {loadingStatus !== RequestStatus.SUCCESSFUL && intl.formatMessage(messages.searchInfoSearching)}
<Button variant="link" size="inline" className="ml-auto mr-3" onClick={onClear} style={{ minWidth: '26%' }}> </Button>
{intl.formatMessage(messages.clearSearch)} <Button variant="link" size="inline" className="ml-auto mr-3" onClick={onClear} style={{ minWidth: '26%' }}>
</Button> {intl.formatMessage(messages.clearSearch)}
</div> </Button>
); </div>
);
}
SearchInfo.propTypes = { SearchInfo.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -58,7 +58,7 @@ const setup = (editor) => {
}; };
/* istanbul ignore next */ /* istanbul ignore next */
const TinyMCEEditor = (props) => { export default function TinyMCEEditor(props) {
// note that skin and content_css is disabled to avoid the normal // note that skin and content_css is disabled to avoid the normal
// loading process and is instead loaded as a string via content_style // loading process and is instead loaded as a string via content_style
@@ -148,6 +148,4 @@ const TinyMCEEditor = (props) => {
</> </>
); );
}; }
export default TinyMCEEditor;

View File

@@ -14,12 +14,12 @@ import {
} from '../discussions/data/selectors'; } from '../discussions/data/selectors';
import messages from '../discussions/in-context-topics/messages'; import messages from '../discussions/in-context-topics/messages';
const TopicStats = ({ function TopicStats({
threadCounts, threadCounts,
activeFlags, activeFlags,
inactiveFlags, inactiveFlags,
intl, intl,
}) => { }) {
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa); const userIsGroupTa = useSelector(selectUserIsGroupTa);
const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa); const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa);
@@ -87,7 +87,7 @@ const TopicStats = ({
)} )}
</div> </div>
); );
}; }
TopicStats.propTypes = { TopicStats.propTypes = {
threadCounts: PropTypes.shape({ threadCounts: PropTypes.shape({

View File

@@ -1,20 +1,20 @@
import React from 'react'; import React from 'react';
const InsertLink = () => ( export default function InsertLink() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" width="24"
fill="none" height="24"
viewBox="0 0 24 24" fill="none"
> viewBox="0 0 24 24"
<path >
fill="currentColor" <path
fillRule="evenodd" fill="currentColor"
d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" fillRule="evenodd"
clipRule="evenodd" d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"
/> clipRule="evenodd"
</svg> />
); </svg>
);
export default InsertLink; }

View File

@@ -1,26 +1,26 @@
import React from 'react'; import React from 'react';
const Issue = () => ( export default function Issue() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="28" xmlns="http://www.w3.org/2000/svg"
height="28" width="28"
fill="none" height="28"
viewBox="0 0 28 28" fill="none"
> viewBox="0 0 28 28"
<path >
fill="#F2F0EF" <path
d="M0 14C0 6.268 6.268 0 14 0s14 6.268 14 14-6.268 14-14 14S0 21.732 0 14z" fill="#F2F0EF"
/> d="M0 14C0 6.268 6.268 0 14 0s14 6.268 14 14-6.268 14-14 14S0 21.732 0 14z"
<path />
fill="#2D494E" <path
d="M14 2.333C7.56 2.333 2.333 7.56 2.333 14c0 6.44 5.227 11.667 11.667 11.667 6.44 0 11.667-5.227 11.667-11.667C25.667 7.56 20.44 2.334 14 2.334z" fill="#2D494E"
/> d="M14 2.333C7.56 2.333 2.333 7.56 2.333 14c0 6.44 5.227 11.667 11.667 11.667 6.44 0 11.667-5.227 11.667-11.667C25.667 7.56 20.44 2.334 14 2.334z"
<path />
fill="#fff" <path
d="M12.833 22.167h2.334v-2.334h-2.334v2.334zM16.532 14.198l1.05-1.073a3.713 3.713 0 001.085-2.625A4.665 4.665 0 0014 5.833 4.665 4.665 0 009.333 10.5h2.334A2.34 2.34 0 0114 8.167a2.34 2.34 0 012.333 2.333c0 .642-.256 1.225-.688 1.645l-1.447 1.47a4.696 4.696 0 00-1.365 3.302v.583h2.334c0-1.75.525-2.45 1.365-3.302z" fill="#fff"
/> d="M12.833 22.167h2.334v-2.334h-2.334v2.334zM16.532 14.198l1.05-1.073a3.713 3.713 0 001.085-2.625A4.665 4.665 0 0014 5.833 4.665 4.665 0 009.333 10.5h2.334A2.34 2.34 0 0114 8.167a2.34 2.34 0 012.333 2.333c0 .642-.256 1.225-.688 1.645l-1.447 1.47a4.696 4.696 0 00-1.365 3.302v.583h2.334c0-1.75.525-2.45 1.365-3.302z"
</svg> />
); </svg>
);
export default Issue; }

View File

@@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
const People = () => ( export default function People() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="16" xmlns="http://www.w3.org/2000/svg"
height="16" width="16"
fill="none" height="16"
viewBox="0 0 16 16" fill="none"
> viewBox="0 0 16 16"
<path >
fill="#707070" <path
d="M11.072 7.332a1.992 1.992 0 001.993-2 1.997 1.997 0 10-3.993 0c0 1.107.893 2 2 2zm-5.334 0a1.992 1.992 0 001.994-2 1.997 1.997 0 10-3.993 0c0 1.107.893 2 2 2zm0 1.333c-1.553 0-4.666.78-4.666 2.334v1.666h9.333V11c0-1.554-3.113-2.334-4.667-2.334zm5.334 0c-.194 0-.414.014-.647.034.773.56 1.313 1.313 1.313 2.3v1.666h4V11c0-1.554-3.113-2.334-4.666-2.334z" fill="#707070"
/> d="M11.072 7.332a1.992 1.992 0 001.993-2 1.997 1.997 0 10-3.993 0c0 1.107.893 2 2 2zm-5.334 0a1.992 1.992 0 001.994-2 1.997 1.997 0 10-3.993 0c0 1.107.893 2 2 2zm0 1.333c-1.553 0-4.666.78-4.666 2.334v1.666h9.333V11c0-1.554-3.113-2.334-4.667-2.334zm5.334 0c-.194 0-.414.014-.647.034.773.56 1.313 1.313 1.313 2.3v1.666h4V11c0-1.554-3.113-2.334-4.666-2.334z"
</svg> />
); </svg>
);
export default People; }

View File

@@ -1,20 +1,20 @@
import React from 'react'; import React from 'react';
const PushPin = () => ( export default function PushPin() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" width="24"
fill="none" height="24"
viewBox="0 0 24 24" fill="none"
> viewBox="0 0 24 24"
<path >
fill="currentColor" <path
fillRule="evenodd" fill="currentColor"
d="M16 9V4H18V2H6V4H8V9C8 10.66 6.66 12 5 12V14H10.97V21L11.97 22L12.97 21V14H19V12C17.34 12 16 10.66 16 9Z" fillRule="evenodd"
clipRule="evenodd" d="M16 9V4H18V2H6V4H8V9C8 10.66 6.66 12 5 12V14H10.97V21L11.97 22L12.97 21V14H19V12C17.34 12 16 10.66 16 9Z"
/> clipRule="evenodd"
</svg> />
); </svg>
);
export default PushPin; }

View File

@@ -1,26 +1,26 @@
import React from 'react'; import React from 'react';
const Question = () => ( export default function Question() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="28" xmlns="http://www.w3.org/2000/svg"
height="28" width="28"
fill="none" height="28"
viewBox="0 0 28 28" fill="none"
> viewBox="0 0 28 28"
<path >
fill="#fff" <path
d="M0 14.001c0-7.732 6.268-14 14-14s14 6.268 14 14-6.268 14-14 14-14-6.268-14-14z" fill="#fff"
/> d="M0 14.001c0-7.732 6.268-14 14-14s14 6.268 14 14-6.268 14-14 14-14-6.268-14-14z"
<path />
fill="#2D494E" <path
d="M14 2.334c-6.44 0-11.667 5.227-11.667 11.667 0 6.44 5.227 11.667 11.667 11.667 6.44 0 11.666-5.227 11.666-11.667 0-6.44-5.226-11.667-11.666-11.667z" fill="#2D494E"
/> d="M14 2.334c-6.44 0-11.667 5.227-11.667 11.667 0 6.44 5.227 11.667 11.667 11.667 6.44 0 11.666-5.227 11.666-11.667 0-6.44-5.226-11.667-11.666-11.667z"
<path />
fill="#fff" <path
d="M12.833 22.168h2.333v-2.334h-2.333v2.334zM16.531 14.2l1.05-1.074a3.712 3.712 0 001.085-2.625A4.665 4.665 0 0014 5.834a4.665 4.665 0 00-4.667 4.667h2.333A2.34 2.34 0 0114 8.168a2.34 2.34 0 012.333 2.333c0 .642-.257 1.225-.688 1.645l-1.447 1.47a4.696 4.696 0 00-1.365 3.302v.583h2.333c0-1.75.525-2.45 1.365-3.302z" fill="#fff"
/> d="M12.833 22.168h2.333v-2.334h-2.333v2.334zM16.531 14.2l1.05-1.074a3.712 3.712 0 001.085-2.625A4.665 4.665 0 0014 5.834a4.665 4.665 0 00-4.667 4.667h2.333A2.34 2.34 0 0114 8.168a2.34 2.34 0 012.333 2.333c0 .642-.257 1.225-.688 1.645l-1.447 1.47a4.696 4.696 0 00-1.365 3.302v.583h2.333c0-1.75.525-2.45 1.365-3.302z"
</svg> />
); </svg>
);
export default Question; }

View File

@@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
const QuestionAnswer = () => ( export default function QuestionAnswer() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="21" xmlns="http://www.w3.org/2000/svg"
height="20" width="21"
fill="none" height="20"
viewBox="0 0 21 20" fill="none"
> viewBox="0 0 21 20"
<path >
fill="currentColor" <path
d="M18.737 5h-2.5v7.5H5.404V15h10l3.333 3.333V5zm-4.166 5.833V1.667H2.07v12.5l3.333-3.334h9.166z" fill="currentColor"
/> d="M18.737 5h-2.5v7.5H5.404V15h10l3.333 3.333V5zm-4.166 5.833V1.667H2.07v12.5l3.333-3.334h9.166z"
</svg> />
); </svg>
);
export default QuestionAnswer; }

View File

@@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
const QuestionAnswerOutline = () => ( export default function QuestionAnswerOutline() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="20" xmlns="http://www.w3.org/2000/svg"
height="20" width="20"
fill="none" height="20"
viewBox="0 0 20 20" fill="none"
> viewBox="0 0 20 20"
<path >
fill="currentColor" <path
d="M12.6 3.267v6.067H4.08l-.512.512-.502.502v-7.08H12.6zm.867-1.733H2.198a.87.87 0 00-.867.867v12.134L4.8 11.068h8.668a.87.87 0 00.866-.867v-7.8a.87.87 0 00-.867-.867zM17.8 5h-1.733v7.8H4.799v1.734c0 .476.39.867.867.867H15.2l3.467 3.466v-13A.87.87 0 0017.8 5z" fill="currentColor"
/> d="M12.6 3.267v6.067H4.08l-.512.512-.502.502v-7.08H12.6zm.867-1.733H2.198a.87.87 0 00-.867.867v12.134L4.8 11.068h8.668a.87.87 0 00.866-.867v-7.8a.87.87 0 00-.867-.867zM17.8 5h-1.733v7.8H4.799v1.734c0 .476.39.867.867.867H15.2l3.467 3.466v-13A.87.87 0 0017.8 5z"
</svg> />
); </svg>
);
export default QuestionAnswerOutline; }

View File

@@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
const StarFilled = () => ( export default function StarFilled() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="21" xmlns="http://www.w3.org/2000/svg"
height="20" width="21"
fill="none" height="20"
viewBox="0 0 21 20" fill="none"
> viewBox="0 0 21 20"
<path >
fill="currentColor" <path
d="M10.404 14.392l5.15 3.108-1.367-5.858 4.55-3.942-5.991-.508-2.342-5.525-2.342 5.525L2.07 7.7l4.55 3.942L5.254 17.5l5.15-3.108z" fill="currentColor"
/> d="M10.404 14.392l5.15 3.108-1.367-5.858 4.55-3.942-5.991-.508-2.342-5.525-2.342 5.525L2.07 7.7l4.55 3.942L5.254 17.5l5.15-3.108z"
</svg> />
); </svg>
);
export default StarFilled; }

View File

@@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
const StarOutline = () => ( export default function StarOutline() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="20" xmlns="http://www.w3.org/2000/svg"
height="20" width="20"
fill="none" height="20"
viewBox="0 0 20 20" fill="none"
> viewBox="0 0 20 20"
<path >
fill="currentColor" <path
d="M18.737 7.7l-5.991-.517-2.342-5.516-2.342 5.525L2.07 7.7l4.55 3.942L5.254 17.5l5.15-3.108 5.15 3.108-1.359-5.858L18.737 7.7zm-8.333 5.133L7.27 14.725l.834-3.567-2.767-2.4 3.65-.316 1.417-3.359 1.425 3.367 3.65.317-2.767 2.4.834 3.566-3.142-1.9z" fill="currentColor"
/> d="M18.737 7.7l-5.991-.517-2.342-5.516-2.342 5.525L2.07 7.7l4.55 3.942L5.254 17.5l5.15-3.108 5.15 3.108-1.359-5.858L18.737 7.7zm-8.333 5.133L7.27 14.725l.834-3.567-2.767-2.4 3.65-.316 1.417-3.359 1.425 3.367 3.65.317-2.767 2.4.834 3.566-3.142-1.9z"
</svg> />
); </svg>
);
export default StarOutline; }

View File

@@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
const ThumbUpFilled = () => ( export default function ThumbUpFilled() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="21" xmlns="http://www.w3.org/2000/svg"
height="20" width="21"
fill="none" height="20"
viewBox="0 0 21 20" fill="none"
> viewBox="0 0 21 20"
<path >
fill="currentColor" <path
d="M12.212.833L6.237 6.817V17.5h10.258l3.075-7.167V6.667h-6.925l.934-4.484-1.367-1.35zM1.237 7.5H4.57v10H1.237v-10z" fill="currentColor"
/> d="M12.212.833L6.237 6.817V17.5h10.258l3.075-7.167V6.667h-6.925l.934-4.484-1.367-1.35zM1.237 7.5H4.57v10H1.237v-10z"
</svg> />
); </svg>
);
export default ThumbUpFilled; }

View File

@@ -1,21 +1,21 @@
import React from 'react'; import React from 'react';
const ThumbUpOutline = () => ( export default function ThumbUpOutline() {
<svg return (
xmlns="http://www.w3.org/2000/svg" <svg
width="20" xmlns="http://www.w3.org/2000/svg"
height="20" width="20"
fill="none" height="20"
viewBox="0 0 20 20" fill="none"
> viewBox="0 0 20 20"
<path >
fill="currentColor" <path
fillRule="evenodd" fill="currentColor"
d="M19.57 6.667v3.666L16.495 17.5H6.238V6.817L12.212.833l1.367 1.35-.934 4.484h6.925zm-11.666.841v8.325h7.492l2.508-5.841V8.333h-7.309l.925-4.45-3.616 3.625z" fillRule="evenodd"
clipRule="evenodd" d="M19.57 6.667v3.666L16.495 17.5H6.238V6.817L12.212.833l1.367 1.35-.934 4.484h6.925zm-11.666.841v8.325h7.492l2.508-5.841V8.333h-7.309l.925-4.45-3.616 3.625z"
/> clipRule="evenodd"
<path fill="currentColor" d="M4.57 17.5H1.237v-10H4.57v10z" /> />
</svg> <path fill="currentColor" d="M4.57 17.5H1.237v-10H4.57v10z" />
); </svg>
);
export default ThumbUpOutline; }

View File

@@ -55,6 +55,8 @@ export const ContentActions = {
CHANGE_TOPIC: 'topic_id', CHANGE_TOPIC: 'topic_id',
CHANGE_TYPE: 'type', CHANGE_TYPE: 'type',
VOTE: 'voted', VOTE: 'voted',
ACCEPT_REVIEW: 'accept_review',
REJECT_REVIEW: 'reject_review',
}; };
/** /**

View File

@@ -17,14 +17,14 @@ import { commentShape } from '../post-comments/comments/comment/proptypes';
import { postShape } from '../posts/post/proptypes'; import { postShape } from '../posts/post/proptypes';
import { inBlackoutDateRange, useActions } from '../utils'; import { inBlackoutDateRange, useActions } from '../utils';
const ActionsDropdown = ({ function ActionsDropdown({
intl, intl,
commentOrPost, commentOrPost,
disabled, disabled,
actionHandlers, actionHandlers,
iconSize, iconSize,
dropDownIconSize, dropDownIconSize,
}) => { }) {
const buttonRef = useRef(); const buttonRef = useRef();
const [isOpen, open, close] = useToggle(false); const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null); const [target, setTarget] = useState(null);
@@ -108,7 +108,7 @@ const ActionsDropdown = ({
</div> </div>
</> </>
); );
}; }
ActionsDropdown.propTypes = { ActionsDropdown.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -16,10 +16,10 @@ import messages from '../post-comments/messages';
import { postShape } from '../posts/post/proptypes'; import { postShape } from '../posts/post/proptypes';
import AuthorLabel from './AuthorLabel'; import AuthorLabel from './AuthorLabel';
const AlertBanner = ({ function AlertBanner({
intl, intl,
content, content,
}) => { }) {
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa); const userIsGroupTa = useSelector(selectUserIsGroupTa);
const userIsGlobalStaff = useSelector(selectUserIsStaff); const userIsGlobalStaff = useSelector(selectUserIsStaff);
@@ -79,7 +79,7 @@ const AlertBanner = ({
)} )}
</> </>
); );
}; }
AlertBanner.propTypes = { AlertBanner.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -16,7 +16,7 @@ import { discussionsPath } from '../utils';
import { DiscussionContext } from './context'; import { DiscussionContext } from './context';
import timeLocale from './time-locale'; import timeLocale from './time-locale';
const AuthorLabel = ({ function AuthorLabel({
intl, intl,
author, author,
authorLabel, authorLabel,
@@ -26,7 +26,7 @@ const AuthorLabel = ({
postCreatedAt, postCreatedAt,
authorToolTip, authorToolTip,
postOrComment, postOrComment,
}) => { }) {
const location = useLocation(); const location = useLocation();
const { courseId } = useContext(DiscussionContext); const { courseId } = useContext(DiscussionContext);
let icon = null; let icon = null;
@@ -127,7 +127,7 @@ const AuthorLabel = ({
</div> </div>
) )
: <div className={className}>{authorName}{labelContents}</div>; : <div className={className}>{authorName}{labelContents}</div>;
}; }
AuthorLabel.propTypes = { AuthorLabel.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -66,20 +66,19 @@ describe('Author label', () => {
['retired__user', null, false, ''], ['retired__user', null, false, ''],
['staff_user', 'Staff', true, 'text-staff-color'], ['staff_user', 'Staff', true, 'text-staff-color'],
['learner_user', null, false, ''], ['learner_user', null, false, ''],
])('for %s', (author, authorLabel, linkToProfile, labelColor) => { ])('for %s', (
it( author, authorLabel, linkToProfile, labelColor,
'it has author name text', ) => {
it('it has author name text',
async () => { async () => {
renderComponent(author, authorLabel, linkToProfile, labelColor); renderComponent(author, authorLabel, linkToProfile, labelColor);
const authorElement = container.querySelector('[role=heading]'); const authorElement = container.querySelector('[role=heading]');
const authorName = author.startsWith('retired__user') ? '[Deactivated]' : author; const authorName = author.startsWith('retired__user') ? '[Deactivated]' : author;
expect(authorElement).toHaveTextContent(authorName); expect(authorElement).toHaveTextContent(authorName);
}, });
);
it( it(`it is "${!linkToProfile && 'not'}" clickable when linkToProfile is ${!!linkToProfile}`,
`it is "${!linkToProfile && 'not'}" clickable when linkToProfile is ${!!linkToProfile}`,
async () => { async () => {
renderComponent(author, authorLabel, linkToProfile, labelColor); renderComponent(author, authorLabel, linkToProfile, labelColor);
@@ -88,11 +87,9 @@ describe('Author label', () => {
} else { } else {
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument(); expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument();
} }
}, });
);
it( it(`it has "${!linkToProfile && 'not'}" label text and label color when linkToProfile is ${!!linkToProfile}`,
`it has "${!linkToProfile && 'not'}" label text and label color when linkToProfile is ${!!linkToProfile}`,
async () => { async () => {
renderComponent(author, authorLabel, linkToProfile, labelColor); renderComponent(author, authorLabel, linkToProfile, labelColor);
const authorElement = container.querySelector('[role=heading]'); const authorElement = container.querySelector('[role=heading]');
@@ -107,7 +104,6 @@ describe('Author label', () => {
expect(authorElement.parentNode.lastChild).not.toHaveTextContent(label, { exact: true }); expect(authorElement.parentNode.lastChild).not.toHaveTextContent(label, { exact: true });
expect(authorElement.parentNode).not.toHaveClass(labelColor, { exact: true }); expect(authorElement.parentNode).not.toHaveClass(labelColor, { exact: true });
} }
}, });
);
}); });
}); });

View File

@@ -6,7 +6,7 @@ import { ActionRow, Button, ModalDialog } from '@edx/paragon';
import messages from '../messages'; import messages from '../messages';
const Confirmation = ({ function Confirmation({
intl, intl,
isOpen, isOpen,
title, title,
@@ -16,28 +16,30 @@ const Confirmation = ({
closeButtonVaraint, closeButtonVaraint,
confirmButtonVariant, confirmButtonVariant,
confirmButtonText, confirmButtonText,
}) => ( }) {
<ModalDialog title={title} isOpen={isOpen} hasCloseButton={false} onClose={onClose} zIndex={5000}> return (
<ModalDialog.Header> <ModalDialog title={title} isOpen={isOpen} hasCloseButton={false} onClose={onClose} zIndex={5000}>
<ModalDialog.Title> <ModalDialog.Header>
{title} <ModalDialog.Title>
</ModalDialog.Title> {title}
</ModalDialog.Header> </ModalDialog.Title>
<ModalDialog.Body> </ModalDialog.Header>
{description} <ModalDialog.Body>
</ModalDialog.Body> {description}
<ModalDialog.Footer> </ModalDialog.Body>
<ActionRow> <ModalDialog.Footer>
<ModalDialog.CloseButton variant={closeButtonVaraint}> <ActionRow>
{intl.formatMessage(messages.confirmationCancel)} <ModalDialog.CloseButton variant={closeButtonVaraint}>
</ModalDialog.CloseButton> {intl.formatMessage(messages.confirmationCancel)}
<Button variant={confirmButtonVariant} onClick={comfirmAction}> </ModalDialog.CloseButton>
{ confirmButtonText || intl.formatMessage(messages.confirmationConfirm)} <Button variant={confirmButtonVariant} onClick={comfirmAction}>
</Button> { confirmButtonText || intl.formatMessage(messages.confirmationConfirm)}
</ActionRow> </Button>
</ModalDialog.Footer> </ActionRow>
</ModalDialog> </ModalDialog.Footer>
); </ModalDialog>
);
}
Confirmation.propTypes = { Confirmation.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -13,11 +13,11 @@ import messages from '../post-comments/messages';
import AuthorLabel from './AuthorLabel'; import AuthorLabel from './AuthorLabel';
import timeLocale from './time-locale'; import timeLocale from './time-locale';
const EndorsedAlertBanner = ({ function EndorsedAlertBanner({
intl, intl,
content, content,
postType, postType,
}) => { }) {
timeago.register('time-locale', timeLocale); timeago.register('time-locale', timeLocale);
const isQuestion = postType === ThreadType.QUESTION; const isQuestion = postType === ThreadType.QUESTION;
const classes = isQuestion ? 'bg-success-500 text-white' : 'bg-dark-500 text-white'; const classes = isQuestion ? 'bg-success-500 text-white' : 'bg-dark-500 text-white';
@@ -58,7 +58,7 @@ const EndorsedAlertBanner = ({
</Alert> </Alert>
) )
); );
}; }
EndorsedAlertBanner.propTypes = { EndorsedAlertBanner.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -21,7 +21,9 @@ function buildTestContent(type, buildParams) {
return camelCaseObject(Factory.build(type, { ...buildParamsSnakeCase }, null)); return camelCaseObject(Factory.build(type, { ...buildParamsSnakeCase }, null));
} }
function renderComponent(content, postType) { function renderComponent(
content, postType,
) {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<AppProvider store={store}> <AppProvider store={store}>

View File

@@ -17,7 +17,7 @@ import { postShape } from '../posts/post/proptypes';
import ActionsDropdown from './ActionsDropdown'; import ActionsDropdown from './ActionsDropdown';
import { DiscussionContext } from './context'; import { DiscussionContext } from './context';
const HoverCard = ({ function HoverCard({
intl, intl,
commentOrPost, commentOrPost,
actionHandlers, actionHandlers,
@@ -27,7 +27,7 @@ const HoverCard = ({
onFollow, onFollow,
isClosedPost, isClosedPost,
endorseIcons, endorseIcons,
}) => { }) {
const { enableInContextSidebar } = useContext(DiscussionContext); const { enableInContextSidebar } = useContext(DiscussionContext);
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate(); const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
return ( return (
@@ -40,10 +40,8 @@ const HoverCard = ({
<div className="d-flex"> <div className="d-flex">
<Button <Button
variant="tertiary" variant="tertiary"
className={classNames( 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 font-size-12', { 'w-100': enableInContextSidebar })}
{ 'w-100': enableInContextSidebar },
)}
onClick={() => handleResponseCommentButton()} onClick={() => handleResponseCommentButton()}
disabled={isClosedPost} disabled={isClosedPost}
style={{ lineHeight: '20px' }} style={{ lineHeight: '20px' }}
@@ -109,7 +107,7 @@ const HoverCard = ({
</div> </div>
</div> </div>
); );
}; }
HoverCard.propTypes = { HoverCard.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
@@ -120,7 +118,6 @@ HoverCard.propTypes = {
onFollow: PropTypes.func, onFollow: PropTypes.func,
addResponseCommentButtonMessage: PropTypes.string.isRequired, addResponseCommentButtonMessage: PropTypes.string.isRequired,
isClosedPost: PropTypes.bool.isRequired, isClosedPost: PropTypes.bool.isRequired,
// eslint-disable-next-line react/forbid-prop-types
endorseIcons: PropTypes.objectOf(PropTypes.any), endorseIcons: PropTypes.objectOf(PropTypes.any),
}; };

View File

@@ -30,14 +30,14 @@ const generateApiResponse = (blackouts = [], isCourseAdmin = false) => ({
describe('Hooks', () => { describe('Hooks', () => {
describe('useCurrentDiscussionTopic', () => { describe('useCurrentDiscussionTopic', () => {
const ComponentWithHook = () => { function ComponentWithHook() {
const topic = useCurrentDiscussionTopic(); const topic = useCurrentDiscussionTopic();
return ( return (
<div> <div>
{String(topic)} {String(topic)}
</div> </div>
); );
}; }
function renderComponent({ topicId, category }) { function renderComponent({ topicId, category }) {
return render( return render(
@@ -103,14 +103,14 @@ describe('Hooks', () => {
}); });
describe('useUserCanAddThreadInBlackoutDate', () => { describe('useUserCanAddThreadInBlackoutDate', () => {
const ComponentWithHook = () => { function ComponentWithHook() {
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate(); const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
return ( return (
<div> <div>
{String(userCanAddThreadInBlackoutDate)} {String(userCanAddThreadInBlackoutDate)}
</div> </div>
); );
}; }
function renderComponent() { function renderComponent() {
return render( return render(

View File

@@ -9,9 +9,9 @@ import { selectBlackoutDate } from '../data/selectors';
import messages from '../messages'; import messages from '../messages';
import { inBlackoutDateRange } from '../utils'; import { inBlackoutDateRange } from '../utils';
const BlackoutInformationBanner = ({ function BlackoutInformationBanner({
intl, intl,
}) => { }) {
const isDiscussionsBlackout = inBlackoutDateRange(useSelector(selectBlackoutDate)); const isDiscussionsBlackout = inBlackoutDateRange(useSelector(selectBlackoutDate));
const [showBanner, setShowBanner] = useState(true); const [showBanner, setShowBanner] = useState(true);
@@ -27,7 +27,7 @@ const BlackoutInformationBanner = ({
</div> </div>
</PageBanner> </PageBanner>
); );
}; }
BlackoutInformationBanner.propTypes = { BlackoutInformationBanner.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -9,7 +9,7 @@ import { Routes } from '../../data/constants';
import { PostCommentsView } from '../post-comments'; import { PostCommentsView } from '../post-comments';
import { PostEditor } from '../posts'; import { PostEditor } from '../posts';
const DiscussionContent = () => { function DiscussionContent() {
const postEditorVisible = useSelector((state) => state.threads.postEditorVisible); const postEditorVisible = useSelector((state) => state.threads.postEditorVisible);
return ( return (
@@ -32,6 +32,6 @@ const DiscussionContent = () => {
</div> </div>
</div> </div>
); );
}; }
export default injectIntl(DiscussionContent); export default injectIntl(DiscussionContent);

View File

@@ -20,7 +20,7 @@ import { LearnerPostsView, LearnersView } from '../learners';
import { PostsView } from '../posts'; import { PostsView } from '../posts';
import { TopicsView as LegacyTopicsView } from '../topics'; import { TopicsView as LegacyTopicsView } from '../topics';
const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => { export default function DiscussionSidebar({ displaySidebar, postActionBarRef }) {
const location = useLocation(); const location = useLocation();
const isOnDesktop = useIsOnDesktop(); const isOnDesktop = useIsOnDesktop();
const isOnXLDesktop = useIsOnXLDesktop(); const isOnXLDesktop = useIsOnXLDesktop();
@@ -98,7 +98,7 @@ const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => {
</Switch> </Switch>
</div> </div>
); );
}; }
DiscussionSidebar.defaultProps = { DiscussionSidebar.defaultProps = {
displaySidebar: false, displaySidebar: false,
@@ -112,5 +112,3 @@ DiscussionSidebar.propTypes = {
PropTypes.shape({ current: PropTypes.instanceOf(Element) }), PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]), ]),
}; };
export default DiscussionSidebar;

View File

@@ -32,7 +32,7 @@ import DiscussionSidebar from './DiscussionSidebar';
import useFeedbackWrapper from './FeedbackWrapper'; import useFeedbackWrapper from './FeedbackWrapper';
import InformationBanner from './InformationBanner'; import InformationBanner from './InformationBanner';
const DiscussionsHome = () => { export default function DiscussionsHome() {
const location = useLocation(); const location = useLocation();
const postActionBarRef = useRef(null); const postActionBarRef = useRef(null);
const postEditorVisible = useSelector(selectPostEditorVisible); const postEditorVisible = useSelector(selectPostEditorVisible);
@@ -67,7 +67,6 @@ const DiscussionsHome = () => {
}, [path]); }, [path]);
return ( return (
// eslint-disable-next-line react/jsx-no-constructed-context-values
<DiscussionContext.Provider value={{ <DiscussionContext.Provider value={{
page, page,
courseId, courseId,
@@ -131,6 +130,4 @@ const DiscussionsHome = () => {
{!enableInContextSidebar && <Footer />} {!enableInContextSidebar && <Footer />}
</DiscussionContext.Provider> </DiscussionContext.Provider>
); );
}; }
export default React.memo(DiscussionsHome);

View File

@@ -172,8 +172,7 @@ describe('DiscussionsHome', () => {
it.each([ it.each([
{ searchByEndPoint: 'category/section-topic-1' }, { searchByEndPoint: 'category/section-topic-1' },
{ searchByEndPoint: 'topics' }, { searchByEndPoint: 'topics' },
])( ])('should display No Topic selected message on inContext topic pages when user has yet to select a topic %s',
'should display No Topic selected message on inContext topic pages when user has yet to select a topic %s',
async ({ searchByEndPoint }) => { async ({ searchByEndPoint }) => {
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
enableInContext: true, provider: 'openedx', hasModerationPrivileges: true, enableInContext: true, provider: 'openedx', hasModerationPrivileges: true,
@@ -194,8 +193,7 @@ describe('DiscussionsHome', () => {
await renderComponent(`/${courseId}/${searchByEndPoint}`); await renderComponent(`/${courseId}/${searchByEndPoint}`);
expect(screen.queryByText('No topic selected')).toBeInTheDocument(); expect(screen.queryByText('No topic selected')).toBeInTheDocument();
}, });
);
it('should display empty page message for empty learners list', async () => { it('should display empty page message for empty learners list', async () => {
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, { axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {

View File

@@ -8,9 +8,9 @@ import { Hyperlink, PageBanner } from '@edx/paragon';
import { selectUserIsStaff, selectUserRoles } from '../data/selectors'; import { selectUserIsStaff, selectUserRoles } from '../data/selectors';
import messages from '../messages'; import messages from '../messages';
const InformationBanner = ({ function InformationBanner({
intl, intl,
}) => { }) {
const [showBanner, setShowBanner] = useState(true); const [showBanner, setShowBanner] = useState(true);
const userRoles = useSelector(selectUserRoles); const userRoles = useSelector(selectUserRoles);
const isAdmin = useSelector(selectUserIsStaff); const isAdmin = useSelector(selectUserIsStaff);
@@ -55,7 +55,7 @@ const InformationBanner = ({
</div> </div>
</PageBanner> </PageBanner>
); );
}; }
InformationBanner.propTypes = { InformationBanner.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -6,7 +6,7 @@ import { useIsOnDesktop } from '../data/hooks';
import messages from '../messages'; import messages from '../messages';
import EmptyPage from './EmptyPage'; import EmptyPage from './EmptyPage';
const EmptyLearners = ({ intl }) => { function EmptyLearners({ intl }) {
const isOnDesktop = useIsOnDesktop(); const isOnDesktop = useIsOnDesktop();
if (!isOnDesktop) { if (!isOnDesktop) {
@@ -16,7 +16,7 @@ const EmptyLearners = ({ intl }) => {
return ( return (
<EmptyPage title={intl.formatMessage(messages.emptyTitle)} /> <EmptyPage title={intl.formatMessage(messages.emptyTitle)} />
); );
}; }
EmptyLearners.propTypes = { EmptyLearners.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -7,13 +7,13 @@ import { Button } from '@edx/paragon';
import { ReactComponent as EmptyIcon } from '../../assets/empty.svg'; import { ReactComponent as EmptyIcon } from '../../assets/empty.svg';
const EmptyPage = ({ function EmptyPage({
title, title,
subTitle = null, subTitle = null,
action = null, action = null,
actionText = null, actionText = null,
fullWidth = false, fullWidth = false,
}) => { }) {
const containerClasses = classNames( const containerClasses = classNames(
'min-content-height justify-content-center align-items-center d-flex w-100 flex-column', 'min-content-height justify-content-center align-items-center d-flex w-100 flex-column',
{ 'bg-light-400': !fullWidth }, { 'bg-light-400': !fullWidth },
@@ -33,7 +33,7 @@ const EmptyPage = ({
</div> </div>
</div> </div>
); );
}; }
EmptyPage.propTypes = { EmptyPage.propTypes = {
title: propTypes.string.isRequired, title: propTypes.string.isRequired,

View File

@@ -8,11 +8,10 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIsOnDesktop } from '../data/hooks'; import { useIsOnDesktop } from '../data/hooks';
import { selectAreThreadsFiltered, selectPostThreadCount } from '../data/selectors'; import { selectAreThreadsFiltered, selectPostThreadCount } from '../data/selectors';
import messages from '../messages'; import messages from '../messages';
// eslint-disable-next-line import/no-cycle
import { messages as postMessages, showPostEditor } from '../posts'; import { messages as postMessages, showPostEditor } from '../posts';
import EmptyPage from './EmptyPage'; import EmptyPage from './EmptyPage';
const EmptyPosts = ({ intl, subTitleMessage }) => { function EmptyPosts({ intl, subTitleMessage }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isFiltered = useSelector(selectAreThreadsFiltered); const isFiltered = useSelector(selectAreThreadsFiltered);
@@ -50,7 +49,7 @@ const EmptyPosts = ({ intl, subTitleMessage }) => {
fullWidth={fullWidth} fullWidth={fullWidth}
/> />
); );
}; }
EmptyPosts.propTypes = { EmptyPosts.propTypes = {
subTitleMessage: propTypes.shape({ subTitleMessage: propTypes.shape({

View File

@@ -9,11 +9,10 @@ import { ALL_ROUTES } from '../../data/constants';
import { useIsOnDesktop, useTotalTopicThreadCount } from '../data/hooks'; import { useIsOnDesktop, useTotalTopicThreadCount } from '../data/hooks';
import { selectTopicThreadCount } from '../data/selectors'; import { selectTopicThreadCount } from '../data/selectors';
import messages from '../messages'; import messages from '../messages';
// eslint-disable-next-line import/no-cycle
import { messages as postMessages, showPostEditor } from '../posts'; import { messages as postMessages, showPostEditor } from '../posts';
import EmptyPage from './EmptyPage'; import EmptyPage from './EmptyPage';
const EmptyTopics = ({ intl }) => { function EmptyTopics({ intl }) {
const match = useRouteMatch(ALL_ROUTES); const match = useRouteMatch(ALL_ROUTES);
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -63,7 +62,7 @@ const EmptyTopics = ({ intl }) => {
fullWidth={fullWidth} fullWidth={fullWidth}
/> />
); );
}; }
EmptyTopics.propTypes = { EmptyTopics.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -1,5 +1,4 @@
export { default as EmptyLearners } from './EmptyLearners'; export { default as EmptyLearners } from './EmptyLearners';
export { default as EmptyPage } from './EmptyPage'; export { default as EmptyPage } from './EmptyPage';
// eslint-disable-next-line import/no-cycle
export { default as EmptyPosts } from './EmptyPosts'; export { default as EmptyPosts } from './EmptyPosts';
export { default as EmptyTopics } from './EmptyTopics'; export { default as EmptyTopics } from './EmptyTopics';

View File

@@ -21,7 +21,7 @@ import { BackButton, NoResults } from './components';
import messages from './messages'; import messages from './messages';
import { Topic } from './topic'; import { Topic } from './topic';
const TopicPostsView = ({ intl }) => { function TopicPostsView({ intl }) {
const location = useLocation(); const location = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { courseId, topicId, category } = useContext(DiscussionContext); const { courseId, topicId, category } = useContext(DiscussionContext);
@@ -90,7 +90,7 @@ const TopicPostsView = ({ intl }) => {
</div> </div>
</div> </div>
); );
}; }
TopicPostsView.propTypes = { TopicPostsView.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -206,8 +206,7 @@ describe('InContext Topic Posts View', () => {
test.each([ test.each([
{ searchText: 'hello world', output: 'Showing 0 results for', resultCount: 0 }, { searchText: 'hello world', output: 'Showing 0 results for', resultCount: 0 },
{ searchText: 'introduction', output: 'Showing 8 results for', resultCount: 8 }, { searchText: 'introduction', output: 'Showing 8 results for', resultCount: 8 },
])( ])('It should have a search bar with a clear button and \'$output\' results found text.',
'It should have a search bar with a clear button and \'$output\' results found text.',
async ({ searchText, output, resultCount }) => { async ({ searchText, output, resultCount }) => {
await setupTopicsMockResponse(); await setupTopicsMockResponse();
await renderComponent(); await renderComponent();
@@ -227,8 +226,7 @@ describe('InContext Topic Posts View', () => {
expect(clearButton).toBeInTheDocument(); expect(clearButton).toBeInTheDocument();
expect(units).toHaveLength(resultCount); expect(units).toHaveLength(resultCount);
}); });
}, });
);
it('When click on the clear button it should move to main topics pages.', async () => { it('When click on the clear button it should move to main topics pages.', async () => {
await setupTopicsMockResponse(); await setupTopicsMockResponse();
@@ -255,8 +253,7 @@ describe('InContext Topic Posts View', () => {
}); });
}); });
it( it('should display Nothing here yet and No topic exists message when topics and selectedSubsectionUnits are empty',
'should display Nothing here yet and No topic exists message when topics and selectedSubsectionUnits are empty',
async () => { async () => {
await setupTopicsMockResponse(0, 0, 0); await setupTopicsMockResponse(0, 0, 0);
await renderComponent({ topicId: 'test-topic', category: 'test-category' }); await renderComponent({ topicId: 'test-topic', category: 'test-category' });
@@ -264,8 +261,7 @@ describe('InContext Topic Posts View', () => {
await waitFor(() => expect(within(container).queryByText('Nothing here yet')).toBeInTheDocument()); await waitFor(() => expect(within(container).queryByText('Nothing here yet')).toBeInTheDocument());
expect(within(container).queryByText('No topic exists')).toBeInTheDocument(); expect(within(container).queryByText('No topic exists')).toBeInTheDocument();
expect(within(container).queryByText('Unnamed Topic')).toBeInTheDocument(); expect(within(container).queryByText('Unnamed Topic')).toBeInTheDocument();
}, });
);
it('should display all topics when search by an empty search string', async () => { it('should display all topics when search by an empty search string', async () => {
await setupTopicsMockResponse(); await setupTopicsMockResponse();

View File

@@ -21,7 +21,7 @@ import { setFilter } from './data/slices';
import { fetchCourseTopicsV3 } from './data/thunks'; import { fetchCourseTopicsV3 } from './data/thunks';
import { ArchivedBaseGroup, SectionBaseGroup, Topic } from './topic'; import { ArchivedBaseGroup, SectionBaseGroup, Topic } from './topic';
const TopicsList = () => { function TopicsList() {
const loadingStatus = useSelector(selectLoadingStatus); const loadingStatus = useSelector(selectLoadingStatus);
const coursewareTopics = useSelector(selectCoursewareTopics); const coursewareTopics = useSelector(selectCoursewareTopics);
const nonCoursewareTopics = useSelector(selectNonCoursewareTopics); const nonCoursewareTopics = useSelector(selectNonCoursewareTopics);
@@ -58,9 +58,9 @@ const TopicsList = () => {
)} )}
</> </>
); );
}; }
const TopicsView = () => { function TopicsView() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { courseId } = useContext(DiscussionContext); const { courseId } = useContext(DiscussionContext);
const provider = useSelector(selectDiscussionProvider); const provider = useSelector(selectDiscussionProvider);
@@ -116,6 +116,6 @@ const TopicsView = () => {
</div> </div>
</div> </div>
); );
}; }
export default TopicsView; export default TopicsView;

View File

@@ -9,9 +9,9 @@ import { ArrowBack } from '@edx/paragon/icons';
import messages from '../messages'; import messages from '../messages';
const BackButton = ({ function BackButton({
intl, path, title, loading, intl, path, title, loading,
}) => { }) {
const history = useHistory(); const history = useHistory();
return ( return (
@@ -32,7 +32,7 @@ const BackButton = ({
<div className="border-bottom border-light-400" /> <div className="border-bottom border-light-400" />
</> </>
); );
}; }
BackButton.propTypes = { BackButton.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -14,7 +14,7 @@ import messages from '../../messages';
import { messages as postMessages, showPostEditor } from '../../posts'; import { messages as postMessages, showPostEditor } from '../../posts';
import { selectCourseWareThreadsCount, selectTotalTopicsThreadsCount } from '../data/selectors'; import { selectCourseWareThreadsCount, selectTotalTopicsThreadsCount } from '../data/selectors';
const EmptyTopics = ({ intl }) => { function EmptyTopics({ intl }) {
const match = useRouteMatch(ALL_ROUTES); const match = useRouteMatch(ALL_ROUTES);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { enableInContextSidebar } = useContext(DiscussionContext); const { enableInContextSidebar } = useContext(DiscussionContext);
@@ -74,7 +74,7 @@ const EmptyTopics = ({ intl }) => {
fullWidth={fullWidth} fullWidth={fullWidth}
/> />
); );
}; }
EmptyTopics.propTypes = { EmptyTopics.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -5,7 +5,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { selectTopics } from '../data/selectors'; import { selectTopics } from '../data/selectors';
import messages from '../messages'; import messages from '../messages';
const NoResults = ({ intl }) => { function NoResults({ intl }) {
const topics = useSelector(selectTopics); const topics = useSelector(selectTopics);
const title = messages.nothingHere; const title = messages.nothingHere;
@@ -20,7 +20,7 @@ const NoResults = ({ intl }) => {
{ helpMessage && <small className="font-weight-normal text-gray-700">{intl.formatMessage(helpMessage)}</small>} { helpMessage && <small className="font-weight-normal text-gray-700">{intl.formatMessage(helpMessage)}</small>}
</div> </div>
); );
}; }
NoResults.propTypes = { NoResults.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -20,21 +20,12 @@ Factory.define('sub-section')
.sequence('id', ['topicPrefix'], (idx, topicPrefix) => `${topicPrefix}-topic-${idx}`) .sequence('id', ['topicPrefix'], (idx, topicPrefix) => `${topicPrefix}-topic-${idx}`)
.sequence('display-name', ['sectionPrefix'], (idx, sectionPrefix) => `Introduction ${sectionPrefix + idx}`) .sequence('display-name', ['sectionPrefix'], (idx, sectionPrefix) => `Introduction ${sectionPrefix + idx}`)
.option('courseId', null, 'course-v1:edX+DemoX+Demo_Course') .option('courseId', null, 'course-v1:edX+DemoX+Demo_Course')
.sequence( .sequence('legacy_web_url', ['id', 'courseId'],
'legacy_web_url', (idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}?experience=legacy`)
['id', 'courseId'], .sequence('lms_web_url', ['id', 'courseId'],
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}?experience=legacy`, (idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}`)
) .sequence('student_view_url', ['id', 'courseId'],
.sequence( (idx, id) => `${getApiBaseUrl}/xblock/block-v1:${id}`)
'lms_web_url',
['id', 'courseId'],
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}`,
)
.sequence(
'student_view_url',
['id', 'courseId'],
(idx, id) => `${getApiBaseUrl}/xblock/block-v1:${id}`,
)
.attr('type', null, 'sequential') .attr('type', null, 'sequential')
.attr('activeFlags', null, true) .attr('activeFlags', null, true)
.attr('thread_counts', ['discussionCount', 'questionCount'], (discCount, questCount) => { .attr('thread_counts', ['discussionCount', 'questionCount'], (discCount, questCount) => {
@@ -60,21 +51,12 @@ Factory.define('section')
.attr('courseware', null, true) .attr('courseware', null, true)
.sequence('display-name', (idx) => `Introduction ${idx}`) .sequence('display-name', (idx) => `Introduction ${idx}`)
.option('courseId', null, 'course-v1:edX+DemoX+Demo_Course') .option('courseId', null, 'course-v1:edX+DemoX+Demo_Course')
.sequence( .sequence('legacy_web_url', ['id', 'courseId'],
'legacy_web_url', (idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}?experience=legacy`)
['id', 'courseId'], .sequence('lms_web_url', ['id', 'courseId'],
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}?experience=legacy`, (idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`)
) .sequence('student_view_url', ['id', 'courseId'],
.sequence( (idx, id, courseId) => `${getApiBaseUrl}/xblock/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`)
'lms_web_url',
['id', 'courseId'],
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`,
)
.sequence(
'student_view_url',
['id', 'courseId'],
(idx, id, courseId) => `${getApiBaseUrl}/xblock/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`,
)
.attr('type', null, 'chapter') .attr('type', null, 'chapter')
.attr('children', ['id', 'display-name'], (id, name) => { .attr('children', ['id', 'display-name'], (id, name) => {
Factory.reset('sub-section'); Factory.reset('sub-section');

View File

@@ -10,7 +10,7 @@ import { DiscussionContext } from '../../common/context';
import postsMessages from '../../posts/post-actions-bar/messages'; import postsMessages from '../../posts/post-actions-bar/messages';
import { setFilter as setTopicFilter } from '../data/slices'; import { setFilter as setTopicFilter } from '../data/slices';
const TopicSearchBar = ({ intl }) => { function TopicSearchBar({ intl }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { page } = useContext(DiscussionContext); const { page } = useContext(DiscussionContext);
const topicSearch = useSelector(({ inContextTopics }) => inContextTopics.filter); const topicSearch = useSelector(({ inContextTopics }) => inContextTopics.filter);
@@ -34,27 +34,29 @@ const TopicSearchBar = ({ intl }) => {
useEffect(() => onClear(), [page]); useEffect(() => onClear(), [page]);
return ( return (
<SearchField.Advanced <>
onClear={onClear} <SearchField.Advanced
onChange={onChange} onClear={onClear}
onSubmit={onSubmit} onChange={onChange}
value={topicSearch} onSubmit={onSubmit}
> value={topicSearch}
<SearchField.Label /> >
<SearchField.Input <SearchField.Label />
style={{ paddingRight: '1rem' }} <SearchField.Input
placeholder={intl.formatMessage(postsMessages.search, { page: 'topics' })} style={{ paddingRight: '1rem' }}
/> placeholder={intl.formatMessage(postsMessages.search, { page: 'topics' })}
<span className="mt-auto mb-auto mr-2.5 pointer-cursor-hover">
<Icon
src={SearchIcon}
onClick={() => onSubmit(searchValue)}
data-testid="search-icon"
/> />
</span> <span className="mt-auto mb-auto mr-2.5 pointer-cursor-hover">
</SearchField.Advanced> <Icon
src={SearchIcon}
onClick={() => onSubmit(searchValue)}
data-testid="search-icon"
/>
</span>
</SearchField.Advanced>
</>
); );
}; }
TopicSearchBar.propTypes = { TopicSearchBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -8,7 +8,7 @@ import { SearchField } from '@edx/paragon';
import { setFilter } from '../data'; import { setFilter } from '../data';
import messages from '../messages'; import messages from '../messages';
const TopicSearchResultBar = ({ intl }) => { function TopicSearchResultBar({ intl }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
return ( return (
@@ -21,7 +21,7 @@ const TopicSearchResultBar = ({ intl }) => {
/> />
</div> </div>
); );
}; }
TopicSearchResultBar.propTypes = { TopicSearchResultBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -6,33 +6,35 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from '../messages'; import messages from '../messages';
import Topic, { topicShape } from './Topic'; import Topic, { topicShape } from './Topic';
const ArchivedBaseGroup = ({ function ArchivedBaseGroup({
archivedTopics, archivedTopics,
showDivider, showDivider,
intl, intl,
}) => ( }) {
<> return (
{showDivider && (
<> <>
<div className="divider border-top border-light-500" /> {showDivider && (
<div className="divider pt-1 bg-light-300" /> <>
<div className="divider border-top border-light-500" />
<div className="divider pt-1 bg-light-300" />
</>
)}
<div
className="discussion-topic-group d-flex flex-column text-primary-500"
data-testid="archived-group"
>
<div className="pt-3 px-4 font-weight-bold">{intl.formatMessage(messages.archivedTopics)}</div>
{archivedTopics?.map((topic, index) => (
<Topic
key={topic.id}
topic={topic}
showDivider={(archivedTopics.length - 1) !== index}
/>
))}
</div>
</> </>
)} );
<div }
className="discussion-topic-group d-flex flex-column text-primary-500"
data-testid="archived-group"
>
<div className="pt-3 px-4 font-weight-bold">{intl.formatMessage(messages.archivedTopics)}</div>
{archivedTopics?.map((topic, index) => (
<Topic
key={topic.id}
topic={topic}
showDivider={(archivedTopics.length - 1) !== index}
/>
))}
</div>
</>
);
ArchivedBaseGroup.propTypes = { ArchivedBaseGroup.propTypes = {
archivedTopics: PropTypes.arrayOf(topicShape).isRequired, archivedTopics: PropTypes.arrayOf(topicShape).isRequired,

View File

@@ -13,13 +13,13 @@ import { discussionsPath } from '../../utils';
import messages from '../messages'; import messages from '../messages';
import { topicShape } from './Topic'; import { topicShape } from './Topic';
const SectionBaseGroup = ({ function SectionBaseGroup({
section, section,
sectionTitle, sectionTitle,
sectionId, sectionId,
showDivider, showDivider,
intl, intl,
}) => { }) {
const { courseId } = useParams(); const { courseId } = useParams();
const isSelected = (id) => window.location.pathname.includes(id); const isSelected = (id) => window.location.pathname.includes(id);
const sectionUrl = (id) => discussionsPath(Routes.TOPICS.CATEGORY, { const sectionUrl = (id) => discussionsPath(Routes.TOPICS.CATEGORY, {
@@ -70,7 +70,7 @@ const SectionBaseGroup = ({
)} )}
</div> </div>
); );
}; }
SectionBaseGroup.propTypes = { SectionBaseGroup.propTypes = {
section: PropTypes.arrayOf(PropTypes.shape({ section: PropTypes.arrayOf(PropTypes.shape({

View File

@@ -1,4 +1,4 @@
/* eslint-disable no-unused-vars, react/forbid-prop-types, react/prop-types */ /* eslint-disable no-unused-vars, react/forbid-prop-types */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -17,12 +17,12 @@ import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../da
import { discussionsPath } from '../../utils'; import { discussionsPath } from '../../utils';
import messages from '../messages'; import messages from '../messages';
const Topic = ({ function Topic({
topic, topic,
showDivider, showDivider,
index, index,
intl, intl,
}) => { }) {
const { courseId } = useParams(); const { courseId } = useParams();
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa); const userIsGroupTa = useSelector(selectUserIsGroupTa);
@@ -70,7 +70,7 @@ const Topic = ({
)} )}
</> </>
); );
}; }
export const topicShape = PropTypes.shape({ export const topicShape = PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,

View File

@@ -31,7 +31,7 @@ import { fetchUserPosts } from './data/thunks';
import LearnerPostFilterBar from './learner-post-filter-bar/LearnerPostFilterBar'; import LearnerPostFilterBar from './learner-post-filter-bar/LearnerPostFilterBar';
import messages from './messages'; import messages from './messages';
const LearnerPostsView = ({ intl }) => { function LearnerPostsView({ intl }) {
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -114,7 +114,7 @@ const LearnerPostsView = ({ intl }) => {
</div> </div>
</div> </div>
); );
}; }
LearnerPostsView.propTypes = { LearnerPostsView.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -106,8 +106,7 @@ describe('Learner Posts View', () => {
expect(backButton).toBeInTheDocument(); expect(backButton).toBeInTheDocument();
}); });
test( test('Learner title bar should redirect to the learners list when clicking on the back button',
'Learner title bar should redirect to the learners list when clicking on the back button',
async () => { async () => {
await renderComponent(); await renderComponent();
@@ -117,8 +116,7 @@ describe('Learner Posts View', () => {
await waitFor(() => { await waitFor(() => {
expect(lastLocation.pathname.endsWith('/learners')).toBeTruthy(); expect(lastLocation.pathname.endsWith('/learners')).toBeTruthy();
}); });
}, });
);
it('should display a post-filter bar and All posts sorted by recent activity text.', async () => { it('should display a post-filter bar and All posts sorted by recent activity text.', async () => {
await renderComponent(); await renderComponent();

View File

@@ -24,7 +24,7 @@ import { fetchLearners } from './data/thunks';
import { LearnerCard, LearnerFilterBar } from './learner'; import { LearnerCard, LearnerFilterBar } from './learner';
import messages from './messages'; import messages from './messages';
const LearnersView = ({ intl }) => { function LearnersView({ intl }) {
const { courseId } = useParams(); const { courseId } = useParams();
const location = useLocation(); const location = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -98,7 +98,7 @@ const LearnersView = ({ intl }) => {
</div> </div>
</div> </div>
); );
}; }
LearnersView.propTypes = { LearnersView.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -1,4 +1,3 @@
/* eslint-disable default-param-last */
import React from 'react'; import React from 'react';
import { import {
@@ -201,8 +200,7 @@ describe('LearnersView', () => {
username: username:
['learner-1', 'learner-2'], ['learner-1', 'learner-2'],
}, },
])( ])('should have a search bar with a clear button and \'$output\' results found text.',
'should have a search bar with a clear button and \'$output\' results found text.',
async ({ async ({
searchText, output, learnersCount, username, searchText, output, learnersCount, username,
}) => { }) => {
@@ -228,8 +226,7 @@ describe('LearnersView', () => {
expect(clearButton).toBeInTheDocument(); expect(clearButton).toBeInTheDocument();
expect(leaners).toHaveLength(learnersCount); expect(leaners).toHaveLength(learnersCount);
}); });
}, });
);
test('When click on the clear button it should move to a list of all learners.', async () => { test('When click on the clear button it should move to a list of all learners.', async () => {
await setUpLearnerMockResponse(); await setUpLearnerMockResponse();
@@ -259,8 +256,7 @@ describe('LearnersView', () => {
expect(learners).toHaveLength(3); expect(learners).toHaveLength(3);
}); });
it( it('should display reported and previously reported message by passing activeFlags or inactiveFlags',
'should display reported and previously reported message by passing activeFlags or inactiveFlags',
async () => { async () => {
await setUpLearnerMockResponse(2, 2, 1, ['learner-1', 'learner-2'], '', 1, 1); await setUpLearnerMockResponse(2, 2, 1, ['learner-1', 'learner-2'], '', 1, 1);
await assignPrivilages(true); await assignPrivilages(true);
@@ -277,8 +273,7 @@ describe('LearnersView', () => {
expect(reportedIcon).toBeInTheDocument(); expect(reportedIcon).toBeInTheDocument();
expect(reported).toBeInTheDocument(); expect(reported).toBeInTheDocument();
expect(previouslyReported).toBeInTheDocument(); expect(previouslyReported).toBeInTheDocument();
}, });
);
it('should display load more button and display more learners by clicking on button.', async () => { it('should display load more button and display more learners by clicking on button.', async () => {
await setUpLearnerMockResponse(); await setUpLearnerMockResponse();

View File

@@ -29,8 +29,7 @@ describe('Learner api test cases', () => {
axiosMock.reset(); axiosMock.reset();
}); });
it( it('Successfully get and store API response for the learner\'s list and learners posts in redux',
'Successfully get and store API response for the learner\'s list and learners posts in redux',
async () => { async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
const threads = await setupPostsMockResponse(); const threads = await setupPostsMockResponse();
@@ -39,23 +38,20 @@ describe('Learner api test cases', () => {
expect(Object.values(learners.learnerProfiles)).toHaveLength(3); expect(Object.values(learners.learnerProfiles)).toHaveLength(3);
expect(threads.status).toEqual('successful'); expect(threads.status).toEqual('successful');
expect(Object.values(threads.threadsById)).toHaveLength(2); expect(Object.values(threads.threadsById)).toHaveLength(2);
}, });
);
it.each([ it.each([
{ status: 'statusUnread', search: 'Title', cohort: 'post' }, { status: 'statusUnread', search: 'Title', cohort: 'post' },
{ status: 'statusUnanswered', search: 'Title', cohort: 'post' }, { status: 'statusUnanswered', search: 'Title', cohort: 'post' },
{ status: 'statusReported', search: 'Title', cohort: 'post' }, { status: 'statusReported', search: 'Title', cohort: 'post' },
{ status: 'statusUnresponded', search: 'Title', cohort: 'post' }, { status: 'statusUnresponded', search: 'Title', cohort: 'post' },
])( ])('Successfully fetch user posts based on %s filters',
'Successfully fetch user posts based on %s filters',
async ({ status, search, cohort }) => { async ({ status, search, cohort }) => {
const threads = await setupPostsMockResponse({ filters: { status, search, cohort } }); const threads = await setupPostsMockResponse({ filters: { status, search, cohort } });
expect(threads.status).toEqual('successful'); expect(threads.status).toEqual('successful');
expect(Object.values(threads.threadsById)).toHaveLength(2); expect(Object.values(threads.threadsById)).toHaveLength(2);
}, });
);
it('Failed to fetch learners', async () => { it('Failed to fetch learners', async () => {
const learners = await setupLearnerMockResponse({ learnerCourseId: courseId2 }); const learners = await setupLearnerMockResponse({ learnerCourseId: courseId2 });

View File

@@ -37,8 +37,7 @@ describe('Learner redux test cases', () => {
test('Successfully load initial states in redux', async () => { test('Successfully load initial states in redux', async () => {
executeThunk( executeThunk(
fetchLearners('course-v1:edX+DemoX+Demo_Course', { usernameSearch: 'learner-1' }), fetchLearners('course-v1:edX+DemoX+Demo_Course', { usernameSearch: 'learner-1' }),
store.dispatch, store.dispatch, store.getState,
store.getState,
); );
const { learners } = store.getState(); const { learners } = store.getState();
@@ -56,8 +55,7 @@ describe('Learner redux test cases', () => {
expect(learners.postFilter.cohort).toEqual(''); expect(learners.postFilter.cohort).toEqual('');
}); });
test( test('Successfully store a learner posts stats data as pages object in redux',
'Successfully store a learner posts stats data as pages object in redux',
async () => { async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
const page = learners.pages[0]; const page = learners.pages[0];
@@ -67,11 +65,9 @@ describe('Learner redux test cases', () => {
expect(statsObject.responses).toEqual(3); expect(statsObject.responses).toEqual(3);
expect(statsObject.threads).toEqual(1); expect(statsObject.threads).toEqual(1);
expect(statsObject.replies).toEqual(0); expect(statsObject.replies).toEqual(0);
}, });
);
test( test('Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux',
'Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux',
async () => { async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
@@ -79,8 +75,7 @@ describe('Learner redux test cases', () => {
expect(learners.totalPages).toEqual(2); expect(learners.totalPages).toEqual(2);
expect(learners.totalLearners).toEqual(3); expect(learners.totalLearners).toEqual(3);
expect(learners.sortedBy).toEqual('activity'); expect(learners.sortedBy).toEqual('activity');
}, });
);
test('Successfully updated the learner\'s sort data in redux', async () => { test('Successfully updated the learner\'s sort data in redux', async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
@@ -111,8 +106,7 @@ describe('Learner redux test cases', () => {
expect(updatedLearners.pages).toHaveLength(0); expect(updatedLearners.pages).toHaveLength(0);
}); });
test( test('Successfully update the learner\'s search query in redux when searching for a learner',
'Successfully update the learner\'s search query in redux when searching for a learner',
async () => { async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
@@ -122,6 +116,5 @@ describe('Learner redux test cases', () => {
const updatedLearners = store.getState().learners; const updatedLearners = store.getState().learners;
expect(updatedLearners.usernameSearch).toEqual('learner-2'); expect(updatedLearners.usernameSearch).toEqual('learner-2');
}, });
);
}); });

View File

@@ -12,7 +12,7 @@ import { fetchCourseCohorts } from '../../cohorts/data/thunks';
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors'; import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
import { setPostFilter } from '../data/slices'; import { setPostFilter } from '../data/slices';
const LearnerPostFilterBar = () => { function LearnerPostFilterBar() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { courseId } = useParams(); const { courseId } = useParams();
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
@@ -98,6 +98,6 @@ const LearnerPostFilterBar = () => {
showCohortsFilter={userHasModerationPrivileges || userIsGroupTa} showCohortsFilter={userHasModerationPrivileges || userIsGroupTa}
/> />
); );
}; }
export default LearnerPostFilterBar; export default LearnerPostFilterBar;

View File

@@ -4,18 +4,20 @@ import { Avatar } from '@edx/paragon';
import { learnerShape } from './proptypes'; import { learnerShape } from './proptypes';
const LearnerAvatar = ({ learner }) => ( function LearnerAvatar({ learner }) {
<div className="mr-3 mt-1"> return (
<Avatar <div className="mr-3 mt-1">
size="sm" <Avatar
alt={learner.username} size="sm"
style={{ alt={learner.username}
height: '2rem', style={{
width: '2rem', height: '2rem',
}} width: '2rem',
/> }}
</div> />
); </div>
);
}
LearnerAvatar.propTypes = { LearnerAvatar.propTypes = {
learner: learnerShape.isRequired, learner: learnerShape.isRequired,

View File

@@ -12,10 +12,10 @@ import LearnerAvatar from './LearnerAvatar';
import LearnerFooter from './LearnerFooter'; import LearnerFooter from './LearnerFooter';
import { learnerShape } from './proptypes'; import { learnerShape } from './proptypes';
const LearnerCard = ({ function LearnerCard({
learner, learner,
courseId, courseId,
}) => { }) {
const { enableInContextSidebar, learnerUsername } = useContext(DiscussionContext); const { enableInContextSidebar, learnerUsername } = useContext(DiscussionContext);
const linkUrl = discussionsPath(Routes.LEARNERS.POSTS, { const linkUrl = discussionsPath(Routes.LEARNERS.POSTS, {
0: enableInContextSidebar ? 'in-context' : undefined, 0: enableInContextSidebar ? 'in-context' : undefined,
@@ -51,7 +51,7 @@ const LearnerCard = ({
</div> </div>
</Link> </Link>
); );
}; }
LearnerCard.propTypes = { LearnerCard.propTypes = {
learner: learnerShape.isRequired, learner: learnerShape.isRequired,

View File

@@ -47,9 +47,9 @@ ActionItem.propTypes = {
selected: PropTypes.string.isRequired, selected: PropTypes.string.isRequired,
}; };
const LearnerFilterBar = ({ function LearnerFilterBar({
intl, intl,
}) => { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa); const userIsGroupTa = useSelector(selectUserIsGroupTa);
@@ -124,7 +124,7 @@ const LearnerFilterBar = ({
</Collapsible.Body> </Collapsible.Body>
</Collapsible.Advanced> </Collapsible.Advanced>
); );
}; }
LearnerFilterBar.propTypes = { LearnerFilterBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -11,10 +11,10 @@ import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../da
import messages from '../messages'; import messages from '../messages';
import { learnerShape } from './proptypes'; import { learnerShape } from './proptypes';
const LearnerFooter = ({ function LearnerFooter({
learner, learner,
intl, intl,
}) => { }) {
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa); const userIsGroupTa = useSelector(selectUserIsGroupTa);
const { inactiveFlags } = learner; const { inactiveFlags } = learner;
@@ -83,7 +83,7 @@ const LearnerFooter = ({
)} )}
</div> </div>
); );
}; }
LearnerFooter.propTypes = { LearnerFooter.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -26,6 +26,16 @@ const messages = defineMessages({
defaultMessage: 'Unpin', defaultMessage: 'Unpin',
description: 'Action to unpin a post', description: 'Action to unpin a post',
}, },
acceptReview: {
id: 'discussions.actions.reviewAccept',
defaultMessage: 'Accept',
description: 'Action to accept content flagged for review',
},
rejectReview: {
id: 'discussions.actions.reviewReject',
defaultMessage: 'Decline',
description: 'Action to reject content flagged for review',
},
deleteAction: { deleteAction: {
id: 'discussions.actions.delete', id: 'discussions.actions.delete',
defaultMessage: 'Delete', defaultMessage: 'Delete',

View File

@@ -8,7 +8,7 @@ import { Dropdown, DropdownButton } from '@edx/paragon';
import messages from './messages'; import messages from './messages';
const BreadcrumbDropdown = ({ function BreadcrumbDropdown({
currentItem, currentItem,
intl, intl,
showAllPath, showAllPath,
@@ -17,7 +17,7 @@ const BreadcrumbDropdown = ({
itemLabelFunc, itemLabelFunc,
itemActiveFunc, itemActiveFunc,
itemFilterFunc, itemFilterFunc,
}) => { }) {
const showAllMsg = intl.formatMessage(messages.showAll); const showAllMsg = intl.formatMessage(messages.showAll);
return ( return (
<DropdownButton <DropdownButton
@@ -46,7 +46,7 @@ const BreadcrumbDropdown = ({
))} ))}
</DropdownButton> </DropdownButton>
); );
}; }
BreadcrumbDropdown.propTypes = { BreadcrumbDropdown.propTypes = {
// eslint-disable-next-line react/forbid-prop-types // eslint-disable-next-line react/forbid-prop-types

View File

@@ -13,7 +13,7 @@ import {
import { discussionsPath } from '../../utils'; import { discussionsPath } from '../../utils';
import BreadcrumbDropdown from './BreadcrumbDropdown'; import BreadcrumbDropdown from './BreadcrumbDropdown';
const LegacyBreadcrumbMenu = () => { function LegacyBreadcrumbMenu() {
const { const {
params: { params: {
courseId, courseId,
@@ -78,7 +78,7 @@ const LegacyBreadcrumbMenu = () => {
)} )}
</div> </div>
); );
}; }
LegacyBreadcrumbMenu.propTypes = {}; LegacyBreadcrumbMenu.propTypes = {};

View File

@@ -11,7 +11,7 @@ import { useShowLearnersTab } from '../../data/hooks';
import { discussionsPath } from '../../utils'; import { discussionsPath } from '../../utils';
import messages from './messages'; import messages from './messages';
const NavigationBar = ({ intl }) => { function NavigationBar({ intl }) {
const { courseId } = useParams(); const { courseId } = useParams();
const showLearnersTab = useShowLearnersTab(); const showLearnersTab = useShowLearnersTab();
@@ -52,7 +52,7 @@ const NavigationBar = ({ intl }) => {
))} ))}
</Nav> </Nav>
); );
}; }
NavigationBar.propTypes = { NavigationBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -23,7 +23,7 @@ import CommentsView from './comments/CommentsView';
import { useCommentsCount, usePost } from './data/hooks'; import { useCommentsCount, usePost } from './data/hooks';
import messages from './messages'; import messages from './messages';
const PostCommentsView = ({ intl }) => { function PostCommentsView({ intl }) {
const [isLoading, submitDispatch] = useDispatchWithState(); const [isLoading, submitDispatch] = useDispatchWithState();
const { postId } = useParams(); const { postId } = useParams();
const thread = usePost(postId); const thread = usePost(postId);
@@ -134,7 +134,7 @@ const PostCommentsView = ({ intl }) => {
)} )}
</> </>
); );
}; }
PostCommentsView.propTypes = { PostCommentsView.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -384,16 +384,12 @@ describe('ThreadView', () => {
expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument(); expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument();
expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2); expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
await act(async () => { await act(async () => {
fireEvent.change( fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }),
screen.queryByRole('combobox', { name: /reason for editing/i }), { target: { value: null } });
{ target: { value: null } },
);
}); });
await act(async () => { await act(async () => {
fireEvent.change(screen.queryByRole( fireEvent.change(screen.queryByRole('combobox',
'combobox', { name: /reason for editing/i }), { target: { value: 'reason-1' } });
{ name: /reason for editing/i },
), { target: { value: 'reason-1' } });
}); });
await act(async () => { await act(async () => {
fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } }); fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });

View File

@@ -13,7 +13,7 @@ import { selectCommentSortOrder } from '../data/selectors';
import { setCommentSortOrder } from '../data/slices'; import { setCommentSortOrder } from '../data/slices';
import messages from '../messages'; import messages from '../messages';
const CommentSortDropdown = ({ intl }) => { function CommentSortDropdown({ intl }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const sortedOrder = useSelector(selectCommentSortOrder); const sortedOrder = useSelector(selectCommentSortOrder);
const [isOpen, open, close] = useToggle(false); const [isOpen, open, close] = useToggle(false);
@@ -94,7 +94,7 @@ const CommentSortDropdown = ({ intl }) => {
</div> </div>
</> </>
); );
}; }
CommentSortDropdown.propTypes = { CommentSortDropdown.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -11,13 +11,13 @@ import { usePostComments } from '../data/hooks';
import messages from '../messages'; import messages from '../messages';
import { Comment, ResponseEditor } from './comment'; import { Comment, ResponseEditor } from './comment';
const CommentsView = ({ function CommentsView({
postType, postType,
postId, postId,
intl, intl,
endorsed, endorsed,
isClosed, isClosed,
}) => { }) {
const { const {
comments, comments,
hasMorePages, hasMorePages,
@@ -71,49 +71,51 @@ const CommentsView = ({
); );
return ( return (
((hasMorePages && isLoading) || !isLoading) && (
<> <>
{endorsedComments.length > 0 && ( {((hasMorePages && isLoading) || !isLoading) && (
<> <>
{handleDefinition(messages.endorsedResponseCount, endorsedComments.length)} {endorsedComments.length > 0 && (
{endorsed === EndorsementStatus.DISCUSSION <>
? handleComments(endorsedComments, true) {handleDefinition(messages.endorsedResponseCount, endorsedComments.length)}
: handleComments(endorsedComments, false)} {endorsed === EndorsementStatus.DISCUSSION
</> ? handleComments(endorsedComments, true)
)} : handleComments(endorsedComments, false)}
{endorsed !== EndorsementStatus.ENDORSED && ( </>
<>
{handleDefinition(messages.responseCount, unEndorsedComments.length)}
{unEndorsedComments.length === 0 && <br />}
{handleComments(unEndorsedComments, false)}
{(userCanAddThreadInBlackoutDate && !!unEndorsedComments.length && !isClosed) && (
<div className="mx-4">
{!addingResponse && (
<Button
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"
onClick={() => setAddingResponse(true)}
data-testid="add-response"
>
{intl.formatMessage(messages.addResponse)}
</Button>
)} )}
<ResponseEditor {endorsed !== EndorsementStatus.ENDORSED && (
postId={postId} <>
handleCloseEditor={() => setAddingResponse(false)} {handleDefinition(messages.responseCount, unEndorsedComments.length)}
addWrappingDiv {unEndorsedComments.length === 0 && <br />}
addingResponse={addingResponse} {handleComments(unEndorsedComments, false)}
/> {(userCanAddThreadInBlackoutDate && !!unEndorsedComments.length && !isClosed) && (
</div> <div className="mx-4">
)} {!addingResponse && (
</> <Button
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"
onClick={() => setAddingResponse(true)}
data-testid="add-response"
>
{intl.formatMessage(messages.addResponse)}
</Button>
)}
<ResponseEditor
postId={postId}
handleCloseEditor={() => setAddingResponse(false)}
addWrappingDiv
addingResponse={addingResponse}
/>
</div>
)}
</>
)}
</>
)} )}
</> </>
)
); );
}; }
CommentsView.propTypes = { CommentsView.propTypes = {
postId: PropTypes.string.isRequired, postId: PropTypes.string.isRequired,

View File

@@ -32,14 +32,14 @@ import CommentHeader from './CommentHeader';
import { commentShape } from './proptypes'; import { commentShape } from './proptypes';
import Reply from './Reply'; import Reply from './Reply';
const Comment = ({ function Comment({
postType, postType,
comment, comment,
showFullThread = true, showFullThread = true,
isClosedPost, isClosedPost,
intl, intl,
marginBottom, marginBottom,
}) => { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const hasChildren = comment.childCount > 0; const hasChildren = comment.childCount > 0;
const isNested = Boolean(comment.parentId); const isNested = Boolean(comment.parentId);
@@ -201,24 +201,26 @@ const Comment = ({
/> />
</div> </div>
) : ( ) : (
!isClosedPost && userCanAddThreadInBlackoutDate && (inlineReplies.length >= 5) <>
&& ( {!isClosedPost && userCanAddThreadInBlackoutDate && (inlineReplies.length >= 5)
<Button && (
className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500" <Button
variant="plain" className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500"
style={{ height: '36px' }} variant="plain"
onClick={() => setReplying(true)} style={{ height: '36px' }}
> onClick={() => setReplying(true)}
{intl.formatMessage(messages.addComment)} >
</Button> {intl.formatMessage(messages.addComment)}
) </Button>
)}
</>
) )
)} )}
</div> </div>
</div> </div>
</div> </div>
); );
}; }
Comment.propTypes = { Comment.propTypes = {
postType: PropTypes.oneOf(['discussion', 'question']).isRequired, postType: PropTypes.oneOf(['discussion', 'question']).isRequired,

View File

@@ -24,13 +24,13 @@ import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
import { addComment, editComment } from '../../data/thunks'; import { addComment, editComment } from '../../data/thunks';
import messages from '../../messages'; import messages from '../../messages';
const CommentEditor = ({ function CommentEditor({
intl, intl,
comment, comment,
onCloseEditor, onCloseEditor,
edit, edit,
formClasses, formClasses,
}) => { }) {
const editorRef = useRef(null); const editorRef = useRef(null);
const { authenticatedUser } = useContext(AppContext); const { authenticatedUser } = useContext(AppContext);
const { enableInContextSidebar } = useContext(DiscussionContext); const { enableInContextSidebar } = useContext(DiscussionContext);
@@ -57,10 +57,7 @@ const CommentEditor = ({
const initialValues = { const initialValues = {
comment: comment.rawBody, comment: comment.rawBody,
// eslint-disable-next-line react/prop-types editReasonCode: comment?.lastEdit?.reasonCode || (userIsStaff ? 'violates-guidelines' : ''),
editReasonCode: comment?.lastEdit?.reasonCode || (
userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined
),
}; };
const handleCloseEditor = (resetForm) => { const handleCloseEditor = (resetForm) => {
@@ -176,7 +173,7 @@ const CommentEditor = ({
)} )}
</Formik> </Formik>
); );
}; }
CommentEditor.propTypes = { CommentEditor.propTypes = {
comment: PropTypes.shape({ comment: PropTypes.shape({
@@ -185,7 +182,7 @@ CommentEditor.propTypes = {
parentId: PropTypes.string, parentId: PropTypes.string,
rawBody: PropTypes.string, rawBody: PropTypes.string,
author: PropTypes.string, author: PropTypes.string,
lastEdit: PropTypes.shape({}), lastEdit: PropTypes.object,
}).isRequired, }).isRequired,
onCloseEditor: PropTypes.func.isRequired, onCloseEditor: PropTypes.func.isRequired,
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -10,9 +10,9 @@ import { AuthorLabel } from '../../../common';
import { useAlertBannerVisible } from '../../../data/hooks'; import { useAlertBannerVisible } from '../../../data/hooks';
import { commentShape } from './proptypes'; import { commentShape } from './proptypes';
const CommentHeader = ({ function CommentHeader({
comment, comment,
}) => { }) {
const colorClass = AvatarOutlineAndLabelColors[comment.authorLabel]; const colorClass = AvatarOutlineAndLabelColors[comment.authorLabel];
const hasAnyAlert = useAlertBannerVisible(comment); const hasAnyAlert = useAlertBannerVisible(comment);
@@ -41,7 +41,7 @@ const CommentHeader = ({
</div> </div>
</div> </div>
); );
}; }
CommentHeader.propTypes = { CommentHeader.propTypes = {
comment: commentShape.isRequired, comment: commentShape.isRequired,

View File

@@ -19,11 +19,11 @@ import messages from '../../messages';
import CommentEditor from './CommentEditor'; import CommentEditor from './CommentEditor';
import { commentShape } from './proptypes'; import { commentShape } from './proptypes';
const Reply = ({ function Reply({
reply, reply,
postType, postType,
intl, intl,
}) => { }) {
timeago.register('time-locale', timeLocale); timeago.register('time-locale', timeLocale);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isEditing, setEditing] = useState(false); const [isEditing, setEditing] = useState(false);
@@ -143,7 +143,7 @@ const Reply = ({
</div> </div>
</div> </div>
); );
}; }
Reply.propTypes = { Reply.propTypes = {
postType: PropTypes.oneOf(['discussion', 'question']).isRequired, postType: PropTypes.oneOf(['discussion', 'question']).isRequired,
reply: commentShape.isRequired, reply: commentShape.isRequired,

View File

@@ -7,12 +7,12 @@ import { injectIntl } from '@edx/frontend-platform/i18n';
import CommentEditor from './CommentEditor'; import CommentEditor from './CommentEditor';
const ResponseEditor = ({ function ResponseEditor({
postId, postId,
addWrappingDiv, addWrappingDiv,
handleCloseEditor, handleCloseEditor,
addingResponse, addingResponse,
}) => { }) {
useEffect(() => { useEffect(() => {
handleCloseEditor(); handleCloseEditor();
}, [postId]); }, [postId]);
@@ -27,7 +27,7 @@ const ResponseEditor = ({
/> />
</div> </div>
); );
}; }
ResponseEditor.propTypes = { ResponseEditor.propTypes = {
postId: PropTypes.string.isRequired, postId: PropTypes.string.isRequired,

View File

@@ -20,13 +20,15 @@ export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussi
* @param enableInContextSidebar * @param enableInContextSidebar
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
export async function getThreadComments(threadId, { export async function getThreadComments(
endorsed, threadId, {
page, endorsed,
pageSize, page,
reverseOrder, pageSize,
enableInContextSidebar = false, reverseOrder,
} = {}) { enableInContextSidebar = false,
} = {},
) {
const params = snakeCaseObject({ const params = snakeCaseObject({
threadId, threadId,
endorsed: EndorsementValue[endorsed], endorsed: EndorsementValue[endorsed],
@@ -49,11 +51,13 @@ export async function getThreadComments(threadId, {
* @param {number=} pageSize * @param {number=} pageSize
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
export async function getCommentResponses(commentId, { export async function getCommentResponses(
page, commentId, {
pageSize, page,
reverseOrder, pageSize,
} = {}) { reverseOrder,
} = {},
) {
const url = `${getCommentsApiUrl()}${commentId}/`; const url = `${getCommentsApiUrl()}${commentId}/`;
const params = snakeCaseObject({ const params = snakeCaseObject({
page, page,

View File

@@ -141,7 +141,7 @@ const commentsSlice = createSlice({
const commentRemoveListType = !endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED; const commentRemoveListType = !endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED;
state.commentsInThreads[threadId][commentRemoveListType] = ( state.commentsInThreads[threadId][commentRemoveListType] = (
state.commentsInThreads[threadId]?.[commentRemoveListType]?.filter(item => item !== commentId) state.commentsInThreads[threadId]?.[commentRemoveListType]?.filter(item => item !== commentId)
); );
state.commentsInThreads[threadId][commentAddListtype] = [ state.commentsInThreads[threadId][commentAddListtype] = [
...state.commentsInThreads[threadId][commentAddListtype], payload.id, ...state.commentsInThreads[threadId][commentAddListtype], payload.id,

View File

@@ -7,7 +7,7 @@ import { selectAreThreadsFiltered } from '../data/selectors';
import { selectTopicFilter } from '../in-context-topics/data/selectors'; import { selectTopicFilter } from '../in-context-topics/data/selectors';
import messages from '../messages'; import messages from '../messages';
const NoResults = ({ intl }) => { function NoResults({ intl }) {
const postsFiltered = useSelector(selectAreThreadsFiltered); const postsFiltered = useSelector(selectAreThreadsFiltered);
const inContextTopicsFilter = useSelector(selectTopicFilter); const inContextTopicsFilter = useSelector(selectTopicFilter);
const topicsFilter = useSelector(({ topics }) => topics.filter); const topicsFilter = useSelector(({ topics }) => topics.filter);
@@ -37,7 +37,7 @@ const NoResults = ({ intl }) => {
<small className={textCssClasses}>{intl.formatMessage(helpMessage)}</small> <small className={textCssClasses}>{intl.formatMessage(helpMessage)}</small>
</div> </div>
); );
}; }
NoResults.propTypes = { NoResults.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -22,9 +22,9 @@ import { fetchThreads } from './data/thunks';
import NoResults from './NoResults'; import NoResults from './NoResults';
import { PostLink } from './post'; import { PostLink } from './post';
const PostsList = ({ function PostsList({
posts, topics, intl, isTopicTab, parentIsLoading, posts, topics, intl, isTopicTab, parentIsLoading,
}) => { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const {
courseId, courseId,
@@ -101,7 +101,7 @@ const PostsList = ({
)} )}
</> </>
); );
}; }
PostsList.propTypes = { PostsList.propTypes = {
posts: PropTypes.arrayOf(PropTypes.shape({ posts: PropTypes.arrayOf(PropTypes.shape({

View File

@@ -18,34 +18,34 @@ import { setSearchQuery } from './data/slices';
import PostFilterBar from './post-filter-bar/PostFilterBar'; import PostFilterBar from './post-filter-bar/PostFilterBar';
import PostsList from './PostsList'; import PostsList from './PostsList';
const AllPostsList = () => { function AllPostsList() {
const posts = useSelector(selectAllThreads); const posts = useSelector(selectAllThreads);
return <PostsList posts={posts} topics={null} />; return <PostsList posts={posts} topics={null} />;
}; }
const TopicPostsList = ({ topicId }) => { function TopicPostsList({ topicId }) {
const posts = useSelector(selectTopicThreads([topicId])); const posts = useSelector(selectTopicThreads([topicId]));
return <PostsList posts={posts} topics={[topicId]} isTopicTab />; return <PostsList posts={posts} topics={[topicId]} isTopicTab />;
}; }
TopicPostsList.propTypes = { TopicPostsList.propTypes = {
topicId: PropTypes.string.isRequired, topicId: PropTypes.string.isRequired,
}; };
const CategoryPostsList = ({ category }) => { function CategoryPostsList({ category }) {
const { enableInContextSidebar } = useContext(DiscussionContext); const { enableInContextSidebar } = useContext(DiscussionContext);
const groupedCategory = useSelector(selectCurrentCategoryGrouping)(category); const groupedCategory = useSelector(selectCurrentCategoryGrouping)(category);
// If grouping at subsection is enabled, only apply it when browsing discussions in context in the learning MFE. // If grouping at subsection is enabled, only apply it when browsing discussions in context in the learning MFE.
const topicIds = useSelector(selectTopicsUnderCategory)(enableInContextSidebar ? groupedCategory : category); const topicIds = useSelector(selectTopicsUnderCategory)(enableInContextSidebar ? groupedCategory : category);
const posts = useSelector(enableInContextSidebar ? selectAllThreads : selectTopicThreads(topicIds)); const posts = useSelector(enableInContextSidebar ? selectAllThreads : selectTopicThreads(topicIds));
return <PostsList posts={posts} topics={topicIds} />; return <PostsList posts={posts} topics={topicIds} />;
}; }
CategoryPostsList.propTypes = { CategoryPostsList.propTypes = {
category: PropTypes.string.isRequired, category: PropTypes.string.isRequired,
}; };
const PostsView = () => { function PostsView() {
const { const {
topicId, topicId,
category, category,
@@ -96,7 +96,7 @@ const PostsView = () => {
</div> </div>
</div> </div>
); );
}; }
PostsView.propTypes = { PostsView.propTypes = {
}; };

View File

@@ -28,20 +28,22 @@ export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussio
* @param {number} cohort * @param {number} cohort
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
export async function getThreads(courseId, { export async function getThreads(
topicIds, courseId, {
page, topicIds,
pageSize, page,
textSearch, pageSize,
orderBy, textSearch,
following, orderBy,
view, following,
author, view,
flagged, author,
threadType, flagged,
countFlagged, threadType,
cohort, countFlagged,
} = {}) { cohort,
} = {},
) {
const params = snakeCaseObject({ const params = snakeCaseObject({
courseId, courseId,
page, page,
@@ -149,6 +151,7 @@ export async function updateThread(threadId, {
pinned, pinned,
editReasonCode, editReasonCode,
closeReasonCode, closeReasonCode,
reviewStatus,
} = {}) { } = {}) {
const url = `${getThreadsApiUrl()}${threadId}/`; const url = `${getThreadsApiUrl()}${threadId}/`;
const patchData = snakeCaseObject({ const patchData = snakeCaseObject({
@@ -164,6 +167,7 @@ export async function updateThread(threadId, {
pinned, pinned,
editReasonCode, editReasonCode,
closeReasonCode, closeReasonCode,
reviewStatus,
}); });
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.patch(url, patchData, { headers: { 'Content-Type': 'application/merge-patch+json' } }); .patch(url, patchData, { headers: { 'Content-Type': 'application/merge-patch+json' } });

View File

@@ -239,6 +239,7 @@ export function createNewThread({
export function updateExistingThread(threadId, { export function updateExistingThread(threadId, {
flagged, voted, read, topicId, type, title, content, following, closed, pinned, closeReasonCode, editReasonCode, flagged, voted, read, topicId, type, title, content, following, closed, pinned, closeReasonCode, editReasonCode,
reviewStatus,
}) { }) {
return async (dispatch) => { return async (dispatch) => {
try { try {
@@ -256,6 +257,7 @@ export function updateExistingThread(threadId, {
pinned, pinned,
editReasonCode, editReasonCode,
closeReasonCode, closeReasonCode,
reviewStatus,
})); }));
const data = await updateThread(threadId, { const data = await updateThread(threadId, {
flagged, flagged,
@@ -270,6 +272,7 @@ export function updateExistingThread(threadId, {
pinned, pinned,
editReasonCode, editReasonCode,
closeReasonCode, closeReasonCode,
reviewStatus,
}); });
dispatch(updateThreadSuccess(camelCaseObject(data))); dispatch(updateThreadSuccess(camelCaseObject(data)));
} catch (error) { } catch (error) {

View File

@@ -2,6 +2,5 @@
export { showPostEditor } from './data'; export { showPostEditor } from './data';
export { default as Post } from './post/Post'; export { default as Post } from './post/Post';
export { default as messages } from './post-actions-bar/messages'; export { default as messages } from './post-actions-bar/messages';
// eslint-disable-next-line import/no-cycle
export { default as PostEditor } from './post-editor/PostEditor'; export { default as PostEditor } from './post-editor/PostEditor';
export { default as PostsView } from './PostsView'; export { default as PostsView } from './PostsView';

View File

@@ -21,9 +21,9 @@ import messages from './messages';
import './actionBar.scss'; import './actionBar.scss';
const PostActionsBar = ({ function PostActionsBar({
intl, intl,
}) => { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const loadingStatus = useSelector(selectconfigLoadingStatus); const loadingStatus = useSelector(selectconfigLoadingStatus);
const enableInContext = useSelector(selectEnableInContext); const enableInContext = useSelector(selectEnableInContext);
@@ -51,10 +51,8 @@ const PostActionsBar = ({
{!enableInContextSidebar && <div className="border-right border-light-400 mx-3" />} {!enableInContextSidebar && <div className="border-right border-light-400 mx-3" />}
<Button <Button
variant={enableInContextSidebar ? 'plain' : 'brand'} variant={enableInContextSidebar ? 'plain' : 'brand'}
className={classNames( className={classNames('my-0 font-style border-0 line-height-24',
'my-0 font-style border-0 line-height-24', { 'px-3 py-10px border-0': enableInContextSidebar })}
{ 'px-3 py-10px border-0': enableInContextSidebar },
)}
onClick={() => dispatch(showPostEditor())} onClick={() => dispatch(showPostEditor())}
size={enableInContextSidebar ? 'md' : 'sm'} size={enableInContextSidebar ? 'md' : 'sm'}
> >
@@ -79,7 +77,7 @@ const PostActionsBar = ({
)} )}
</div> </div>
); );
}; }
PostActionsBar.propTypes = { PostActionsBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -34,7 +34,6 @@ import {
selectUserIsGroupTa, selectUserIsGroupTa,
selectUserIsStaff, selectUserIsStaff,
} from '../../data/selectors'; } from '../../data/selectors';
// eslint-disable-next-line import/no-cycle
import { EmptyPage } from '../../empty-posts'; import { EmptyPage } from '../../empty-posts';
import { import {
selectArchivedTopics, selectArchivedTopics,
@@ -51,12 +50,12 @@ import { selectThread } from '../data/selectors';
import { createNewThread, fetchThread, updateExistingThread } from '../data/thunks'; import { createNewThread, fetchThread, updateExistingThread } from '../data/thunks';
import messages from './messages'; import messages from './messages';
const DiscussionPostType = ({ function DiscussionPostType({
value, value,
type, type,
selected, selected,
icon, icon,
}) => { }) {
const { enableInContextSidebar } = useContext(DiscussionContext); const { enableInContextSidebar } = useContext(DiscussionContext);
// Need to use regular label since Form.Label doesn't support overriding htmlFor // Need to use regular label since Form.Label doesn't support overriding htmlFor
return ( return (
@@ -76,7 +75,7 @@ const DiscussionPostType = ({
</Card> </Card>
</label> </label>
); );
}; }
DiscussionPostType.propTypes = { DiscussionPostType.propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
@@ -85,9 +84,9 @@ DiscussionPostType.propTypes = {
icon: PropTypes.element.isRequired, icon: PropTypes.element.isRequired,
}; };
const PostEditor = ({ function PostEditor({
editExisting, editExisting,
}) => { }) {
const intl = useIntl(); const intl = useIntl();
const { authenticatedUser } = useContext(AppContext); const { authenticatedUser } = useContext(AppContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -501,7 +500,7 @@ const PostEditor = ({
} }
</Formik> </Formik>
); );
}; }
PostEditor.propTypes = { PostEditor.propTypes = {
editExisting: PropTypes.bool, editExisting: PropTypes.bool,

View File

@@ -61,9 +61,9 @@ ActionItem.propTypes = {
selected: PropTypes.string.isRequired, selected: PropTypes.string.isRequired,
}; };
const PostFilterBar = ({ function PostFilterBar({
intl, intl,
}) => { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { courseId } = useParams(); const { courseId } = useParams();
const { page } = useContext(DiscussionContext); const { page } = useContext(DiscussionContext);
@@ -75,11 +75,9 @@ const PostFilterBar = ({
const cohorts = useSelector(selectCourseCohorts); const cohorts = useSelector(selectCourseCohorts);
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const selectedCohort = useMemo( const selectedCohort = useMemo(() => cohorts.find(cohort => (
() => cohorts.find(cohort => ( toString(cohort.id) === currentFilters.cohort)),
toString(cohort.id) === currentFilters.cohort)), [currentFilters.cohort]);
[currentFilters.cohort],
);
const handleSortFilterChange = (event) => { const handleSortFilterChange = (event) => {
const currentType = currentFilters.postType; const currentType = currentFilters.postType;
@@ -301,7 +299,7 @@ const PostFilterBar = ({
</Collapsible.Body> </Collapsible.Body>
</Collapsible.Advanced> </Collapsible.Advanced>
); );
}; }
PostFilterBar.propTypes = { PostFilterBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -14,12 +14,12 @@ import {
import { selectModerationSettings } from '../../data/selectors'; import { selectModerationSettings } from '../../data/selectors';
import messages from './messages'; import messages from './messages';
const ClosePostReasonModal = ({ function ClosePostReasonModal({
intl, intl,
isOpen, isOpen,
onCancel, onCancel,
onConfirm, onConfirm,
}) => { }) {
const scrollTo = useRef(null); const scrollTo = useRef(null);
const [reasonCode, setReasonCode] = useState(null); const [reasonCode, setReasonCode] = useState(null);
@@ -87,7 +87,7 @@ const ClosePostReasonModal = ({
</ModalDialog.Footer> </ModalDialog.Footer>
</ModalDialog> </ModalDialog>
); );
}; }
ClosePostReasonModal.propTypes = { ClosePostReasonModal.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -9,12 +9,12 @@ import {
import { ThumbUpFilled, ThumbUpOutline } from '../../../components/icons'; import { ThumbUpFilled, ThumbUpOutline } from '../../../components/icons';
import messages from './messages'; import messages from './messages';
const LikeButton = ({ function LikeButton({
count, count,
intl, intl,
onClick, onClick,
voted, voted,
}) => { }) {
const handleClick = (e) => { const handleClick = (e) => {
e.preventDefault(); e.preventDefault();
if (onClick) { if (onClick) {
@@ -47,7 +47,7 @@ const LikeButton = ({
</div> </div>
); );
}; }
LikeButton.propTypes = { LikeButton.propTypes = {
count: PropTypes.number.isRequired, count: PropTypes.number.isRequired,

View File

@@ -24,11 +24,11 @@ import PostFooter from './PostFooter';
import PostHeader from './PostHeader'; import PostHeader from './PostHeader';
import { postShape } from './proptypes'; import { postShape } from './proptypes';
const Post = ({ function Post({
post, post,
intl, intl,
handleAddResponseButton, handleAddResponseButton,
}) => { }) {
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -44,6 +44,7 @@ const Post = ({
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const displayPostFooter = post.following || post.voteCount || post.closed const displayPostFooter = post.following || post.voteCount || post.closed
|| (post.groupId && userHasModerationPrivileges); || (post.groupId && userHasModerationPrivileges);
const displayReviewContentLabel = post.reviewStatus === "PENDING";
const handleAbusedFlag = useCallback(() => { const handleAbusedFlag = useCallback(() => {
if (post.abuseFlagged) { if (post.abuseFlagged) {
@@ -67,11 +68,6 @@ const Post = ({
hideReportConfirmation(); hideReportConfirmation();
}; };
const handlePostCopyLink = useCallback(() => {
const postURL = new URL(`${getConfig().PUBLIC_PATH}${courseId}/posts/${post.id}`, window.location.origin);
navigator.clipboard.writeText(postURL.href);
}, [window.location.origin, post.id, courseId]);
const actionHandlers = useMemo(() => ({ const actionHandlers = useMemo(() => ({
[ContentActions.EDIT_CONTENT]: () => history.push({ [ContentActions.EDIT_CONTENT]: () => history.push({
...location, ...location,
@@ -87,9 +83,11 @@ const Post = ({
dispatch(updateExistingThread(post.id, { closed: true })); dispatch(updateExistingThread(post.id, { closed: true }));
} }
}, },
[ContentActions.COPY_LINK]: handlePostCopyLink, [ContentActions.COPY_LINK]: () => { navigator.clipboard.writeText(`${window.location.origin}/${courseId}/posts/${post.id}`); },
[ContentActions.PIN]: () => dispatch(updateExistingThread(post.id, { pinned: !post.pinned })), [ContentActions.PIN]: () => dispatch(updateExistingThread(post.id, { pinned: !post.pinned })),
[ContentActions.REPORT]: () => handleAbusedFlag(), [ContentActions.REPORT]: () => handleAbusedFlag(),
[ContentActions.ACCEPT_REVIEW]: () => dispatch(updateExistingThread(post.id, { reviewStatus: "ACCEPTED" })),
[ContentActions.REJECT_REVIEW]: () => dispatch(updateExistingThread(post.id, { reviewStatus: "REJECTED" })),
}), [ }), [
showDeleteConfirmation, showDeleteConfirmation,
history, history,
@@ -149,15 +147,15 @@ const Post = ({
/> />
<AlertBanner content={post} /> <AlertBanner content={post} />
<PostHeader post={post} /> <PostHeader post={post} />
{displayReviewContentLabel && <p style= {{ background: "yellow" }}> This content is under review </p>}
<div className="d-flex mt-14px text-break font-style text-primary-500"> <div className="d-flex mt-14px text-break font-style text-primary-500">
<HTMLLoader htmlNode={post.renderedBody} componentId="post" cssClassName="html-loader" testId={post.id} /> <HTMLLoader htmlNode={post.renderedBody} componentId="post" cssClassName="html-loader" testId={post.id} />
</div> </div>
{(topicContext || topic) && ( {(topicContext || topic) && (
<div <div
className={classNames( className={classNames('mt-14px font-style font-size-12',
'mt-14px font-style font-size-12', { 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter })}
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter },
)}
style={{ lineHeight: '20px' }} style={{ lineHeight: '20px' }}
> >
<span className="text-gray-500" style={{ lineHeight: '20px' }}>{intl.formatMessage(messages.relatedTo)}{' '}</span> <span className="text-gray-500" style={{ lineHeight: '20px' }}>{intl.formatMessage(messages.relatedTo)}{' '}</span>
@@ -190,7 +188,7 @@ const Post = ({
/> />
</div> </div>
); );
}; }
Post.propTypes = { Post.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -15,11 +15,11 @@ import LikeButton from './LikeButton';
import messages from './messages'; import messages from './messages';
import { postShape } from './proptypes'; import { postShape } from './proptypes';
const PostFooter = ({ function PostFooter({
intl, intl,
post, post,
userHasModerationPrivileges, userHasModerationPrivileges,
}) => { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
return ( return (
<div className="d-flex align-items-center ml-n1.5 mt-10px" style={{ height: '32px' }} data-testid="post-footer"> <div className="d-flex align-items-center ml-n1.5 mt-10px" style={{ height: '32px' }} data-testid="post-footer">
@@ -95,7 +95,7 @@ const PostFooter = ({
</div> </div>
</div> </div>
); );
}; }
PostFooter.propTypes = { PostFooter.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

Some files were not shown because too many files have changed in this diff Show More