Compare commits

...

8 Commits

Author SHA1 Message Date
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
116 changed files with 5246 additions and 27992 deletions

5
.env
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,9 @@ import { Close } from '@edx/paragon/icons';
import messages from '../discussions/posts/post-editor/messages';
import HTMLLoader from './HTMLLoader';
function PostPreviewPanel({
const PostPreviewPanel = ({
htmlNode, intl, isPost, editExisting,
}) {
}) => {
const [showPreviewPane, setShowPreviewPane] = useState(false);
return (
@@ -55,7 +55,7 @@ function PostPreviewPanel({
</div>
</>
);
}
};
PostPreviewPanel.propTypes = {
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 { 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 { setFilter as setTopicFilter } from '../discussions/topics/data/slices';
function Search({ intl }) {
const Search = ({ intl }) => {
const [previousSearchValue, setPreviousSearchValue] = useState('');
const dispatch = useDispatch();
const { page } = useContext(DiscussionContext);
const postSearch = useSelector(({ threads }) => threads.filters.search);
@@ -35,6 +36,7 @@ function Search({ intl }) {
dispatch(setSearchQuery(''));
dispatch(setTopicFilter(''));
dispatch(setUsernameSearch(''));
setPreviousSearchValue('');
};
const onChange = (query) => {
@@ -42,7 +44,7 @@ function Search({ intl }) {
};
const onSubmit = (query) => {
if (query === '') {
if (query === '' || query === previousSearchValue) {
return;
}
if (isPostSearch) {
@@ -52,33 +54,32 @@ function Search({ intl }) {
} else if (page === 'learners') {
dispatch(setUsernameSearch(query));
}
setPreviousSearchValue(query);
};
useEffect(() => onClear(), [page]);
return (
<>
<SearchField.Advanced
onClear={onClear}
onChange={onChange}
onSubmit={onSubmit}
value={currentValue}
>
<SearchField.Label />
<SearchField.Input
style={{ paddingRight: '1rem' }}
placeholder={intl.formatMessage(postsMessages.search, { page: camelCase(page) })}
<SearchField.Advanced
onClear={onClear}
onChange={onChange}
onSubmit={onSubmit}
value={currentValue}
>
<SearchField.Label />
<SearchField.Input
style={{ paddingRight: '1rem' }}
placeholder={intl.formatMessage(postsMessages.search, { page: camelCase(page) })}
/>
<span className="mt-auto mb-auto mr-2.5 pointer-cursor-hover">
<Icon
src={SearchIcon}
onClick={() => onSubmit(searchValue)}
data-testid="search-icon"
/>
<span className="mt-auto mb-auto mr-2.5 pointer-cursor-hover">
<Icon
src={SearchIcon}
onClick={() => onSubmit(searchValue)}
data-testid="search-icon"
/>
</span>
</SearchField.Advanced>
</>
</span>
</SearchField.Advanced>
);
}
};
Search.propTypes = {
intl: intlShape.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ import {
import Footer from '@edx/frontend-component-footer';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import { getConfig } from '@edx/frontend-platform';
import { PostActionsBar } from '../../components';
import { CourseTabsNavigation } from '../../components/NavigationBar';
@@ -30,9 +29,8 @@ import BlackoutInformationBanner from './BlackoutInformationBanner';
import DiscussionContent from './DiscussionContent';
import DiscussionSidebar from './DiscussionSidebar';
import useFeedbackWrapper from './FeedbackWrapper';
import InformationBanner from './InformationBanner';
export default function DiscussionsHome() {
const DiscussionsHome = () => {
const location = useLocation();
const postActionBarRef = useRef(null);
const postEditorVisible = useSelector(selectPostEditorVisible);
@@ -46,7 +44,6 @@ export default function DiscussionsHome() {
const isOnDesktop = useIsOnDesktop();
let displaySidebar = useSidebarVisible();
const enableInContextSidebar = Boolean(new URLSearchParams(location.search).get('inContextSidebar') !== null);
const isFeedbackBannerVisible = getConfig().DISPLAY_FEEDBACK_BANNER === 'true';
const {
courseId, postId, topicId, category, learnerUsername,
} = params;
@@ -67,6 +64,7 @@ export default function DiscussionsHome() {
}, [path]);
return (
// eslint-disable-next-line react/jsx-no-constructed-context-values
<DiscussionContext.Provider value={{
page,
courseId,
@@ -94,7 +92,6 @@ export default function DiscussionsHome() {
{!enableInContextSidebar && <Route path={Routes.DISCUSSIONS.PATH} component={NavigationBar} />}
<PostActionsBar />
</div>
{isFeedbackBannerVisible && <InformationBanner />}
<BlackoutInformationBanner />
</div>
{provider === DiscussionProvider.LEGACY && (
@@ -130,4 +127,6 @@ export default function DiscussionsHome() {
{!enableInContextSidebar && <Footer />}
</DiscussionContext.Provider>
);
}
};
export default React.memo(DiscussionsHome);

View File

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

View File

@@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { RequestStatus } from '../../data/constants';
@@ -22,9 +23,9 @@ export default function useFeedbackWrapper() {
useEffect(() => {
if (configStatus === RequestStatus.SUCCESSFUL) {
let url = '//w.usabilla.com/9e6036348fa1.js';
let url = getConfig().LEARNER_FEEDBACK_URL;
if (isStaff || isUserGroupTA || isCourseAdmin || isCourseStaff) {
url = '//w.usabilla.com/767740a06856.js';
url = getConfig().STAFF_FEEDBACK_URL;
}
try {
// 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 EmptyPage from './EmptyPage';
function EmptyLearners({ intl }) {
const EmptyLearners = ({ intl }) => {
const isOnDesktop = useIsOnDesktop();
if (!isOnDesktop) {
@@ -16,7 +16,7 @@ function EmptyLearners({ intl }) {
return (
<EmptyPage title={intl.formatMessage(messages.emptyTitle)} />
);
}
};
EmptyLearners.propTypes = {
intl: intlShape.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -168,21 +168,6 @@ const messages = defineMessages({
defaultMessage: '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: {
id: 'discussion.blackoutBanner.information',
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';
function BreadcrumbDropdown({
const BreadcrumbDropdown = ({
currentItem,
intl,
showAllPath,
@@ -17,7 +17,7 @@ function BreadcrumbDropdown({
itemLabelFunc,
itemActiveFunc,
itemFilterFunc,
}) {
}) => {
const showAllMsg = intl.formatMessage(messages.showAll);
return (
<DropdownButton
@@ -46,7 +46,7 @@ function BreadcrumbDropdown({
))}
</DropdownButton>
);
}
};
BreadcrumbDropdown.propTypes = {
// eslint-disable-next-line react/forbid-prop-types

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,13 +11,13 @@ import { usePostComments } from '../data/hooks';
import messages from '../messages';
import { Comment, ResponseEditor } from './comment';
function CommentsView({
const CommentsView = ({
postType,
postId,
intl,
endorsed,
isClosed,
}) {
}) => {
const {
comments,
hasMorePages,
@@ -71,51 +71,49 @@ function CommentsView({
);
return (
((hasMorePages && isLoading) || !isLoading) && (
<>
{((hasMorePages && isLoading) || !isLoading) && (
<>
{endorsedComments.length > 0 && (
<>
{handleDefinition(messages.endorsedResponseCount, endorsedComments.length)}
{endorsed === EndorsementStatus.DISCUSSION
? handleComments(endorsedComments, true)
: handleComments(endorsedComments, false)}
</>
)}
{endorsed !== EndorsementStatus.ENDORSED && (
<>
{handleDefinition(messages.responseCount, unEndorsedComments.length)}
{unEndorsedComments.length === 0 && <br />}
{handleComments(unEndorsedComments, false)}
{(userCanAddThreadInBlackoutDate && !!unEndorsedComments.length && !isClosed) && (
<div className="mx-4">
{!addingResponse && (
<Button
variant="plain"
block="true"
className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
{endorsedComments.length > 0 && (
<>
{handleDefinition(messages.endorsedResponseCount, endorsedComments.length)}
{endorsed === EndorsementStatus.DISCUSSION
? handleComments(endorsedComments, true)
: handleComments(endorsedComments, false)}
</>
)}
{endorsed !== EndorsementStatus.ENDORSED && (
<>
{handleDefinition(messages.responseCount, unEndorsedComments.length)}
{unEndorsedComments.length === 0 && <br />}
{handleComments(unEndorsedComments, false)}
{(userCanAddThreadInBlackoutDate && !!unEndorsedComments.length && !isClosed) && (
<div className="mx-4">
{!addingResponse && (
<Button
variant="plain"
block="true"
className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
line-height-24 font-size-14 text-primary-500"
onClick={() => setAddingResponse(true)}
data-testid="add-response"
>
{intl.formatMessage(messages.addResponse)}
</Button>
)}
<ResponseEditor
postId={postId}
handleCloseEditor={() => setAddingResponse(false)}
addWrappingDiv
addingResponse={addingResponse}
/>
</div>
)}
</>
onClick={() => setAddingResponse(true)}
data-testid="add-response"
>
{intl.formatMessage(messages.addResponse)}
</Button>
)}
</>
<ResponseEditor
postId={postId}
handleCloseEditor={() => setAddingResponse(false)}
addWrappingDiv
addingResponse={addingResponse}
/>
</div>
)}
</>
)}
</>
)
);
}
};
CommentsView.propTypes = {
postId: PropTypes.string.isRequired,

View File

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

View File

@@ -24,13 +24,13 @@ import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
import { addComment, editComment } from '../../data/thunks';
import messages from '../../messages';
function CommentEditor({
const CommentEditor = ({
intl,
comment,
onCloseEditor,
edit,
formClasses,
}) {
}) => {
const editorRef = useRef(null);
const { authenticatedUser } = useContext(AppContext);
const { enableInContextSidebar } = useContext(DiscussionContext);
@@ -57,7 +57,10 @@ function CommentEditor({
const initialValues = {
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) => {
@@ -173,7 +176,7 @@ function CommentEditor({
)}
</Formik>
);
}
};
CommentEditor.propTypes = {
comment: PropTypes.shape({
@@ -182,7 +185,7 @@ CommentEditor.propTypes = {
parentId: PropTypes.string,
rawBody: PropTypes.string,
author: PropTypes.string,
lastEdit: PropTypes.object,
lastEdit: PropTypes.shape({}),
}).isRequired,
onCloseEditor: PropTypes.func.isRequired,
intl: intlShape.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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