Compare commits
3 Commits
release/te
...
abdullahwa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a54819ca2 | ||
|
|
0e2bd9480d | ||
|
|
35541c9268 |
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
|
||||
|
||||
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,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { setSearchQuery } from '../discussions/posts/data';
|
||||
import postsMessages from '../discussions/posts/post-actions-bar/messages';
|
||||
import { setFilter as setTopicFilter } from '../discussions/topics/data/slices';
|
||||
|
||||
function Search({ intl }) {
|
||||
const Search = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { page } = useContext(DiscussionContext);
|
||||
const postSearch = useSelector(({ threads }) => threads.filters.search);
|
||||
@@ -56,29 +56,27 @@ function Search({ intl }) {
|
||||
|
||||
useEffect(() => onClear(), [page]);
|
||||
return (
|
||||
<>
|
||||
<SearchField.Advanced
|
||||
onClear={onClear}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
value={currentValue}
|
||||
>
|
||||
<SearchField.Label />
|
||||
<SearchField.Input
|
||||
style={{ paddingRight: '1rem' }}
|
||||
placeholder={intl.formatMessage(postsMessages.search, { page: camelCase(page) })}
|
||||
<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);
|
||||
@@ -108,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;
|
||||
|
||||
@@ -32,7 +32,7 @@ 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);
|
||||
@@ -67,6 +67,7 @@ export default function DiscussionsHome() {
|
||||
}, [path]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
<DiscussionContext.Provider value={{
|
||||
page,
|
||||
courseId,
|
||||
@@ -130,4 +131,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, {
|
||||
|
||||
@@ -8,9 +8,9 @@ import { Hyperlink, PageBanner } from '@edx/paragon';
|
||||
import { selectUserIsStaff, selectUserRoles } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
|
||||
function InformationBanner({
|
||||
const InformationBanner = ({
|
||||
intl,
|
||||
}) {
|
||||
}) => {
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
const userRoles = useSelector(selectUserRoles);
|
||||
const isAdmin = useSelector(selectUserIsStaff);
|
||||
@@ -55,7 +55,7 @@ function InformationBanner({
|
||||
</div>
|
||||
</PageBanner>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
InformationBanner.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,6 +57,7 @@ function CommentEditor({
|
||||
|
||||
const initialValues = {
|
||||
comment: comment.rawBody,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
editReasonCode: comment?.lastEdit?.reasonCode || (userIsStaff ? 'violates-guidelines' : ''),
|
||||
};
|
||||
|
||||
@@ -173,7 +174,7 @@ function CommentEditor({
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CommentEditor.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
@@ -182,7 +183,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,
|
||||
|
||||
@@ -61,9 +61,9 @@ ActionItem.propTypes = {
|
||||
selected: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function PostFilterBar({
|
||||
const PostFilterBar = ({
|
||||
intl,
|
||||
}) {
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { courseId } = useParams();
|
||||
const { page } = useContext(DiscussionContext);
|
||||
@@ -75,9 +75,11 @@ function PostFilterBar({
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const selectedCohort = useMemo(() => cohorts.find(cohort => (
|
||||
toString(cohort.id) === currentFilters.cohort)),
|
||||
[currentFilters.cohort]);
|
||||
const selectedCohort = useMemo(
|
||||
() => cohorts.find(cohort => (
|
||||
toString(cohort.id) === currentFilters.cohort)),
|
||||
[currentFilters.cohort],
|
||||
);
|
||||
|
||||
const handleSortFilterChange = (event) => {
|
||||
const currentType = currentFilters.postType;
|
||||
@@ -299,7 +301,7 @@ function PostFilterBar({
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PostFilterBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
import { selectModerationSettings } from '../../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
function ClosePostReasonModal({
|
||||
const ClosePostReasonModal = ({
|
||||
intl,
|
||||
isOpen,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}) {
|
||||
}) => {
|
||||
const scrollTo = useRef(null);
|
||||
const [reasonCode, setReasonCode] = useState(null);
|
||||
|
||||
@@ -87,7 +87,7 @@ function ClosePostReasonModal({
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ClosePostReasonModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
import { ThumbUpFilled, ThumbUpOutline } from '../../../components/icons';
|
||||
import messages from './messages';
|
||||
|
||||
function LikeButton({
|
||||
const LikeButton = ({
|
||||
count,
|
||||
intl,
|
||||
onClick,
|
||||
voted,
|
||||
}) {
|
||||
}) => {
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
if (onClick) {
|
||||
@@ -47,7 +47,7 @@ function LikeButton({
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LikeButton.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
|
||||
@@ -24,11 +24,11 @@ import PostFooter from './PostFooter';
|
||||
import PostHeader from './PostHeader';
|
||||
import { postShape } from './proptypes';
|
||||
|
||||
function Post({
|
||||
const Post = ({
|
||||
post,
|
||||
intl,
|
||||
handleAddResponseButton,
|
||||
}) {
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
@@ -67,6 +67,11 @@ function Post({
|
||||
hideReportConfirmation();
|
||||
};
|
||||
|
||||
const handlePostCopyLink = useCallback(() => {
|
||||
const postURL = new URL(`${getConfig().PUBLIC_PATH}${courseId}/posts/${post.id}`, window.location.origin);
|
||||
navigator.clipboard.writeText(postURL.href);
|
||||
}, [window.location.origin, post.id, courseId]);
|
||||
|
||||
const actionHandlers = useMemo(() => ({
|
||||
[ContentActions.EDIT_CONTENT]: () => history.push({
|
||||
...location,
|
||||
@@ -82,7 +87,7 @@ function Post({
|
||||
dispatch(updateExistingThread(post.id, { closed: true }));
|
||||
}
|
||||
},
|
||||
[ContentActions.COPY_LINK]: () => { navigator.clipboard.writeText(`${window.location.origin}/${courseId}/posts/${post.id}`); },
|
||||
[ContentActions.COPY_LINK]: handlePostCopyLink,
|
||||
[ContentActions.PIN]: () => dispatch(updateExistingThread(post.id, { pinned: !post.pinned })),
|
||||
[ContentActions.REPORT]: () => handleAbusedFlag(),
|
||||
}), [
|
||||
@@ -149,8 +154,10 @@ function Post({
|
||||
</div>
|
||||
{(topicContext || topic) && (
|
||||
<div
|
||||
className={classNames('mt-14px font-style font-size-12',
|
||||
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter })}
|
||||
className={classNames(
|
||||
'mt-14px font-style font-size-12',
|
||||
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter },
|
||||
)}
|
||||
style={{ lineHeight: '20px' }}
|
||||
>
|
||||
<span className="text-gray-500" style={{ lineHeight: '20px' }}>{intl.formatMessage(messages.relatedTo)}{' '}</span>
|
||||
@@ -183,7 +190,7 @@ function Post({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Post.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -15,11 +15,11 @@ import LikeButton from './LikeButton';
|
||||
import messages from './messages';
|
||||
import { postShape } from './proptypes';
|
||||
|
||||
function PostFooter({
|
||||
const PostFooter = ({
|
||||
intl,
|
||||
post,
|
||||
userHasModerationPrivileges,
|
||||
}) {
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<div className="d-flex align-items-center ml-n1.5 mt-10px" style={{ height: '32px' }} data-testid="post-footer">
|
||||
@@ -95,7 +95,7 @@ function PostFooter({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PostFooter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -13,9 +13,9 @@ import { useAlertBannerVisible } from '../../data/hooks';
|
||||
import messages from './messages';
|
||||
import { postShape } from './proptypes';
|
||||
|
||||
export function PostAvatar({
|
||||
export const PostAvatar = ({
|
||||
post, authorLabel, fromPostLink, read,
|
||||
}) {
|
||||
}) => {
|
||||
const outlineColor = AvatarOutlineAndLabelColors[authorLabel];
|
||||
|
||||
const avatarSize = useMemo(() => {
|
||||
@@ -63,7 +63,7 @@ export function PostAvatar({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PostAvatar.propTypes = {
|
||||
post: postShape.isRequired,
|
||||
@@ -78,11 +78,11 @@ PostAvatar.defaultProps = {
|
||||
read: false,
|
||||
};
|
||||
|
||||
function PostHeader({
|
||||
const PostHeader = ({
|
||||
intl,
|
||||
post,
|
||||
preview,
|
||||
}) {
|
||||
}) => {
|
||||
const showAnsweredBadge = preview && post.hasEndorsed && post.type === ThreadType.QUESTION;
|
||||
const authorLabelColor = AvatarOutlineAndLabelColors[post.authorLabel];
|
||||
const hasAnyAlert = useAlertBannerVisible(post);
|
||||
@@ -127,7 +127,7 @@ function PostHeader({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PostHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -19,12 +19,12 @@ import { PostAvatar } from './PostHeader';
|
||||
import PostSummaryFooter from './PostSummaryFooter';
|
||||
import { postShape } from './proptypes';
|
||||
|
||||
function PostLink({
|
||||
const PostLink = ({
|
||||
post,
|
||||
isSelected,
|
||||
showDivider,
|
||||
idx,
|
||||
}) {
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
page,
|
||||
@@ -47,87 +47,89 @@ function PostLink({
|
||||
const read = post.read || (!post.read && post.commentCount !== post.unreadCommentCount);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
className={
|
||||
<Link
|
||||
className={
|
||||
classNames('discussion-post p-0 text-decoration-none text-gray-900', {
|
||||
'border-bottom border-light-400': showDivider,
|
||||
})
|
||||
}
|
||||
to={linkUrl}
|
||||
onClick={() => isSelected(post.id)}
|
||||
aria-current={isSelected(post.id) ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(isSelected(post.id) || idx === 0) ? 0 : -1}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
classNames('d-flex flex-row pt-2 pb-2 px-4 border-primary-500 position-relative',
|
||||
to={linkUrl}
|
||||
onClick={() => isSelected(post.id)}
|
||||
aria-current={isSelected(post.id) ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(isSelected(post.id) || idx === 0) ? 0 : -1}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
classNames(
|
||||
'd-flex flex-row pt-2 pb-2 px-4 border-primary-500 position-relative',
|
||||
{ 'bg-light-300': read },
|
||||
{ 'post-summary-card-selected': post.id === postId })
|
||||
{ 'post-summary-card-selected': post.id === postId },
|
||||
)
|
||||
}
|
||||
>
|
||||
<PostAvatar post={post} authorLabel={post.authorLabel} fromPostLink read={read} />
|
||||
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill" style={{ marginBottom: '-3px' }}>
|
||||
<div className="d-flex align-items-center pb-0 mb-0 flex-fill font-weight-500">
|
||||
<Truncate lines={1} className="mr-1.5" whiteSpace>
|
||||
<span
|
||||
class={
|
||||
classNames('font-weight-500 font-size-14 text-primary-500 font-style align-bottom',
|
||||
{ 'font-weight-bolder': !read })
|
||||
>
|
||||
<PostAvatar post={post} authorLabel={post.authorLabel} fromPostLink read={read} />
|
||||
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill" style={{ marginBottom: '-3px' }}>
|
||||
<div className="d-flex align-items-center pb-0 mb-0 flex-fill font-weight-500">
|
||||
<Truncate lines={1} className="mr-1.5" whiteSpace>
|
||||
<span
|
||||
class={
|
||||
classNames(
|
||||
'font-weight-500 font-size-14 text-primary-500 font-style align-bottom',
|
||||
{ 'font-weight-bolder': !read },
|
||||
)
|
||||
}
|
||||
>
|
||||
{post.title}
|
||||
</span>
|
||||
<span class="align-bottom"> </span>
|
||||
<span
|
||||
class="text-gray-700 font-weight-normal font-size-14 font-style align-bottom"
|
||||
>
|
||||
{isPostPreviewAvailable(post.previewBody)
|
||||
? post.previewBody
|
||||
: intl.formatMessage(messages.postWithoutPreview)}
|
||||
</span>
|
||||
</Truncate>
|
||||
{showAnsweredBadge && (
|
||||
<Icon src={CheckCircle} className="text-success font-weight-500 ml-auto badge-padding" data-testid="check-icon">
|
||||
<span className="sr-only">{' '}answered</span>
|
||||
</Icon>
|
||||
)}
|
||||
>
|
||||
{post.title}
|
||||
</span>
|
||||
<span class="align-bottom"> </span>
|
||||
<span
|
||||
class="text-gray-700 font-weight-normal font-size-14 font-style align-bottom"
|
||||
>
|
||||
{isPostPreviewAvailable(post.previewBody)
|
||||
? post.previewBody
|
||||
: intl.formatMessage(messages.postWithoutPreview)}
|
||||
</span>
|
||||
</Truncate>
|
||||
{showAnsweredBadge && (
|
||||
<Icon src={CheckCircle} className="text-success font-weight-500 ml-auto badge-padding" data-testid="check-icon">
|
||||
<span className="sr-only">{' '}answered</span>
|
||||
</Icon>
|
||||
)}
|
||||
|
||||
{canSeeReportedBadge && (
|
||||
<Badge
|
||||
variant="danger"
|
||||
data-testid="reported-post"
|
||||
className={`font-weight-500 badge-padding ${showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
>
|
||||
{intl.formatMessage(messages.contentReported)}
|
||||
<span className="sr-only">{' '}reported</span>
|
||||
</Badge>
|
||||
)}
|
||||
{canSeeReportedBadge && (
|
||||
<Badge
|
||||
variant="danger"
|
||||
data-testid="reported-post"
|
||||
className={`font-weight-500 badge-padding ${showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
>
|
||||
{intl.formatMessage(messages.contentReported)}
|
||||
<span className="sr-only">{' '}reported</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{post.pinned && (
|
||||
<Icon
|
||||
src={PushPin}
|
||||
className={`post-summary-icons-dimensions text-gray-700
|
||||
{post.pinned && (
|
||||
<Icon
|
||||
src={PushPin}
|
||||
className={`post-summary-icons-dimensions text-gray-700
|
||||
${canSeeReportedBadge || showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<AuthorLabel
|
||||
author={post.author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={post.authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
/>
|
||||
<PostSummaryFooter post={post} preview showNewCountLabel={read} />
|
||||
</div>
|
||||
<AuthorLabel
|
||||
author={post.author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={post.authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
/>
|
||||
<PostSummaryFooter post={post} preview showNewCountLabel={read} />
|
||||
</div>
|
||||
{!showDivider && post.pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
</>
|
||||
</div>
|
||||
{!showDivider && post.pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PostLink.propTypes = {
|
||||
post: postShape.isRequired,
|
||||
|
||||
@@ -18,12 +18,12 @@ import { selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import messages from './messages';
|
||||
import { postShape } from './proptypes';
|
||||
|
||||
function PostSummaryFooter({
|
||||
const PostSummaryFooter = ({
|
||||
post,
|
||||
intl,
|
||||
preview,
|
||||
showNewCountLabel,
|
||||
}) {
|
||||
}) => {
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
timeago.register('time-locale', timeLocale);
|
||||
return (
|
||||
@@ -104,7 +104,7 @@ function PostSummaryFooter({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PostSummaryFooter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user