Compare commits

...

5 Commits

Author SHA1 Message Date
edX requirements bot
5ca61b9480 Merge pull request #535 from DmytroAlipov/fix-edit-reason-palm
fix: 400 error editing comment
2023-06-12 06:05:04 -04:00
alipov_d
e801fbb5cd fix: 400 error editing comment
This is a backport from the master
2023-06-09 12:38:27 +02:00
Bilal Qamar
5c6e40bc48 feat: upgraded to node v18, added .nvmrc and updated workflows (#471)
* feat: upgraded to node v18, added .nvmrc and updated workflows

* refactor: updated packages

* refactor: resolved eslint issues
2023-06-09 09:12:08 +02:00
Eugene Dyudyunov
0e2bd9480d fix: post sharing URL (#519) 2023-05-31 13:57:50 +05:00
Sagirov Eugeniy
35541c9268 chore: update frontend-platform version to v4.2.0 2023-05-02 17:17:14 -03:00
107 changed files with 5231 additions and 27758 deletions

View File

@@ -1,9 +1,10 @@
const { createConfig } = require('@edx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint', module.exports = createConfig(
{ 'eslint',
"plugins": ["simple-import-sort"], {
"rules": { plugins: ['simple-import-sort'],
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',
@@ -25,7 +26,6 @@ module.exports = createConfig('eslint',
}, },
], ],
'simple-import-sort/exports': 'error', 'simple-import-sort/exports': 'error',
} },
} },
); );

View File

@@ -9,18 +9,17 @@ 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: ${{ matrix.node }} node-version: ${{ env.NODE_VER }}
- 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.yml@master uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

2
.nvmrc
View File

@@ -1 +1 @@
16 18

31338
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": "11.2.0", "@edx/frontend-component-footer": "12.0.0",
"@edx/frontend-component-header": "3.2.0", "@edx/frontend-component-header": "4.0.3",
"@edx/frontend-platform": "2.6.1", "@edx/frontend-platform": "4.4.0",
"@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": "11.0.1", "@edx/frontend-build": "12.8.38",
"@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,19 +19,21 @@ 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';
function FilterBar({ const 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(() => cohorts.find(cohort => ( const selectedCohort = useMemo(
toString(cohort.id) === selectedFilters.cohort)), () => cohorts.find(cohort => (
[selectedFilters.cohort]); toString(cohort.id) === selectedFilters.cohort)),
[selectedFilters.cohort],
);
const allFilters = [ const allFilters = [
{ {
@@ -183,7 +185,7 @@ function 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';
function FormikErrorFeedback({ name }) { const FormikErrorFeedback = ({ name }) => {
const { const {
touched, touched,
errors, errors,
@@ -26,7 +26,7 @@ function 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'],
}; };
function HTMLLoader({ const 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 @@ function 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';
function CourseTabsNavigation({ const 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 @@ function CourseTabsNavigation({
</div> </div>
</div> </div>
); );
} };
CourseTabsNavigation.propTypes = { CourseTabsNavigation.propTypes = {
activeTab: PropTypes.string, activeTab: PropTypes.string,

View File

@@ -56,8 +56,10 @@ 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(200, axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(
(Factory.build('navigationBar', 1, { hasCourseAccess: false }))); 200,
(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';
export default function Tabs({ children, className, ...attrs }) { const Tabs = ({ children, className, ...attrs }) => {
const [ const [
indexOfLastVisibleChild, indexOfLastVisibleChild,
containerElementRef, containerElementRef,
@@ -31,25 +31,28 @@ export default function 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(indexOfOverflowStart, 0, ( wrappedChildren.splice(
<div indexOfOverflowStart,
className="nav-item flex-shrink-0" 0, (
style={indexOfOverflowStart >= React.Children.count(children) ? invisibleStyle : null} <div
ref={overflowElementRef} className="nav-item flex-shrink-0"
key="overflow" style={indexOfOverflowStart >= React.Children.count(children) ? invisibleStyle : null}
> ref={overflowElementRef}
<Dropdown className="h-100"> key="overflow"
<Dropdown.Toggle variant="link" className="nav-link h-100" id="learn.course.tabs.navigation.overflow.menu"> >
<FormattedMessage <Dropdown className="h-100">
id="learn.course.tabs.navigation.overflow.menu" <Dropdown.Toggle variant="link" className="nav-link h-100" id="learn.course.tabs.navigation.overflow.menu">
description="The title of the overflow menu for course tabs" <FormattedMessage
defaultMessage="More..." id="learn.course.tabs.navigation.overflow.menu"
/> description="The title of the overflow menu for course tabs"
</Dropdown.Toggle> defaultMessage="More..."
<Dropdown.Menu className="dropdown-menu-right">{overflowChildren}</Dropdown.Menu> />
</Dropdown> </Dropdown.Toggle>
</div> <Dropdown.Menu className="dropdown-menu-right">{overflowChildren}</Dropdown.Menu>
)); </Dropdown>
</div>
),
);
return wrappedChildren; return wrappedChildren;
}, [children, indexOfLastVisibleChild]); }, [children, indexOfLastVisibleChild]);
@@ -62,7 +65,7 @@ export default function Tabs({ children, className, ...attrs }) {
{tabChildren} {tabChildren}
</nav> </nav>
); );
} };
Tabs.propTypes = { Tabs.propTypes = {
children: PropTypes.node, children: PropTypes.node,
@@ -73,3 +76,5 @@ 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';
function PostPreviewPanel({ const 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 @@ function PostPreviewPanel({
</div> </div>
</> </>
); );
} };
PostPreviewPanel.propTypes = { PostPreviewPanel.propTypes = {
intl: intlShape.isRequired, 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 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';
function Search({ intl }) { const Search = ({ intl }) => {
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);
@@ -56,29 +56,27 @@ function Search({ intl }) {
useEffect(() => onClear(), [page]); useEffect(() => onClear(), [page]);
return ( return (
<> <SearchField.Advanced
<SearchField.Advanced onClear={onClear}
onClear={onClear} onChange={onChange}
onChange={onChange} onSubmit={onSubmit}
onSubmit={onSubmit} value={currentValue}
value={currentValue} >
> <SearchField.Label />
<SearchField.Label /> <SearchField.Input
<SearchField.Input style={{ paddingRight: '1rem' }}
style={{ paddingRight: '1rem' }} placeholder={intl.formatMessage(postsMessages.search, { page: camelCase(page) })}
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 className="mt-auto mb-auto mr-2.5 pointer-cursor-hover"> </span>
<Icon </SearchField.Advanced>
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,34 +8,32 @@ 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';
function SearchInfo({ const SearchInfo = ({
intl, intl,
count, count,
text, text,
loadingStatus, loadingStatus,
onClear, onClear,
textSearchRewrite, textSearchRewrite,
}) { }) => (
return ( <div className="d-flex flex-row border-bottom border-light-400">
<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" />
<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">
<Button variant="" size="inline" className="text-justify p-2"> {loadingStatus === RequestStatus.SUCCESSFUL && (
{loadingStatus === RequestStatus.SUCCESSFUL && ( textSearchRewrite ? intl.formatMessage(messages.searchRewriteInfo, {
textSearchRewrite ? intl.formatMessage(messages.searchRewriteInfo, { searchString: text,
searchString: text, count,
count, textSearchRewrite,
textSearchRewrite, })
}) : intl.formatMessage(messages.searchInfo, { count, text })
: intl.formatMessage(messages.searchInfo, { count, text }) )}
)} {loadingStatus !== RequestStatus.SUCCESSFUL && intl.formatMessage(messages.searchInfoSearching)}
{loadingStatus !== RequestStatus.SUCCESSFUL && intl.formatMessage(messages.searchInfoSearching)} </Button>
</Button> <Button variant="link" size="inline" className="ml-auto mr-3" onClick={onClear} style={{ minWidth: '26%' }}>
<Button variant="link" size="inline" className="ml-auto mr-3" onClick={onClear} style={{ minWidth: '26%' }}> {intl.formatMessage(messages.clearSearch)}
{intl.formatMessage(messages.clearSearch)} </Button>
</Button> </div>
</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 */
export default function TinyMCEEditor(props) { const 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,4 +148,6 @@ export default function 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';
function TopicStats({ const 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 @@ function 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';
export default function InsertLink() { const InsertLink = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="24"
width="24" height="24"
height="24" fill="none"
fill="none" viewBox="0 0 24 24"
viewBox="0 0 24 24" >
> <path
<path fill="currentColor"
fill="currentColor" fillRule="evenodd"
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"
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"
clipRule="evenodd" />
/> </svg>
</svg> );
);
} export default InsertLink;

View File

@@ -1,26 +1,26 @@
import React from 'react'; import React from 'react';
export default function Issue() { const Issue = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="28"
width="28" height="28"
height="28" fill="none"
fill="none" viewBox="0 0 28 28"
viewBox="0 0 28 28" >
> <path
<path fill="#F2F0EF"
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"
d="M0 14C0 6.268 6.268 0 14 0s14 6.268 14 14-6.268 14-14 14S0 21.732 0 14z" />
/> <path
<path fill="#2D494E"
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"
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
<path fill="#fff"
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"
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';
export default function People() { const People = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="16"
width="16" height="16"
height="16" fill="none"
fill="none" viewBox="0 0 16 16"
viewBox="0 0 16 16" >
> <path
<path fill="#707070"
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"
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';
export default function PushPin() { const PushPin = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="24"
width="24" height="24"
height="24" fill="none"
fill="none" viewBox="0 0 24 24"
viewBox="0 0 24 24" >
> <path
<path fill="currentColor"
fill="currentColor" fillRule="evenodd"
fillRule="evenodd" d="M16 9V4H18V2H6V4H8V9C8 10.66 6.66 12 5 12V14H10.97V21L11.97 22L12.97 21V14H19V12C17.34 12 16 10.66 16 9Z"
d="M16 9V4H18V2H6V4H8V9C8 10.66 6.66 12 5 12V14H10.97V21L11.97 22L12.97 21V14H19V12C17.34 12 16 10.66 16 9Z" clipRule="evenodd"
clipRule="evenodd" />
/> </svg>
</svg> );
);
} export default PushPin;

View File

@@ -1,26 +1,26 @@
import React from 'react'; import React from 'react';
export default function Question() { const Question = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="28"
width="28" height="28"
height="28" fill="none"
fill="none" viewBox="0 0 28 28"
viewBox="0 0 28 28" >
> <path
<path fill="#fff"
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"
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
<path fill="#2D494E"
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"
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
<path fill="#fff"
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"
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';
export default function QuestionAnswer() { const QuestionAnswer = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="21"
width="21" height="20"
height="20" fill="none"
fill="none" viewBox="0 0 21 20"
viewBox="0 0 21 20" >
> <path
<path fill="currentColor"
fill="currentColor" d="M18.737 5h-2.5v7.5H5.404V15h10l3.333 3.333V5zm-4.166 5.833V1.667H2.07v12.5l3.333-3.334h9.166z"
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';
export default function QuestionAnswerOutline() { const QuestionAnswerOutline = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="20"
width="20" height="20"
height="20" fill="none"
fill="none" viewBox="0 0 20 20"
viewBox="0 0 20 20" >
> <path
<path fill="currentColor"
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"
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';
export default function StarFilled() { const StarFilled = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="21"
width="21" height="20"
height="20" fill="none"
fill="none" viewBox="0 0 21 20"
viewBox="0 0 21 20" >
> <path
<path fill="currentColor"
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"
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';
export default function StarOutline() { const StarOutline = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="20"
width="20" height="20"
height="20" fill="none"
fill="none" viewBox="0 0 20 20"
viewBox="0 0 20 20" >
> <path
<path fill="currentColor"
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"
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';
export default function ThumbUpFilled() { const ThumbUpFilled = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="21"
width="21" height="20"
height="20" fill="none"
fill="none" viewBox="0 0 21 20"
viewBox="0 0 21 20" >
> <path
<path fill="currentColor"
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"
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';
export default function ThumbUpOutline() { const ThumbUpOutline = () => (
return ( <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" width="20"
width="20" height="20"
height="20" fill="none"
fill="none" viewBox="0 0 20 20"
viewBox="0 0 20 20" >
> <path
<path fill="currentColor"
fill="currentColor" fillRule="evenodd"
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"
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"
clipRule="evenodd" />
/> <path fill="currentColor" d="M4.57 17.5H1.237v-10H4.57v10z" />
<path fill="currentColor" d="M4.57 17.5H1.237v-10H4.57v10z" /> </svg>
</svg> );
);
} export default ThumbUpOutline;

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';
function ActionsDropdown({ const 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 @@ function 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';
function AlertBanner({ const 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 @@ function 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';
function AuthorLabel({ const AuthorLabel = ({
intl, intl,
author, author,
authorLabel, authorLabel,
@@ -26,7 +26,7 @@ function 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 @@ function 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,19 +66,20 @@ 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', ( ])('for %s', (author, authorLabel, linkToProfile, labelColor) => {
author, authorLabel, linkToProfile, labelColor, it(
) => { '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 is "${!linkToProfile && 'not'}" clickable when linkToProfile is ${!!linkToProfile}`, it(
`it is "${!linkToProfile && 'not'}" clickable when linkToProfile is ${!!linkToProfile}`,
async () => { async () => {
renderComponent(author, authorLabel, linkToProfile, labelColor); renderComponent(author, authorLabel, linkToProfile, labelColor);
@@ -87,9 +88,11 @@ describe('Author label', () => {
} else { } else {
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument(); 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 () => { async () => {
renderComponent(author, authorLabel, linkToProfile, labelColor); renderComponent(author, authorLabel, linkToProfile, labelColor);
const authorElement = container.querySelector('[role=heading]'); const authorElement = container.querySelector('[role=heading]');
@@ -104,6 +107,7 @@ 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';
function Confirmation({ const Confirmation = ({
intl, intl,
isOpen, isOpen,
title, title,
@@ -16,30 +16,28 @@ function Confirmation({
closeButtonVaraint, closeButtonVaraint,
confirmButtonVariant, confirmButtonVariant,
confirmButtonText, confirmButtonText,
}) { }) => (
return ( <ModalDialog title={title} isOpen={isOpen} hasCloseButton={false} onClose={onClose} zIndex={5000}>
<ModalDialog title={title} isOpen={isOpen} hasCloseButton={false} onClose={onClose} zIndex={5000}> <ModalDialog.Header>
<ModalDialog.Header> <ModalDialog.Title>
<ModalDialog.Title> {title}
{title} </ModalDialog.Title>
</ModalDialog.Title> </ModalDialog.Header>
</ModalDialog.Header> <ModalDialog.Body>
<ModalDialog.Body> {description}
{description} </ModalDialog.Body>
</ModalDialog.Body> <ModalDialog.Footer>
<ModalDialog.Footer> <ActionRow>
<ActionRow> <ModalDialog.CloseButton variant={closeButtonVaraint}>
<ModalDialog.CloseButton variant={closeButtonVaraint}> {intl.formatMessage(messages.confirmationCancel)}
{intl.formatMessage(messages.confirmationCancel)} </ModalDialog.CloseButton>
</ModalDialog.CloseButton> <Button variant={confirmButtonVariant} onClick={comfirmAction}>
<Button variant={confirmButtonVariant} onClick={comfirmAction}> { confirmButtonText || intl.formatMessage(messages.confirmationConfirm)}
{ confirmButtonText || intl.formatMessage(messages.confirmationConfirm)} </Button>
</Button> </ActionRow>
</ActionRow> </ModalDialog.Footer>
</ModalDialog.Footer> </ModalDialog>
</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';
function EndorsedAlertBanner({ const 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 @@ function EndorsedAlertBanner({
</Alert> </Alert>
) )
); );
} };
EndorsedAlertBanner.propTypes = { EndorsedAlertBanner.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -21,9 +21,7 @@ function buildTestContent(type, buildParams) {
return camelCaseObject(Factory.build(type, { ...buildParamsSnakeCase }, null)); return camelCaseObject(Factory.build(type, { ...buildParamsSnakeCase }, null));
} }
function renderComponent( function renderComponent(content, postType) {
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';
function HoverCard({ const HoverCard = ({
intl, intl,
commentOrPost, commentOrPost,
actionHandlers, actionHandlers,
@@ -27,7 +27,7 @@ function HoverCard({
onFollow, onFollow,
isClosedPost, isClosedPost,
endorseIcons, endorseIcons,
}) { }) => {
const { enableInContextSidebar } = useContext(DiscussionContext); const { enableInContextSidebar } = useContext(DiscussionContext);
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate(); const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
return ( return (
@@ -40,8 +40,10 @@ function HoverCard({
<div className="d-flex"> <div className="d-flex">
<Button <Button
variant="tertiary" variant="tertiary"
className={classNames('px-2.5 py-2 border-0 font-style text-gray-700 font-size-12', className={classNames(
{ 'w-100': enableInContextSidebar })} 'px-2.5 py-2 border-0 font-style text-gray-700 font-size-12',
{ 'w-100': enableInContextSidebar },
)}
onClick={() => handleResponseCommentButton()} onClick={() => handleResponseCommentButton()}
disabled={isClosedPost} disabled={isClosedPost}
style={{ lineHeight: '20px' }} style={{ lineHeight: '20px' }}
@@ -107,7 +109,7 @@ function HoverCard({
</div> </div>
</div> </div>
); );
} };
HoverCard.propTypes = { HoverCard.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
@@ -118,6 +120,7 @@ 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', () => {
function ComponentWithHook() { const 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', () => {
function ComponentWithHook() { const 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';
function BlackoutInformationBanner({ const 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 @@ function 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';
function DiscussionContent() { const DiscussionContent = () => {
const postEditorVisible = useSelector((state) => state.threads.postEditorVisible); const postEditorVisible = useSelector((state) => state.threads.postEditorVisible);
return ( return (
@@ -32,6 +32,6 @@ function 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';
export default function DiscussionSidebar({ displaySidebar, postActionBarRef }) { const 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 @@ export default function DiscussionSidebar({ displaySidebar, postActionBarRef })
</Switch> </Switch>
</div> </div>
); );
} };
DiscussionSidebar.defaultProps = { DiscussionSidebar.defaultProps = {
displaySidebar: false, displaySidebar: false,
@@ -112,3 +112,5 @@ 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';
export default function DiscussionsHome() { const DiscussionsHome = () => {
const location = useLocation(); const location = useLocation();
const postActionBarRef = useRef(null); const postActionBarRef = useRef(null);
const postEditorVisible = useSelector(selectPostEditorVisible); const postEditorVisible = useSelector(selectPostEditorVisible);
@@ -67,6 +67,7 @@ export default function 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,
@@ -130,4 +131,6 @@ export default function DiscussionsHome() {
{!enableInContextSidebar && <Footer />} {!enableInContextSidebar && <Footer />}
</DiscussionContext.Provider> </DiscussionContext.Provider>
); );
} };
export default React.memo(DiscussionsHome);

View File

@@ -172,7 +172,8 @@ 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,
@@ -193,7 +194,8 @@ 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';
function InformationBanner({ const 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 @@ function 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';
function EmptyLearners({ intl }) { const EmptyLearners = ({ intl }) => {
const isOnDesktop = useIsOnDesktop(); const isOnDesktop = useIsOnDesktop();
if (!isOnDesktop) { if (!isOnDesktop) {
@@ -16,7 +16,7 @@ function 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';
function EmptyPage({ const 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 @@ function EmptyPage({
</div> </div>
</div> </div>
); );
} };
EmptyPage.propTypes = { EmptyPage.propTypes = {
title: propTypes.string.isRequired, title: propTypes.string.isRequired,

View File

@@ -8,10 +8,11 @@ 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';
function EmptyPosts({ intl, subTitleMessage }) { const EmptyPosts = ({ intl, subTitleMessage }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isFiltered = useSelector(selectAreThreadsFiltered); const isFiltered = useSelector(selectAreThreadsFiltered);
@@ -49,7 +50,7 @@ function EmptyPosts({ intl, subTitleMessage }) {
fullWidth={fullWidth} fullWidth={fullWidth}
/> />
); );
} };
EmptyPosts.propTypes = { EmptyPosts.propTypes = {
subTitleMessage: propTypes.shape({ subTitleMessage: propTypes.shape({

View File

@@ -9,10 +9,11 @@ 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';
function EmptyTopics({ intl }) { const EmptyTopics = ({ intl }) => {
const match = useRouteMatch(ALL_ROUTES); const match = useRouteMatch(ALL_ROUTES);
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -62,7 +63,7 @@ function EmptyTopics({ intl }) {
fullWidth={fullWidth} fullWidth={fullWidth}
/> />
); );
} };
EmptyTopics.propTypes = { EmptyTopics.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -1,4 +1,5 @@
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';
function TopicPostsView({ intl }) { const 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 @@ function TopicPostsView({ intl }) {
</div> </div>
</div> </div>
); );
} };
TopicPostsView.propTypes = { TopicPostsView.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -206,7 +206,8 @@ 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();
@@ -226,7 +227,8 @@ 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();
@@ -253,7 +255,8 @@ 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 () => { 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' });
@@ -261,7 +264,8 @@ 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';
function TopicsList() { const 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 @@ function TopicsList() {
)} )}
</> </>
); );
} };
function TopicsView() { const 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 @@ function 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';
function BackButton({ const BackButton = ({
intl, path, title, loading, intl, path, title, loading,
}) { }) => {
const history = useHistory(); const history = useHistory();
return ( return (
@@ -32,7 +32,7 @@ function 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';
function EmptyTopics({ intl }) { const 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 @@ function 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';
function NoResults({ intl }) { const NoResults = ({ intl }) => {
const topics = useSelector(selectTopics); const topics = useSelector(selectTopics);
const title = messages.nothingHere; const title = messages.nothingHere;
@@ -20,7 +20,7 @@ function 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,12 +20,21 @@ 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('legacy_web_url', ['id', 'courseId'], .sequence(
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}?experience=legacy`) 'legacy_web_url',
.sequence('lms_web_url', ['id', 'courseId'], ['id', 'courseId'],
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}`) (idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}?experience=legacy`,
.sequence('student_view_url', ['id', 'courseId'], )
(idx, id) => `${getApiBaseUrl}/xblock/block-v1:${id}`) .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('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) => {
@@ -51,12 +60,21 @@ 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('legacy_web_url', ['id', 'courseId'], .sequence(
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}?experience=legacy`) 'legacy_web_url',
.sequence('lms_web_url', ['id', 'courseId'], ['id', 'courseId'],
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`) (idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}?experience=legacy`,
.sequence('student_view_url', ['id', 'courseId'], )
(idx, id, courseId) => `${getApiBaseUrl}/xblock/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`) .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('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';
function TopicSearchBar({ intl }) { const 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,29 +34,27 @@ function TopicSearchBar({ intl }) {
useEffect(() => onClear(), [page]); useEffect(() => onClear(), [page]);
return ( return (
<> <SearchField.Advanced
<SearchField.Advanced onClear={onClear}
onClear={onClear} onChange={onChange}
onChange={onChange} onSubmit={onSubmit}
onSubmit={onSubmit} value={topicSearch}
value={topicSearch} >
> <SearchField.Label />
<SearchField.Label /> <SearchField.Input
<SearchField.Input style={{ paddingRight: '1rem' }}
style={{ paddingRight: '1rem' }} placeholder={intl.formatMessage(postsMessages.search, { page: 'topics' })}
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 className="mt-auto mb-auto mr-2.5 pointer-cursor-hover"> </span>
<Icon </SearchField.Advanced>
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';
function TopicSearchResultBar({ intl }) { const TopicSearchResultBar = ({ intl }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
return ( return (
@@ -21,7 +21,7 @@ function TopicSearchResultBar({ intl }) {
/> />
</div> </div>
); );
} };
TopicSearchResultBar.propTypes = { TopicSearchResultBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -6,35 +6,33 @@ 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';
function ArchivedBaseGroup({ const ArchivedBaseGroup = ({
archivedTopics, archivedTopics,
showDivider, showDivider,
intl, intl,
}) { }) => (
return ( <>
{showDivider && (
<> <>
{showDivider && ( <div className="divider border-top border-light-500" />
<> <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';
function SectionBaseGroup({ const 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 @@ function 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 */ /* eslint-disable no-unused-vars, react/forbid-prop-types, react/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';
function Topic({ const 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 @@ function 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';
function LearnerPostsView({ intl }) { const LearnerPostsView = ({ intl }) => {
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -114,7 +114,7 @@ function LearnerPostsView({ intl }) {
</div> </div>
</div> </div>
); );
} };
LearnerPostsView.propTypes = { LearnerPostsView.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -106,7 +106,8 @@ describe('Learner Posts View', () => {
expect(backButton).toBeInTheDocument(); 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 () => { async () => {
await renderComponent(); await renderComponent();
@@ -116,7 +117,8 @@ 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';
function LearnersView({ intl }) { const LearnersView = ({ intl }) => {
const { courseId } = useParams(); const { courseId } = useParams();
const location = useLocation(); const location = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -98,7 +98,7 @@ function LearnersView({ intl }) {
</div> </div>
</div> </div>
); );
} };
LearnersView.propTypes = { LearnersView.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -1,3 +1,4 @@
/* eslint-disable default-param-last */
import React from 'react'; import React from 'react';
import { import {
@@ -200,7 +201,8 @@ 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,
}) => { }) => {
@@ -226,7 +228,8 @@ 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();
@@ -256,7 +259,8 @@ describe('LearnersView', () => {
expect(learners).toHaveLength(3); 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 () => { 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);
@@ -273,7 +277,8 @@ 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,7 +29,8 @@ describe('Learner api test cases', () => {
axiosMock.reset(); 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 () => { async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
const threads = await setupPostsMockResponse(); const threads = await setupPostsMockResponse();
@@ -38,20 +39,23 @@ 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,7 +37,8 @@ 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.getState, store.dispatch,
store.getState,
); );
const { learners } = store.getState(); const { learners } = store.getState();
@@ -55,7 +56,8 @@ describe('Learner redux test cases', () => {
expect(learners.postFilter.cohort).toEqual(''); 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 () => { async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
const page = learners.pages[0]; const page = learners.pages[0];
@@ -65,9 +67,11 @@ 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('Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux', test(
'Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux',
async () => { async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
@@ -75,7 +79,8 @@ 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();
@@ -106,7 +111,8 @@ describe('Learner redux test cases', () => {
expect(updatedLearners.pages).toHaveLength(0); 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 () => { async () => {
const learners = await setupLearnerMockResponse(); const learners = await setupLearnerMockResponse();
@@ -116,5 +122,6 @@ 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';
function LearnerPostFilterBar() { const 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 @@ function LearnerPostFilterBar() {
showCohortsFilter={userHasModerationPrivileges || userIsGroupTa} showCohortsFilter={userHasModerationPrivileges || userIsGroupTa}
/> />
); );
} };
export default LearnerPostFilterBar; export default LearnerPostFilterBar;

View File

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

View File

@@ -8,7 +8,7 @@ import { Dropdown, DropdownButton } from '@edx/paragon';
import messages from './messages'; import messages from './messages';
function BreadcrumbDropdown({ const BreadcrumbDropdown = ({
currentItem, currentItem,
intl, intl,
showAllPath, showAllPath,
@@ -17,7 +17,7 @@ function 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 @@ function 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';
function LegacyBreadcrumbMenu() { const LegacyBreadcrumbMenu = () => {
const { const {
params: { params: {
courseId, courseId,
@@ -78,7 +78,7 @@ function 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';
function NavigationBar({ intl }) { const NavigationBar = ({ intl }) => {
const { courseId } = useParams(); const { courseId } = useParams();
const showLearnersTab = useShowLearnersTab(); const showLearnersTab = useShowLearnersTab();
@@ -52,7 +52,7 @@ function 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';
function PostCommentsView({ intl }) { const 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 @@ function PostCommentsView({ intl }) {
)} )}
</> </>
); );
} };
PostCommentsView.propTypes = { PostCommentsView.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -384,12 +384,16 @@ 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(screen.queryByRole('combobox', { name: /reason for editing/i }), fireEvent.change(
{ target: { value: null } }); screen.queryByRole('combobox', { name: /reason for editing/i }),
{ target: { value: null } },
);
}); });
await act(async () => { await act(async () => {
fireEvent.change(screen.queryByRole('combobox', fireEvent.change(screen.queryByRole(
{ name: /reason for editing/i }), { target: { value: 'reason-1' } }); 'combobox',
{ 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';
function CommentSortDropdown({ intl }) { const 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 @@ function 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';
function CommentsView({ const CommentsView = ({
postType, postType,
postId, postId,
intl, intl,
endorsed, endorsed,
isClosed, isClosed,
}) { }) => {
const { const {
comments, comments,
hasMorePages, hasMorePages,
@@ -71,51 +71,49 @@ function CommentsView({
); );
return ( return (
((hasMorePages && isLoading) || !isLoading) && (
<> <>
{((hasMorePages && isLoading) || !isLoading) && ( {endorsedComments.length > 0 && (
<> <>
{endorsedComments.length > 0 && ( {handleDefinition(messages.endorsedResponseCount, endorsedComments.length)}
<> {endorsed === EndorsementStatus.DISCUSSION
{handleDefinition(messages.endorsedResponseCount, endorsedComments.length)} ? handleComments(endorsedComments, true)
{endorsed === EndorsementStatus.DISCUSSION : handleComments(endorsedComments, false)}
? handleComments(endorsedComments, true) </>
: handleComments(endorsedComments, false)} )}
</> {endorsed !== EndorsementStatus.ENDORSED && (
)} <>
{endorsed !== EndorsementStatus.ENDORSED && ( {handleDefinition(messages.responseCount, unEndorsedComments.length)}
<> {unEndorsedComments.length === 0 && <br />}
{handleDefinition(messages.responseCount, unEndorsedComments.length)} {handleComments(unEndorsedComments, false)}
{unEndorsedComments.length === 0 && <br />} {(userCanAddThreadInBlackoutDate && !!unEndorsedComments.length && !isClosed) && (
{handleComments(unEndorsedComments, false)} <div className="mx-4">
{(userCanAddThreadInBlackoutDate && !!unEndorsedComments.length && !isClosed) && ( {!addingResponse && (
<div className="mx-4"> <Button
{!addingResponse && ( variant="plain"
<Button block="true"
variant="plain" className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
block="true"
className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
line-height-24 font-size-14 text-primary-500" line-height-24 font-size-14 text-primary-500"
onClick={() => setAddingResponse(true)} onClick={() => setAddingResponse(true)}
data-testid="add-response" data-testid="add-response"
> >
{intl.formatMessage(messages.addResponse)} {intl.formatMessage(messages.addResponse)}
</Button> </Button>
)}
<ResponseEditor
postId={postId}
handleCloseEditor={() => setAddingResponse(false)}
addWrappingDiv
addingResponse={addingResponse}
/>
</div>
)}
</>
)} )}
</> <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';
function Comment({ const 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,26 +201,24 @@ function Comment({
/> />
</div> </div>
) : ( ) : (
<> !isClosedPost && userCanAddThreadInBlackoutDate && (inlineReplies.length >= 5)
{!isClosedPost && userCanAddThreadInBlackoutDate && (inlineReplies.length >= 5) && (
&& ( <Button
<Button className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500"
className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500" variant="plain"
variant="plain" style={{ height: '36px' }}
style={{ height: '36px' }} onClick={() => setReplying(true)}
onClick={() => setReplying(true)} >
> {intl.formatMessage(messages.addComment)}
{intl.formatMessage(messages.addComment)} </Button>
</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';
function CommentEditor({ const 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,7 +57,10 @@ function CommentEditor({
const initialValues = { const initialValues = {
comment: comment.rawBody, comment: comment.rawBody,
editReasonCode: comment?.lastEdit?.reasonCode || (userIsStaff ? 'violates-guidelines' : ''), // eslint-disable-next-line react/prop-types
editReasonCode: comment?.lastEdit?.reasonCode || (
userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined
),
}; };
const handleCloseEditor = (resetForm) => { const handleCloseEditor = (resetForm) => {
@@ -173,7 +176,7 @@ function CommentEditor({
)} )}
</Formik> </Formik>
); );
} };
CommentEditor.propTypes = { CommentEditor.propTypes = {
comment: PropTypes.shape({ comment: PropTypes.shape({
@@ -182,7 +185,7 @@ CommentEditor.propTypes = {
parentId: PropTypes.string, parentId: PropTypes.string,
rawBody: PropTypes.string, rawBody: PropTypes.string,
author: PropTypes.string, author: PropTypes.string,
lastEdit: PropTypes.object, lastEdit: PropTypes.shape({}),
}).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';
function CommentHeader({ const 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 @@ function 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';
function Reply({ const 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 @@ function 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';
function ResponseEditor({ const ResponseEditor = ({
postId, postId,
addWrappingDiv, addWrappingDiv,
handleCloseEditor, handleCloseEditor,
addingResponse, addingResponse,
}) { }) => {
useEffect(() => { useEffect(() => {
handleCloseEditor(); handleCloseEditor();
}, [postId]); }, [postId]);
@@ -27,7 +27,7 @@ function ResponseEditor({
/> />
</div> </div>
); );
} };
ResponseEditor.propTypes = { ResponseEditor.propTypes = {
postId: PropTypes.string.isRequired, postId: PropTypes.string.isRequired,

View File

@@ -20,15 +20,13 @@ export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussi
* @param enableInContextSidebar * @param enableInContextSidebar
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
export async function getThreadComments( export async function getThreadComments(threadId, {
threadId, { endorsed,
endorsed, page,
page, pageSize,
pageSize, reverseOrder,
reverseOrder, enableInContextSidebar = false,
enableInContextSidebar = false, } = {}) {
} = {},
) {
const params = snakeCaseObject({ const params = snakeCaseObject({
threadId, threadId,
endorsed: EndorsementValue[endorsed], endorsed: EndorsementValue[endorsed],
@@ -51,13 +49,11 @@ export async function getThreadComments(
* @param {number=} pageSize * @param {number=} pageSize
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
export async function getCommentResponses( export async function getCommentResponses(commentId, {
commentId, { page,
page, pageSize,
pageSize, reverseOrder,
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';
function NoResults({ intl }) { const 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 @@ function 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';
function PostsList({ const 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 @@ function 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';
function AllPostsList() { const AllPostsList = () => {
const posts = useSelector(selectAllThreads); const posts = useSelector(selectAllThreads);
return <PostsList posts={posts} topics={null} />; return <PostsList posts={posts} topics={null} />;
} };
function TopicPostsList({ topicId }) { const 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,
}; };
function CategoryPostsList({ category }) { const 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,
}; };
function PostsView() { const PostsView = () => {
const { const {
topicId, topicId,
category, category,
@@ -96,7 +96,7 @@ function PostsView() {
</div> </div>
</div> </div>
); );
} };
PostsView.propTypes = { PostsView.propTypes = {
}; };

View File

@@ -28,22 +28,20 @@ export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussio
* @param {number} cohort * @param {number} cohort
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
export async function getThreads( export async function getThreads(courseId, {
courseId, { topicIds,
topicIds, page,
page, pageSize,
pageSize, textSearch,
textSearch, orderBy,
orderBy, following,
following, view,
view, author,
author, flagged,
flagged, threadType,
threadType, countFlagged,
countFlagged, cohort,
cohort, } = {}) {
} = {},
) {
const params = snakeCaseObject({ const params = snakeCaseObject({
courseId, courseId,
page, page,

View File

@@ -2,5 +2,6 @@
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';
function PostActionsBar({ const 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,8 +51,10 @@ function 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('my-0 font-style border-0 line-height-24', className={classNames(
{ 'px-3 py-10px border-0': enableInContextSidebar })} 'my-0 font-style border-0 line-height-24',
{ 'px-3 py-10px border-0': enableInContextSidebar },
)}
onClick={() => dispatch(showPostEditor())} onClick={() => dispatch(showPostEditor())}
size={enableInContextSidebar ? 'md' : 'sm'} size={enableInContextSidebar ? 'md' : 'sm'}
> >
@@ -77,7 +79,7 @@ function PostActionsBar({
)} )}
</div> </div>
); );
} };
PostActionsBar.propTypes = { PostActionsBar.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -34,6 +34,7 @@ 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,
@@ -50,12 +51,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';
function DiscussionPostType({ const 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 (
@@ -75,7 +76,7 @@ function DiscussionPostType({
</Card> </Card>
</label> </label>
); );
} };
DiscussionPostType.propTypes = { DiscussionPostType.propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
@@ -84,9 +85,9 @@ DiscussionPostType.propTypes = {
icon: PropTypes.element.isRequired, icon: PropTypes.element.isRequired,
}; };
function PostEditor({ const PostEditor = ({
editExisting, editExisting,
}) { }) => {
const intl = useIntl(); const intl = useIntl();
const { authenticatedUser } = useContext(AppContext); const { authenticatedUser } = useContext(AppContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -500,7 +501,7 @@ function 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,
}; };
function PostFilterBar({ const PostFilterBar = ({
intl, intl,
}) { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { courseId } = useParams(); const { courseId } = useParams();
const { page } = useContext(DiscussionContext); const { page } = useContext(DiscussionContext);
@@ -75,9 +75,11 @@ function PostFilterBar({
const cohorts = useSelector(selectCourseCohorts); const cohorts = useSelector(selectCourseCohorts);
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const selectedCohort = useMemo(() => cohorts.find(cohort => ( const selectedCohort = useMemo(
toString(cohort.id) === currentFilters.cohort)), () => cohorts.find(cohort => (
[currentFilters.cohort]); toString(cohort.id) === currentFilters.cohort)),
[currentFilters.cohort],
);
const handleSortFilterChange = (event) => { const handleSortFilterChange = (event) => {
const currentType = currentFilters.postType; const currentType = currentFilters.postType;
@@ -299,7 +301,7 @@ function 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';
function ClosePostReasonModal({ const 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 @@ function 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';
function LikeButton({ const 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 @@ function 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';
function Post({ const 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();
@@ -67,6 +67,11 @@ function 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,
@@ -82,7 +87,7 @@ function Post({
dispatch(updateExistingThread(post.id, { closed: true })); dispatch(updateExistingThread(post.id, { closed: true }));
} }
}, },
[ContentActions.COPY_LINK]: () => { navigator.clipboard.writeText(`${window.location.origin}/${courseId}/posts/${post.id}`); }, [ContentActions.COPY_LINK]: handlePostCopyLink,
[ContentActions.PIN]: () => dispatch(updateExistingThread(post.id, { pinned: !post.pinned })), [ContentActions.PIN]: () => dispatch(updateExistingThread(post.id, { pinned: !post.pinned })),
[ContentActions.REPORT]: () => handleAbusedFlag(), [ContentActions.REPORT]: () => handleAbusedFlag(),
}), [ }), [
@@ -149,8 +154,10 @@ function Post({
</div> </div>
{(topicContext || topic) && ( {(topicContext || topic) && (
<div <div
className={classNames('mt-14px font-style font-size-12', className={classNames(
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter })} 'mt-14px font-style font-size-12',
{ '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>
@@ -183,7 +190,7 @@ function 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';
function PostFooter({ const 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 @@ function PostFooter({
</div> </div>
</div> </div>
); );
} };
PostFooter.propTypes = { PostFooter.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -13,9 +13,9 @@ import { useAlertBannerVisible } from '../../data/hooks';
import messages from './messages'; import messages from './messages';
import { postShape } from './proptypes'; import { postShape } from './proptypes';
export function PostAvatar({ export const PostAvatar = ({
post, authorLabel, fromPostLink, read, post, authorLabel, fromPostLink, read,
}) { }) => {
const outlineColor = AvatarOutlineAndLabelColors[authorLabel]; const outlineColor = AvatarOutlineAndLabelColors[authorLabel];
const avatarSize = useMemo(() => { const avatarSize = useMemo(() => {
@@ -63,7 +63,7 @@ export function PostAvatar({
/> />
</div> </div>
); );
} };
PostAvatar.propTypes = { PostAvatar.propTypes = {
post: postShape.isRequired, post: postShape.isRequired,
@@ -78,11 +78,11 @@ PostAvatar.defaultProps = {
read: false, read: false,
}; };
function PostHeader({ const PostHeader = ({
intl, intl,
post, post,
preview, preview,
}) { }) => {
const showAnsweredBadge = preview && post.hasEndorsed && post.type === ThreadType.QUESTION; const showAnsweredBadge = preview && post.hasEndorsed && post.type === ThreadType.QUESTION;
const authorLabelColor = AvatarOutlineAndLabelColors[post.authorLabel]; const authorLabelColor = AvatarOutlineAndLabelColors[post.authorLabel];
const hasAnyAlert = useAlertBannerVisible(post); const hasAnyAlert = useAlertBannerVisible(post);
@@ -127,7 +127,7 @@ function PostHeader({
</div> </div>
</div> </div>
); );
} };
PostHeader.propTypes = { PostHeader.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

View File

@@ -19,12 +19,12 @@ import { PostAvatar } from './PostHeader';
import PostSummaryFooter from './PostSummaryFooter'; import PostSummaryFooter from './PostSummaryFooter';
import { postShape } from './proptypes'; import { postShape } from './proptypes';
function PostLink({ const PostLink = ({
post, post,
isSelected, isSelected,
showDivider, showDivider,
idx, idx,
}) { }) => {
const intl = useIntl(); const intl = useIntl();
const { const {
page, page,
@@ -47,87 +47,89 @@ function PostLink({
const read = post.read || (!post.read && post.commentCount !== post.unreadCommentCount); const read = post.read || (!post.read && post.commentCount !== post.unreadCommentCount);
return ( return (
<> <Link
<Link className={
className={
classNames('discussion-post p-0 text-decoration-none text-gray-900', { classNames('discussion-post p-0 text-decoration-none text-gray-900', {
'border-bottom border-light-400': showDivider, 'border-bottom border-light-400': showDivider,
}) })
} }
to={linkUrl} to={linkUrl}
onClick={() => isSelected(post.id)} onClick={() => isSelected(post.id)}
aria-current={isSelected(post.id) ? 'page' : undefined} aria-current={isSelected(post.id) ? 'page' : undefined}
role="option" role="option"
tabIndex={(isSelected(post.id) || idx === 0) ? 0 : -1} tabIndex={(isSelected(post.id) || idx === 0) ? 0 : -1}
> >
<div <div
className={ className={
classNames('d-flex flex-row pt-2 pb-2 px-4 border-primary-500 position-relative', classNames(
'd-flex flex-row pt-2 pb-2 px-4 border-primary-500 position-relative',
{ 'bg-light-300': read }, { 'bg-light-300': read },
{ 'post-summary-card-selected': post.id === postId }) { 'post-summary-card-selected': post.id === postId },
)
} }
> >
<PostAvatar post={post} authorLabel={post.authorLabel} fromPostLink read={read} /> <PostAvatar post={post} authorLabel={post.authorLabel} fromPostLink read={read} />
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}> <div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
<div className="d-flex flex-column justify-content-start mw-100 flex-fill" style={{ marginBottom: '-3px' }}> <div className="d-flex flex-column justify-content-start mw-100 flex-fill" style={{ marginBottom: '-3px' }}>
<div className="d-flex align-items-center pb-0 mb-0 flex-fill font-weight-500"> <div className="d-flex align-items-center pb-0 mb-0 flex-fill font-weight-500">
<Truncate lines={1} className="mr-1.5" whiteSpace> <Truncate lines={1} className="mr-1.5" whiteSpace>
<span <span
class={ class={
classNames('font-weight-500 font-size-14 text-primary-500 font-style align-bottom', classNames(
{ 'font-weight-bolder': !read }) 'font-weight-500 font-size-14 text-primary-500 font-style align-bottom',
{ 'font-weight-bolder': !read },
)
} }
> >
{post.title} {post.title}
</span> </span>
<span class="align-bottom"> </span> <span class="align-bottom"> </span>
<span <span
class="text-gray-700 font-weight-normal font-size-14 font-style align-bottom" class="text-gray-700 font-weight-normal font-size-14 font-style align-bottom"
> >
{isPostPreviewAvailable(post.previewBody) {isPostPreviewAvailable(post.previewBody)
? post.previewBody ? post.previewBody
: intl.formatMessage(messages.postWithoutPreview)} : intl.formatMessage(messages.postWithoutPreview)}
</span> </span>
</Truncate> </Truncate>
{showAnsweredBadge && ( {showAnsweredBadge && (
<Icon src={CheckCircle} className="text-success font-weight-500 ml-auto badge-padding" data-testid="check-icon"> <Icon src={CheckCircle} className="text-success font-weight-500 ml-auto badge-padding" data-testid="check-icon">
<span className="sr-only">{' '}answered</span> <span className="sr-only">{' '}answered</span>
</Icon> </Icon>
)} )}
{canSeeReportedBadge && ( {canSeeReportedBadge && (
<Badge <Badge
variant="danger" variant="danger"
data-testid="reported-post" data-testid="reported-post"
className={`font-weight-500 badge-padding ${showAnsweredBadge ? 'ml-2' : 'ml-auto'}`} className={`font-weight-500 badge-padding ${showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
> >
{intl.formatMessage(messages.contentReported)} {intl.formatMessage(messages.contentReported)}
<span className="sr-only">{' '}reported</span> <span className="sr-only">{' '}reported</span>
</Badge> </Badge>
)} )}
{post.pinned && ( {post.pinned && (
<Icon <Icon
src={PushPin} src={PushPin}
className={`post-summary-icons-dimensions text-gray-700 className={`post-summary-icons-dimensions text-gray-700
${canSeeReportedBadge || showAnsweredBadge ? 'ml-2' : 'ml-auto'}`} ${canSeeReportedBadge || showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
/> />
)} )}
</div>
</div> </div>
<AuthorLabel
author={post.author || intl.formatMessage(messages.anonymous)}
authorLabel={post.authorLabel}
labelColor={authorLabelColor && `text-${authorLabelColor}`}
/>
<PostSummaryFooter post={post} preview showNewCountLabel={read} />
</div> </div>
<AuthorLabel
author={post.author || intl.formatMessage(messages.anonymous)}
authorLabel={post.authorLabel}
labelColor={authorLabelColor && `text-${authorLabelColor}`}
/>
<PostSummaryFooter post={post} preview showNewCountLabel={read} />
</div> </div>
{!showDivider && post.pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />} </div>
</Link> {!showDivider && post.pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
</> </Link>
); );
} };
PostLink.propTypes = { PostLink.propTypes = {
post: postShape.isRequired, post: postShape.isRequired,

View File

@@ -18,12 +18,12 @@ import { selectUserHasModerationPrivileges } from '../../data/selectors';
import messages from './messages'; import messages from './messages';
import { postShape } from './proptypes'; import { postShape } from './proptypes';
function PostSummaryFooter({ const PostSummaryFooter = ({
post, post,
intl, intl,
preview, preview,
showNewCountLabel, showNewCountLabel,
}) { }) => {
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
timeago.register('time-locale', timeLocale); timeago.register('time-locale', timeLocale);
return ( return (
@@ -104,7 +104,7 @@ function PostSummaryFooter({
</div> </div>
</div> </div>
); );
} };
PostSummaryFooter.propTypes = { PostSummaryFooter.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,

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