Compare commits

...

12 Commits

Author SHA1 Message Date
vladislavkeblysh
af2d5b1664 feat: Editor bar visibility (palm.master) (#581)
* feat: fixed editor bar visibility

* feat: fixed z index
2024-01-03 17:49:40 +05:00
vladislavkeblysh
b5133147d5 feat: Enhancements to page (palm.master) (#576)
* feat: fixed page styles
2023-12-14 11:52:06 +05:00
Ihor Romaniuk
173b065bc6 fix: unify font-family with paragon component styles (#596) 2023-11-08 18:34:29 +05:00
Ihor Romaniuk
cb28557c21 fix: container indents and style imports (#599) 2023-11-07 18:19:49 +05:00
Awais Ansari
2abe4dfdff fix: moved feedback widget behind env variables (#562) 2023-09-08 18:40:50 +05:00
edX requirements bot
4a1e77bd13 Merge pull request #541 from DmytroAlipov/fix-discussion-search-palm
Fix bug with a repeated search query  for Palm
2023-07-12 06:05:17 -04:00
alipov_d
dcb0f9e0ec fix: issue with a repeated search query for Palm 2023-06-19 18:32:21 +02:00
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
118 changed files with 5278 additions and 28013 deletions

5
.env
View File

@@ -20,6 +20,5 @@ SEGMENT_KEY=''
SITE_NAME='' SITE_NAME=''
USER_INFO_COOKIE_NAME='' USER_INFO_COOKIE_NAME=''
SUPPORT_URL='' SUPPORT_URL=''
TA_FEEDBACK_FORM= '' LEARNER_FEEDBACK_URL=''
STAFF_FEEDBACK_FORM= '' STAFF_FEEDBACK_URL=''
DISPLAY_FEEDBACK_BANNER='false'

View File

@@ -21,6 +21,5 @@ SEGMENT_KEY=''
SITE_NAME=localhost SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info' USER_INFO_COOKIE_NAME='edx-user-info'
SUPPORT_URL='https://support.edx.org' SUPPORT_URL='https://support.edx.org'
TA_FEEDBACK_FORM='https://learner-form.test' LEARNER_FEEDBACK_URL=''
STAFF_FEEDBACK_FORM='https://staff-form.test' STAFF_FEEDBACK_URL=''
DISPLAY_FEEDBACK_BANNER='false'

View File

@@ -19,6 +19,5 @@ SEGMENT_KEY=''
SITE_NAME='localhost' SITE_NAME='localhost'
USER_INFO_COOKIE_NAME='edx-user-info' USER_INFO_COOKIE_NAME='edx-user-info'
SUPPORT_URL='https://support.edx.org' SUPPORT_URL='https://support.edx.org'
TA_FEEDBACK_FORM='https://learner-form.test' LEARNER_FEEDBACK_URL=''
STAFF_FEEDBACK_FORM='https://staff-form.test' STAFF_FEEDBACK_URL=''
DISPLAY_FEEDBACK_BANNER='false'

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

View File

@@ -1,2 +0,0 @@
process.env.TA_FEEDBACK_FORM= 'https://learner-form.test';
process.env.STAFF_FEEDBACK_FORM= 'https://staff-form.test';

2
.nvmrc
View File

@@ -1 +1 @@
16 18

View File

@@ -3,7 +3,7 @@ const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', { module.exports = createConfig('jest', {
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want. // setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
// If you want to add config BEFORE jest loads, use setupFiles instead. // If you want to add config BEFORE jest loads, use setupFiles instead.
setupFiles: ['<rootDir>/.jest/setEnvVars.js'], setupFiles: ['<rootDir>/.env.test'],
setupFilesAfterEnv: [ setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js', '<rootDir>/src/setupTest.js',
], ],

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

@@ -1,10 +1,11 @@
@import "@edx/brand/paragon/fonts.scss"; @import "~@edx/brand/paragon/fonts.scss";
@import "@edx/brand/paragon/variables.scss"; @import "~@edx/brand/paragon/variables.scss";
@import "@edx/paragon/scss/core/core.scss"; @import "~@edx/paragon/scss/core/core.scss";
@import "@edx/brand/paragon/overrides.scss"; @import "~@edx/brand/paragon/overrides.scss";
$fa-font-path: "~font-awesome/fonts"; $fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome"; @import "~font-awesome/scss/font-awesome";
.course-tabs-navigation { .course-tabs-navigation {
border-bottom: solid 1px #eaeaea; border-bottom: solid 1px #eaeaea;

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

@@ -1,4 +1,4 @@
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import camelCase from 'lodash/camelCase'; import camelCase from 'lodash/camelCase';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@@ -13,7 +13,8 @@ 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 [previousSearchValue, setPreviousSearchValue] = useState('');
const dispatch = useDispatch(); const dispatch = useDispatch();
const { page } = useContext(DiscussionContext); const { page } = useContext(DiscussionContext);
const postSearch = useSelector(({ threads }) => threads.filters.search); const postSearch = useSelector(({ threads }) => threads.filters.search);
@@ -35,6 +36,7 @@ function Search({ intl }) {
dispatch(setSearchQuery('')); dispatch(setSearchQuery(''));
dispatch(setTopicFilter('')); dispatch(setTopicFilter(''));
dispatch(setUsernameSearch('')); dispatch(setUsernameSearch(''));
setPreviousSearchValue('');
}; };
const onChange = (query) => { const onChange = (query) => {
@@ -42,7 +44,7 @@ function Search({ intl }) {
}; };
const onSubmit = (query) => { const onSubmit = (query) => {
if (query === '') { if (query === '' || query === previousSearchValue) {
return; return;
} }
if (isPostSearch) { if (isPostSearch) {
@@ -52,33 +54,32 @@ function Search({ intl }) {
} else if (page === 'learners') { } else if (page === 'learners') {
dispatch(setUsernameSearch(query)); dispatch(setUsernameSearch(query));
} }
setPreviousSearchValue(query);
}; };
useEffect(() => onClear(), [page]); useEffect(() => onClear(), [page]);
return ( return (
<> <SearchField.Advanced
<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;
@@ -51,7 +51,7 @@ function AuthorLabel({
const authorName = ( const authorName = (
<span <span
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', { className={classNames('mr-1.5 font-size-14 font-style font-weight-500 author-name', {
'text-gray-700': isRetiredUser, 'text-gray-700': isRetiredUser,
'text-primary-500': !authorLabelMessage && !isRetiredUser, 'text-primary-500': !authorLabelMessage && !isRetiredUser,
})} })}
@@ -99,7 +99,7 @@ function AuthorLabel({
{postCreatedAt && ( {postCreatedAt && (
<span <span
title={postCreatedAt} title={postCreatedAt}
className={classNames('font-family-inter align-content-center', { className={classNames('align-content-center', {
'text-white': alert, 'text-white': alert,
'text-gray-500': !alert, 'text-gray-500': !alert,
})} })}
@@ -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';
@@ -39,7 +39,7 @@ function EndorsedAlertBanner({
height: '20px', height: '20px',
}} }}
/> />
<strong className="ml-2 font-family-inter"> <strong className="ml-2">
{intl.formatMessage(isQuestion ? messages.answer : messages.endorsed)} {intl.formatMessage(isQuestion ? messages.answer : messages.endorsed)}
</strong> </strong>
</div> </div>
@@ -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

@@ -8,7 +8,6 @@ import {
import Footer from '@edx/frontend-component-footer'; import Footer from '@edx/frontend-component-footer';
import { LearningHeader as Header } from '@edx/frontend-component-header'; import { LearningHeader as Header } from '@edx/frontend-component-header';
import { getConfig } from '@edx/frontend-platform';
import { PostActionsBar } from '../../components'; import { PostActionsBar } from '../../components';
import { CourseTabsNavigation } from '../../components/NavigationBar'; import { CourseTabsNavigation } from '../../components/NavigationBar';
@@ -30,9 +29,8 @@ import BlackoutInformationBanner from './BlackoutInformationBanner';
import DiscussionContent from './DiscussionContent'; import DiscussionContent from './DiscussionContent';
import DiscussionSidebar from './DiscussionSidebar'; import DiscussionSidebar from './DiscussionSidebar';
import useFeedbackWrapper from './FeedbackWrapper'; import useFeedbackWrapper from './FeedbackWrapper';
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);
@@ -46,7 +44,6 @@ export default function DiscussionsHome() {
const isOnDesktop = useIsOnDesktop(); const isOnDesktop = useIsOnDesktop();
let displaySidebar = useSidebarVisible(); let displaySidebar = useSidebarVisible();
const enableInContextSidebar = Boolean(new URLSearchParams(location.search).get('inContextSidebar') !== null); const enableInContextSidebar = Boolean(new URLSearchParams(location.search).get('inContextSidebar') !== null);
const isFeedbackBannerVisible = getConfig().DISPLAY_FEEDBACK_BANNER === 'true';
const { const {
courseId, postId, topicId, category, learnerUsername, courseId, postId, topicId, category, learnerUsername,
} = params; } = params;
@@ -67,6 +64,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,
@@ -94,7 +92,6 @@ export default function DiscussionsHome() {
{!enableInContextSidebar && <Route path={Routes.DISCUSSIONS.PATH} component={NavigationBar} />} {!enableInContextSidebar && <Route path={Routes.DISCUSSIONS.PATH} component={NavigationBar} />}
<PostActionsBar /> <PostActionsBar />
</div> </div>
{isFeedbackBannerVisible && <InformationBanner />}
<BlackoutInformationBanner /> <BlackoutInformationBanner />
</div> </div>
{provider === DiscussionProvider.LEGACY && ( {provider === DiscussionProvider.LEGACY && (
@@ -130,4 +127,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

@@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging'; import { logError } from '@edx/frontend-platform/logging';
import { RequestStatus } from '../../data/constants'; import { RequestStatus } from '../../data/constants';
@@ -22,9 +23,9 @@ export default function useFeedbackWrapper() {
useEffect(() => { useEffect(() => {
if (configStatus === RequestStatus.SUCCESSFUL) { if (configStatus === RequestStatus.SUCCESSFUL) {
let url = '//w.usabilla.com/9e6036348fa1.js'; let url = getConfig().LEARNER_FEEDBACK_URL;
if (isStaff || isUserGroupTA || isCourseAdmin || isCourseStaff) { if (isStaff || isUserGroupTA || isCourseAdmin || isCourseStaff) {
url = '//w.usabilla.com/767740a06856.js'; url = getConfig().STAFF_FEEDBACK_URL;
} }
try { try {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef

View File

@@ -1,64 +0,0 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink, PageBanner } from '@edx/paragon';
import { selectUserIsStaff, selectUserRoles } from '../data/selectors';
import messages from '../messages';
function InformationBanner({
intl,
}) {
const [showBanner, setShowBanner] = useState(true);
const userRoles = useSelector(selectUserRoles);
const isAdmin = useSelector(selectUserIsStaff);
const learnMoreLink = 'https://openedx.atlassian.net/wiki/spaces/COMM/pages/3509551260/Overview+New+discussions+experience';
const TAFeedbackLink = process.env.TA_FEEDBACK_FORM;
const staffFeedbackLink = process.env.STAFF_FEEDBACK_FORM;
const hideLearnMoreButton = ((userRoles.includes('Student') && userRoles.length === 1) || !userRoles.length) && !isAdmin;
const showStaffLink = isAdmin || userRoles.includes('Moderator') || userRoles.includes('Administrator');
return (
<PageBanner
variant="light"
show={showBanner}
dismissible
onDismiss={() => setShowBanner(false)}
>
<div className="font-weight-500">
{intl.formatMessage(messages.bannerMessage)}
{!hideLearnMoreButton
&& (
<Hyperlink
destination={learnMoreLink}
target="_blank"
showLaunchIcon={false}
className="pl-2.5"
variant="muted"
isInline
>
{intl.formatMessage(messages.learnMoreBannerLink)}
</Hyperlink>
)}
<Hyperlink
destination={showStaffLink ? staffFeedbackLink : TAFeedbackLink}
target="_blank"
showLaunchIcon={false}
variant="muted"
className="pl-2.5"
isInline
>
{intl.formatMessage(messages.shareFeedback)}
</Hyperlink>
</div>
</PageBanner>
);
}
InformationBanner.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(InformationBanner);

View File

@@ -1,136 +0,0 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store';
import { DiscussionContext } from '../common/context';
import { fetchConfigSuccess } from '../data/slices';
import messages from '../messages';
import InformationBanner from './InformationBanner';
import '../posts/data/__factories__';
let store;
let container;
const courseId = 'course-v1:edX+DemoX+Demo_Course';
const getConfigData = (isAdmin = true, roles = []) => ({
id: 'course-v1:edX+DemoX+Demo_Course',
userRoles: roles,
hasModerationPrivileges: false,
isGroupTa: false,
isUserAdmin: isAdmin,
});
function renderComponent() {
const wrapper = render(
<IntlProvider locale="en">
<AppProvider store={store}>
<DiscussionContext.Provider value={{ courseId }}>
<InformationBanner />
</DiscussionContext.Provider>
</AppProvider>
</IntlProvider>,
);
container = wrapper.container;
return container;
}
describe('Information Banner learner view', () => {
let element;
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: ['Student'],
},
});
store = initializeStore();
store.dispatch(fetchConfigSuccess(getConfigData(false, ['Student'])));
renderComponent(true);
element = await screen.findByRole('alert');
});
test('Test Banner is visible on app load', async () => {
expect(element).toHaveTextContent(messages.bannerMessage.defaultMessage);
});
test('Test Banner do not have learn more button', async () => {
expect(element).not.toHaveTextContent(messages.learnMoreBannerLink.defaultMessage);
});
test('Test Banner has share feedback button', async () => {
expect(element).toHaveTextContent(messages.shareFeedback.defaultMessage);
});
});
describe('Information Banner moderators/staff/admin view', () => {
let element;
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
store.dispatch(fetchConfigSuccess(getConfigData(true, ['Student', 'Moderator'])));
renderComponent(true);
element = await screen.findByRole('alert');
});
test('Test Banner is visible on app load', async () => {
expect(element).toHaveTextContent(messages.bannerMessage.defaultMessage);
});
test('Test Banner has learn more button', async () => {
expect(element).toHaveTextContent(messages.learnMoreBannerLink.defaultMessage);
});
test('Test Banner has share feedback button', async () => {
expect(element).toHaveTextContent(messages.shareFeedback.defaultMessage);
});
});
describe('User is redirected according to url according to role', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
});
test('TAs are redirected to learners feedback form', async () => {
store.dispatch(fetchConfigSuccess(getConfigData(false, ['Student', 'Community TA'])));
renderComponent(true);
expect(screen.getByText(messages.shareFeedback.defaultMessage)
.closest('a'))
.toHaveAttribute('href', process.env.TA_FEEDBACK_FORM);
});
test('moderators/administrators are redirected to moderators feedback form', async () => {
store.dispatch(fetchConfigSuccess(getConfigData(false, ['Student', 'Moderator', 'Administrator'])));
renderComponent(true);
expect(screen.getByText(messages.shareFeedback.defaultMessage)
.closest('a'))
.toHaveAttribute('href', process.env.STAFF_FEEDBACK_FORM);
});
test('user with only isAdmin true are redirected to moderators feedback form', async () => {
store.dispatch(fetchConfigSuccess(getConfigData(true, ['Student'])));
renderComponent(true);
expect(screen.getByText(messages.shareFeedback.defaultMessage)
.closest('a'))
.toHaveAttribute('href', process.env.STAFF_FEEDBACK_FORM);
});
});

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

@@ -168,21 +168,6 @@ const messages = defineMessages({
defaultMessage: 'anonymous', defaultMessage: 'anonymous',
description: 'Author name displayed when a post is anonymous', description: 'Author name displayed when a post is anonymous',
}, },
bannerMessage: {
id: 'discussion.banner.welcomeMessage',
defaultMessage: '🎉 Welcome to the new and improved discussions experience!',
description: 'Information banner welcome text',
},
learnMoreBannerLink: {
id: 'discussion.banner.learnMore',
defaultMessage: 'Learn more',
description: 'learn more button to redirect users to know more about new discussion experience ',
},
shareFeedback: {
id: 'discussion.banner.shareFeedback',
defaultMessage: 'Share feedback',
description: 'Share feedback button to open feedback forms',
},
blackoutDiscussionInformation: { blackoutDiscussionInformation: {
id: 'discussion.blackoutBanner.information', id: 'discussion.blackoutBanner.information',
defaultMessage: 'Posting in discussions is temporarily disabled by the course team', defaultMessage: 'Posting in discussions is temporarily disabled by the course team',

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,
@@ -32,7 +32,7 @@ function CommentsView({
const handleDefinition = (message, commentsLength) => ( const handleDefinition = (message, commentsLength) => (
<div <div
className="mx-4 my-14px text-gray-700 font-style" className="comment-line mx-4 my-14px text-gray-700 font-style"
role="heading" role="heading"
aria-level="2" aria-level="2"
> >
@@ -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,

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