Compare commits
9 Commits
inf-392
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2abe4dfdff | ||
|
|
4a1e77bd13 | ||
|
|
dcb0f9e0ec | ||
|
|
5ca61b9480 | ||
|
|
e801fbb5cd | ||
|
|
5c6e40bc48 | ||
|
|
0e2bd9480d | ||
|
|
35541c9268 | ||
|
|
f13b34c6c7 |
5
.env
5
.env
@@ -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=''
|
||||
|
||||
@@ -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=''
|
||||
|
||||
@@ -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=''
|
||||
|
||||
14
.eslintrc.js
14
.eslintrc.js
@@ -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',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
process.env.TA_FEEDBACK_FORM= 'https://learner-form.test';
|
||||
process.env.STAFF_FEEDBACK_FORM= 'https://staff-form.test';
|
||||
@@ -1,9 +1,9 @@
|
||||
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.
|
||||
// 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
31338
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -75,7 +75,7 @@ function ActionsDropdown({
|
||||
placement="bottom-end"
|
||||
>
|
||||
<div
|
||||
className="bg-white p-1 shadow d-flex flex-column"
|
||||
className="bg-white shadow d-flex flex-column"
|
||||
data-testid="actions-dropdown-modal-popup"
|
||||
>
|
||||
{actions.map(action => (
|
||||
@@ -91,9 +91,15 @@ function ActionsDropdown({
|
||||
close();
|
||||
handleActions(action.action);
|
||||
}}
|
||||
className="d-flex justify-content-start py-1.5 mr-4"
|
||||
className="d-flex justify-content-start actions-dropdown-item"
|
||||
>
|
||||
<Icon src={action.icon} className="mr-1" /> {intl.formatMessage(action.label)}
|
||||
<Icon
|
||||
src={action.icon}
|
||||
className="icon-size-24"
|
||||
/>
|
||||
<span className="font-weight-normal font-xl ml-2">
|
||||
{intl.formatMessage(action.label)}
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
</React.Fragment>
|
||||
))}
|
||||
@@ -102,7 +108,7 @@ function ActionsDropdown({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ActionsDropdown.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' } });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = {
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user