Compare commits

..

3 Commits

111 changed files with 27802 additions and 5238 deletions

View File

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

View File

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

View File

@@ -10,4 +10,4 @@ on:
jobs:
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": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "12.0.0",
"@edx/frontend-component-header": "4.0.3",
"@edx/frontend-platform": "4.4.0",
"@edx/frontend-component-footer": "11.2.0",
"@edx/frontend-component-header": "3.2.0",
"@edx/frontend-platform": "2.6.1",
"@edx/paragon": "20.15.0",
"@reduxjs/toolkit": "1.8.0",
"@tinymce/tinymce-react": "3.13.1",
@@ -61,7 +61,7 @@
},
"devDependencies": {
"@edx/browserslist-config": "1.1.0",
"@edx/frontend-build": "12.8.38",
"@edx/frontend-build": "11.0.1",
"@edx/reactifex": "1.0.3",
"@testing-library/jest-dom": "5.16.2",
"@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 { ActionItem } from '../discussions/posts/post-filter-bar/PostFilterBar';
const FilterBar = ({
function FilterBar({
intl,
filters,
selectedFilters,
onFilterChange,
showCohortsFilter,
}) => {
}) {
const [isOpen, setOpen] = useState(false);
const cohorts = useSelector(selectCourseCohorts);
const { status } = useSelector(state => state.cohorts);
const selectedCohort = useMemo(
() => cohorts.find(cohort => (
toString(cohort.id) === selectedFilters.cohort)),
[selectedFilters.cohort],
);
const selectedCohort = useMemo(() => cohorts.find(cohort => (
toString(cohort.id) === selectedFilters.cohort)),
[selectedFilters.cohort]);
const allFilters = [
{
@@ -185,7 +183,7 @@ const FilterBar = ({
</Collapsible.Body>
</Collapsible.Advanced>
);
};
}
FilterBar.propTypes = {
intl: intlShape.isRequired,

View File

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

View File

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

View File

@@ -12,9 +12,9 @@ import messages from './messages';
import './navBar.scss';
const CourseTabsNavigation = ({
function CourseTabsNavigation({
activeTab, className, intl, courseId, rootSlug,
}) => {
}) {
const dispatch = useDispatch();
const tabs = useSelector(state => state.courseTabs.tabs);
@@ -45,7 +45,7 @@ const CourseTabsNavigation = ({
</div>
</div>
);
};
}
CourseTabsNavigation.propTypes = {
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 () => {
axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(
200,
(Factory.build('navigationBar', 1, { hasCourseAccess: false })),
);
axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200,
(Factory.build('navigationBar', 1, { hasCourseAccess: false })));
await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState);
expect(store.getState().courseTabs.courseStatus).toEqual('denied');

View File

@@ -8,7 +8,7 @@ import { Dropdown } from '@edx/paragon';
import useIndexOfLastVisibleChild from './useIndexOfLastVisibleChild';
const Tabs = ({ children, className, ...attrs }) => {
export default function Tabs({ children, className, ...attrs }) {
const [
indexOfLastVisibleChild,
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
// it so it can be part of measurements)
wrappedChildren.splice(
indexOfOverflowStart,
0, (
<div
className="nav-item flex-shrink-0"
style={indexOfOverflowStart >= React.Children.count(children) ? invisibleStyle : null}
ref={overflowElementRef}
key="overflow"
>
<Dropdown className="h-100">
<Dropdown.Toggle variant="link" className="nav-link h-100" id="learn.course.tabs.navigation.overflow.menu">
<FormattedMessage
id="learn.course.tabs.navigation.overflow.menu"
description="The title of the overflow menu for course tabs"
defaultMessage="More..."
/>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">{overflowChildren}</Dropdown.Menu>
</Dropdown>
</div>
),
);
wrappedChildren.splice(indexOfOverflowStart, 0, (
<div
className="nav-item flex-shrink-0"
style={indexOfOverflowStart >= React.Children.count(children) ? invisibleStyle : null}
ref={overflowElementRef}
key="overflow"
>
<Dropdown className="h-100">
<Dropdown.Toggle variant="link" className="nav-link h-100" id="learn.course.tabs.navigation.overflow.menu">
<FormattedMessage
id="learn.course.tabs.navigation.overflow.menu"
description="The title of the overflow menu for course tabs"
defaultMessage="More..."
/>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">{overflowChildren}</Dropdown.Menu>
</Dropdown>
</div>
));
return wrappedChildren;
}, [children, indexOfLastVisibleChild]);
@@ -65,7 +62,7 @@ const Tabs = ({ children, className, ...attrs }) => {
{tabChildren}
</nav>
);
};
}
Tabs.propTypes = {
children: PropTypes.node,
@@ -76,5 +73,3 @@ Tabs.defaultProps = {
children: null,
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 HTMLLoader from './HTMLLoader';
const PostPreviewPanel = ({
function PostPreviewPanel({
htmlNode, intl, isPost, editExisting,
}) => {
}) {
const [showPreviewPane, setShowPreviewPane] = useState(false);
return (
@@ -55,7 +55,7 @@ const PostPreviewPanel = ({
</div>
</>
);
};
}
PostPreviewPanel.propTypes = {
intl: intlShape.isRequired,

View File

@@ -13,7 +13,7 @@ import { setSearchQuery } from '../discussions/posts/data';
import postsMessages from '../discussions/posts/post-actions-bar/messages';
import { setFilter as setTopicFilter } from '../discussions/topics/data/slices';
const Search = ({ intl }) => {
function Search({ intl }) {
const dispatch = useDispatch();
const { page } = useContext(DiscussionContext);
const postSearch = useSelector(({ threads }) => threads.filters.search);
@@ -56,27 +56,29 @@ const Search = ({ intl }) => {
useEffect(() => onClear(), [page]);
return (
<SearchField.Advanced
onClear={onClear}
onChange={onChange}
onSubmit={onSubmit}
value={currentValue}
>
<SearchField.Label />
<SearchField.Input
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"
<>
<SearchField.Advanced
onClear={onClear}
onChange={onChange}
onSubmit={onSubmit}
value={currentValue}
>
<SearchField.Label />
<SearchField.Input
style={{ paddingRight: '1rem' }}
placeholder={intl.formatMessage(postsMessages.search, { page: camelCase(page) })}
/>
</span>
</SearchField.Advanced>
<span className="mt-auto mb-auto mr-2.5 pointer-cursor-hover">
<Icon
src={SearchIcon}
onClick={() => onSubmit(searchValue)}
data-testid="search-icon"
/>
</span>
</SearchField.Advanced>
</>
);
};
}
Search.propTypes = {
intl: intlShape.isRequired,

View File

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

View File

@@ -58,7 +58,7 @@ const setup = (editor) => {
};
/* istanbul ignore next */
const TinyMCEEditor = (props) => {
export default function TinyMCEEditor(props) {
// note that skin and content_css is disabled to avoid the normal
// 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';
import messages from '../discussions/in-context-topics/messages';
const TopicStats = ({
function TopicStats({
threadCounts,
activeFlags,
inactiveFlags,
intl,
}) => {
}) {
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa);
@@ -87,7 +87,7 @@ const TopicStats = ({
)}
</div>
);
};
}
TopicStats.propTypes = {
threadCounts: PropTypes.shape({

View File

@@ -1,20 +1,20 @@
import React from 'react';
const InsertLink = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="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>
);
export default InsertLink;
export default function InsertLink() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="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>
);
}

View File

@@ -1,26 +1,26 @@
import React from 'react';
const Issue = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
fill="none"
viewBox="0 0 28 28"
>
<path
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"
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"
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>
);
export default Issue;
export default function Issue() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
fill="none"
viewBox="0 0 28 28"
>
<path
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"
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"
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>
);
}

View File

@@ -1,18 +1,18 @@
import React from 'react';
const People = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="none"
viewBox="0 0 16 16"
>
<path
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>
);
export default People;
export default function People() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="none"
viewBox="0 0 16 16"
>
<path
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>
);
}

View File

@@ -1,20 +1,20 @@
import React from 'react';
const PushPin = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="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>
);
export default PushPin;
export default function PushPin() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
fillRule="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>
);
}

View File

@@ -1,26 +1,26 @@
import React from 'react';
const Question = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
fill="none"
viewBox="0 0 28 28"
>
<path
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"
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"
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>
);
export default Question;
export default function Question() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
fill="none"
viewBox="0 0 28 28"
>
<path
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"
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"
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>
);
}

View File

@@ -1,18 +1,18 @@
import React from 'react';
const QuestionAnswer = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="20"
fill="none"
viewBox="0 0 21 20"
>
<path
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>
);
export default QuestionAnswer;
export default function QuestionAnswer() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="20"
fill="none"
viewBox="0 0 21 20"
>
<path
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>
);
}

View File

@@ -1,18 +1,18 @@
import React from 'react';
const QuestionAnswerOutline = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
viewBox="0 0 20 20"
>
<path
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>
);
export default QuestionAnswerOutline;
export default function QuestionAnswerOutline() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
viewBox="0 0 20 20"
>
<path
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>
);
}

View File

@@ -1,18 +1,18 @@
import React from 'react';
const StarFilled = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="20"
fill="none"
viewBox="0 0 21 20"
>
<path
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>
);
export default StarFilled;
export default function StarFilled() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="20"
fill="none"
viewBox="0 0 21 20"
>
<path
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>
);
}

View File

@@ -1,18 +1,18 @@
import React from 'react';
const StarOutline = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
viewBox="0 0 20 20"
>
<path
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>
);
export default StarOutline;
export default function StarOutline() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
viewBox="0 0 20 20"
>
<path
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>
);
}

View File

@@ -1,18 +1,18 @@
import React from 'react';
const ThumbUpFilled = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="20"
fill="none"
viewBox="0 0 21 20"
>
<path
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>
);
export default ThumbUpFilled;
export default function ThumbUpFilled() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="20"
fill="none"
viewBox="0 0 21 20"
>
<path
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>
);
}

View File

@@ -1,21 +1,21 @@
import React from 'react';
const ThumbUpOutline = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
viewBox="0 0 20 20"
>
<path
fill="currentColor"
fillRule="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>
);
export default ThumbUpOutline;
export default function ThumbUpOutline() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
viewBox="0 0 20 20"
>
<path
fill="currentColor"
fillRule="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>
);
}

View File

@@ -55,6 +55,8 @@ export const ContentActions = {
CHANGE_TOPIC: 'topic_id',
CHANGE_TYPE: 'type',
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 { inBlackoutDateRange, useActions } from '../utils';
const ActionsDropdown = ({
function ActionsDropdown({
intl,
commentOrPost,
disabled,
actionHandlers,
iconSize,
dropDownIconSize,
}) => {
}) {
const buttonRef = useRef();
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);
@@ -108,7 +108,7 @@ const ActionsDropdown = ({
</div>
</>
);
};
}
ActionsDropdown.propTypes = {
intl: intlShape.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -172,8 +172,7 @@ describe('DiscussionsHome', () => {
it.each([
{ searchByEndPoint: 'category/section-topic-1' },
{ 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 }) => {
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
enableInContext: true, provider: 'openedx', hasModerationPrivileges: true,
@@ -194,8 +193,7 @@ describe('DiscussionsHome', () => {
await renderComponent(`/${courseId}/${searchByEndPoint}`);
expect(screen.queryByText('No topic selected')).toBeInTheDocument();
},
);
});
it('should display empty page message for empty learners list', async () => {
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 messages from '../messages';
const InformationBanner = ({
function InformationBanner({
intl,
}) => {
}) {
const [showBanner, setShowBanner] = useState(true);
const userRoles = useSelector(selectUserRoles);
const isAdmin = useSelector(selectUserIsStaff);
@@ -55,7 +55,7 @@ const InformationBanner = ({
</div>
</PageBanner>
);
};
}
InformationBanner.propTypes = {
intl: intlShape.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -206,8 +206,7 @@ describe('InContext Topic Posts View', () => {
test.each([
{ searchText: 'hello world', output: 'Showing 0 results for', resultCount: 0 },
{ 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 }) => {
await setupTopicsMockResponse();
await renderComponent();
@@ -227,8 +226,7 @@ describe('InContext Topic Posts View', () => {
expect(clearButton).toBeInTheDocument();
expect(units).toHaveLength(resultCount);
});
},
);
});
it('When click on the clear button it should move to main topics pages.', async () => {
await setupTopicsMockResponse();
@@ -255,8 +253,7 @@ describe('InContext Topic Posts View', () => {
});
});
it(
'should display Nothing here yet and No topic exists message when topics and selectedSubsectionUnits are empty',
it('should display Nothing here yet and No topic exists message when topics and selectedSubsectionUnits are empty',
async () => {
await setupTopicsMockResponse(0, 0, 0);
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());
expect(within(container).queryByText('No topic exists')).toBeInTheDocument();
expect(within(container).queryByText('Unnamed Topic')).toBeInTheDocument();
},
);
});
it('should display all topics when search by an empty search string', async () => {
await setupTopicsMockResponse();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,33 +6,35 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from '../messages';
import Topic, { topicShape } from './Topic';
const ArchivedBaseGroup = ({
function ArchivedBaseGroup({
archivedTopics,
showDivider,
intl,
}) => (
<>
{showDivider && (
}) {
return (
<>
<div className="divider border-top border-light-500" />
<div className="divider pt-1 bg-light-300" />
{showDivider && (
<>
<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 = {
archivedTopics: PropTypes.arrayOf(topicShape).isRequired,

View File

@@ -13,13 +13,13 @@ import { discussionsPath } from '../../utils';
import messages from '../messages';
import { topicShape } from './Topic';
const SectionBaseGroup = ({
function SectionBaseGroup({
section,
sectionTitle,
sectionId,
showDivider,
intl,
}) => {
}) {
const { courseId } = useParams();
const isSelected = (id) => window.location.pathname.includes(id);
const sectionUrl = (id) => discussionsPath(Routes.TOPICS.CATEGORY, {
@@ -70,7 +70,7 @@ const SectionBaseGroup = ({
)}
</div>
);
};
}
SectionBaseGroup.propTypes = {
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 PropTypes from 'prop-types';
@@ -17,12 +17,12 @@ import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../da
import { discussionsPath } from '../../utils';
import messages from '../messages';
const Topic = ({
function Topic({
topic,
showDivider,
index,
intl,
}) => {
}) {
const { courseId } = useParams();
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
@@ -70,7 +70,7 @@ const Topic = ({
)}
</>
);
};
}
export const topicShape = PropTypes.shape({
id: PropTypes.string,

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,8 +29,7 @@ describe('Learner api test cases', () => {
axiosMock.reset();
});
it(
'Successfully get and store API response for the learner\'s list and learners posts in redux',
it('Successfully get and store API response for the learner\'s list and learners posts in redux',
async () => {
const learners = await setupLearnerMockResponse();
const threads = await setupPostsMockResponse();
@@ -39,23 +38,20 @@ describe('Learner api test cases', () => {
expect(Object.values(learners.learnerProfiles)).toHaveLength(3);
expect(threads.status).toEqual('successful');
expect(Object.values(threads.threadsById)).toHaveLength(2);
},
);
});
it.each([
{ status: 'statusUnread', search: 'Title', cohort: 'post' },
{ status: 'statusUnanswered', search: 'Title', cohort: 'post' },
{ status: 'statusReported', 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 }) => {
const threads = await setupPostsMockResponse({ filters: { status, search, cohort } });
expect(threads.status).toEqual('successful');
expect(Object.values(threads.threadsById)).toHaveLength(2);
},
);
});
it('Failed to fetch learners', async () => {
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 () => {
executeThunk(
fetchLearners('course-v1:edX+DemoX+Demo_Course', { usernameSearch: 'learner-1' }),
store.dispatch,
store.getState,
store.dispatch, store.getState,
);
const { learners } = store.getState();
@@ -56,8 +55,7 @@ describe('Learner redux test cases', () => {
expect(learners.postFilter.cohort).toEqual('');
});
test(
'Successfully store a learner posts stats data as pages object in redux',
test('Successfully store a learner posts stats data as pages object in redux',
async () => {
const learners = await setupLearnerMockResponse();
const page = learners.pages[0];
@@ -67,11 +65,9 @@ describe('Learner redux test cases', () => {
expect(statsObject.responses).toEqual(3);
expect(statsObject.threads).toEqual(1);
expect(statsObject.replies).toEqual(0);
},
);
});
test(
'Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux',
test('Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux',
async () => {
const learners = await setupLearnerMockResponse();
@@ -79,8 +75,7 @@ describe('Learner redux test cases', () => {
expect(learners.totalPages).toEqual(2);
expect(learners.totalLearners).toEqual(3);
expect(learners.sortedBy).toEqual('activity');
},
);
});
test('Successfully updated the learner\'s sort data in redux', async () => {
const learners = await setupLearnerMockResponse();
@@ -111,8 +106,7 @@ describe('Learner redux test cases', () => {
expect(updatedLearners.pages).toHaveLength(0);
});
test(
'Successfully update the learner\'s search query in redux when searching for a learner',
test('Successfully update the learner\'s search query in redux when searching for a learner',
async () => {
const learners = await setupLearnerMockResponse();
@@ -122,6 +116,5 @@ describe('Learner redux test cases', () => {
const updatedLearners = store.getState().learners;
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 { setPostFilter } from '../data/slices';
const LearnerPostFilterBar = () => {
function LearnerPostFilterBar() {
const dispatch = useDispatch();
const { courseId } = useParams();
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
@@ -98,6 +98,6 @@ const LearnerPostFilterBar = () => {
showCohortsFilter={userHasModerationPrivileges || userIsGroupTa}
/>
);
};
}
export default LearnerPostFilterBar;

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,16 @@ const messages = defineMessages({
defaultMessage: 'Unpin',
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: {
id: 'discussions.actions.delete',
defaultMessage: 'Delete',

View File

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

View File

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

View File

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

View File

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

View File

@@ -384,16 +384,12 @@ describe('ThreadView', () => {
expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument();
expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
await act(async () => {
fireEvent.change(
screen.queryByRole('combobox', { name: /reason for editing/i }),
{ target: { value: null } },
);
fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }),
{ target: { value: null } });
});
await act(async () => {
fireEvent.change(screen.queryByRole(
'combobox',
{ name: /reason for editing/i },
), { target: { value: 'reason-1' } });
fireEvent.change(screen.queryByRole('combobox',
{ name: /reason for editing/i }), { target: { value: 'reason-1' } });
});
await act(async () => {
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 messages from '../messages';
const CommentSortDropdown = ({ intl }) => {
function CommentSortDropdown({ intl }) {
const dispatch = useDispatch();
const sortedOrder = useSelector(selectCommentSortOrder);
const [isOpen, open, close] = useToggle(false);
@@ -94,7 +94,7 @@ const CommentSortDropdown = ({ intl }) => {
</div>
</>
);
};
}
CommentSortDropdown.propTypes = {
intl: intlShape.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -141,7 +141,7 @@ const commentsSlice = createSlice({
const commentRemoveListType = !endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED;
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], payload.id,

View File

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

View File

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

View File

@@ -18,34 +18,34 @@ import { setSearchQuery } from './data/slices';
import PostFilterBar from './post-filter-bar/PostFilterBar';
import PostsList from './PostsList';
const AllPostsList = () => {
function AllPostsList() {
const posts = useSelector(selectAllThreads);
return <PostsList posts={posts} topics={null} />;
};
}
const TopicPostsList = ({ topicId }) => {
function TopicPostsList({ topicId }) {
const posts = useSelector(selectTopicThreads([topicId]));
return <PostsList posts={posts} topics={[topicId]} isTopicTab />;
};
}
TopicPostsList.propTypes = {
topicId: PropTypes.string.isRequired,
};
const CategoryPostsList = ({ category }) => {
function CategoryPostsList({ category }) {
const { enableInContextSidebar } = useContext(DiscussionContext);
const groupedCategory = useSelector(selectCurrentCategoryGrouping)(category);
// 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 posts = useSelector(enableInContextSidebar ? selectAllThreads : selectTopicThreads(topicIds));
return <PostsList posts={posts} topics={topicIds} />;
};
}
CategoryPostsList.propTypes = {
category: PropTypes.string.isRequired,
};
const PostsView = () => {
function PostsView() {
const {
topicId,
category,
@@ -96,7 +96,7 @@ const PostsView = () => {
</div>
</div>
);
};
}
PostsView.propTypes = {
};

View File

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

View File

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

View File

@@ -2,6 +2,5 @@
export { showPostEditor } from './data';
export { default as Post } from './post/Post';
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 PostsView } from './PostsView';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,11 +24,11 @@ import PostFooter from './PostFooter';
import PostHeader from './PostHeader';
import { postShape } from './proptypes';
const Post = ({
function Post({
post,
intl,
handleAddResponseButton,
}) => {
}) {
const location = useLocation();
const history = useHistory();
const dispatch = useDispatch();
@@ -44,6 +44,7 @@ const Post = ({
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const displayPostFooter = post.following || post.voteCount || post.closed
|| (post.groupId && userHasModerationPrivileges);
const displayReviewContentLabel = post.reviewStatus === "PENDING";
const handleAbusedFlag = useCallback(() => {
if (post.abuseFlagged) {
@@ -67,11 +68,6 @@ const Post = ({
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(() => ({
[ContentActions.EDIT_CONTENT]: () => history.push({
...location,
@@ -87,9 +83,11 @@ const Post = ({
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.REPORT]: () => handleAbusedFlag(),
[ContentActions.ACCEPT_REVIEW]: () => dispatch(updateExistingThread(post.id, { reviewStatus: "ACCEPTED" })),
[ContentActions.REJECT_REVIEW]: () => dispatch(updateExistingThread(post.id, { reviewStatus: "REJECTED" })),
}), [
showDeleteConfirmation,
history,
@@ -149,15 +147,15 @@ const Post = ({
/>
<AlertBanner content={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">
<HTMLLoader htmlNode={post.renderedBody} componentId="post" cssClassName="html-loader" testId={post.id} />
</div>
{(topicContext || topic) && (
<div
className={classNames(
'mt-14px font-style font-size-12',
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter },
)}
className={classNames('mt-14px font-style font-size-12',
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter })}
style={{ lineHeight: '20px' }}
>
<span className="text-gray-500" style={{ lineHeight: '20px' }}>{intl.formatMessage(messages.relatedTo)}{' '}</span>
@@ -190,7 +188,7 @@ const Post = ({
/>
</div>
);
};
}
Post.propTypes = {
intl: intlShape.isRequired,

View File

@@ -15,11 +15,11 @@ import LikeButton from './LikeButton';
import messages from './messages';
import { postShape } from './proptypes';
const PostFooter = ({
function PostFooter({
intl,
post,
userHasModerationPrivileges,
}) => {
}) {
const dispatch = useDispatch();
return (
<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>
);
};
}
PostFooter.propTypes = {
intl: intlShape.isRequired,

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