build: edx namespace packages upgrade & resolved respective eslint issue (#508)
* refactor: updated frontend-build, frontend-platform, header & footer packages * fix: resolved eslint issues post frontend-build upgrade * refactor: resolved eslint issues * refactor: pinned frontend-build & changed suggested function definitions
This commit is contained in:
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',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
15550
package-lock.json
generated
15550
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": "11.6.3",
|
||||
"@edx/frontend-component-header": "3.6.4",
|
||||
"@edx/frontend-platform": "3.4.1",
|
||||
"@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.4.15",
|
||||
"@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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -42,7 +42,7 @@ import contentCss from '!!raw-loader!tinymce/skins/content/default/content.min.c
|
||||
import contentUiCss from '!!raw-loader!tinymce/skins/ui/oxide/content.min.css';
|
||||
|
||||
/* istanbul ignore next */
|
||||
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
|
||||
|
||||
@@ -152,6 +152,6 @@ function TinyMCEEditor(props) {
|
||||
</AlertModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default React.memo(TinyMCEEditor);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function InsertLink() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function Issue() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function People() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function PushPin() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function Question() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function QuestionAnswer() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function QuestionAnswerOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function StarFilled() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function StarOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function ThumbUpFilled() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function ThumbUpOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -17,14 +17,14 @@ import { selectBlackoutDate } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { inBlackoutDateRange, useActions } from '../utils';
|
||||
|
||||
function ActionsDropdown({
|
||||
const ActionsDropdown = ({
|
||||
actionHandlers,
|
||||
contentType,
|
||||
disabled,
|
||||
dropDownIconSize,
|
||||
iconSize,
|
||||
id,
|
||||
}) {
|
||||
}) => {
|
||||
const buttonRef = useRef();
|
||||
const intl = useIntl();
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
@@ -109,7 +109,7 @@ function ActionsDropdown({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ActionsDropdown.propTypes = {
|
||||
id: PropTypes.string.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 = ({
|
||||
isOpen,
|
||||
title,
|
||||
description,
|
||||
@@ -15,7 +15,7 @@ function Confirmation({
|
||||
closeButtonVaraint,
|
||||
confirmButtonVariant,
|
||||
confirmButtonText,
|
||||
}) {
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
@@ -40,7 +40,7 @@ function Confirmation({
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Confirmation.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -13,12 +13,12 @@ import { PostCommentsContext } from '../post-comments/postCommentsContext';
|
||||
import AuthorLabel from './AuthorLabel';
|
||||
import timeLocale from './time-locale';
|
||||
|
||||
function EndorsedAlertBanner({
|
||||
const EndorsedAlertBanner = ({
|
||||
endorsed,
|
||||
endorsedAt,
|
||||
endorsedBy,
|
||||
endorsedByLabel,
|
||||
}) {
|
||||
}) => {
|
||||
timeago.register('time-locale', timeLocale);
|
||||
|
||||
const intl = useIntl();
|
||||
@@ -62,7 +62,7 @@ function EndorsedAlertBanner({
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
EndorsedAlertBanner.propTypes = {
|
||||
endorsed: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -22,9 +22,7 @@ function buildTestContent(type, buildParams) {
|
||||
return camelCaseObject(Factory.build(type, { ...buildParamsSnakeCase }, null));
|
||||
}
|
||||
|
||||
function renderComponent(
|
||||
content, postType,
|
||||
) {
|
||||
const renderComponent = (content, postType) => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
@@ -47,7 +45,7 @@ function renderComponent(
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
describe.each([
|
||||
{
|
||||
|
||||
@@ -43,8 +43,10 @@ const 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={isClosed}
|
||||
style={{ lineHeight: '20px' }}
|
||||
@@ -125,6 +127,7 @@ HoverCard.propTypes = {
|
||||
addResponseCommentButtonMessage: PropTypes.string.isRequired,
|
||||
onLike: PropTypes.func.isRequired,
|
||||
voted: PropTypes.bool.isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
endorseIcons: PropTypes.objectOf(PropTypes.any),
|
||||
onFollow: PropTypes.func,
|
||||
following: PropTypes.bool,
|
||||
|
||||
@@ -75,13 +75,15 @@ async function mockAxiosReturnPagedCommentsResponses() {
|
||||
};
|
||||
|
||||
[1, 2].forEach(async (page) => {
|
||||
axiosMock.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } }).reply(200,
|
||||
axiosMock.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } }).reply(
|
||||
200,
|
||||
Factory.build('commentsResult', null, {
|
||||
parentId,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
await executeThunk(fetchCommentResponses(parentId), store.dispatch, store.getState);
|
||||
});
|
||||
|
||||
@@ -40,12 +40,14 @@ import { fetchCourseConfig } from './thunks';
|
||||
|
||||
export function useTotalTopicThreadCount() {
|
||||
const topics = useSelector(selectTopics);
|
||||
const count = useMemo(() => (
|
||||
Object.keys(topics)?.reduce((total, topicId) => {
|
||||
const topic = topics[topicId];
|
||||
return total + topic.threadCounts.discussion + topic.threadCounts.question;
|
||||
}, 0)),
|
||||
[]);
|
||||
const count = useMemo(
|
||||
() => (
|
||||
Object.keys(topics)?.reduce((total, topicId) => {
|
||||
const topic = topics[topicId];
|
||||
return total + topic.threadCounts.discussion + topic.threadCounts.question;
|
||||
}, 0)),
|
||||
[],
|
||||
);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React, { lazy, Suspense, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable no-unused-vars, react/forbid-prop-types */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -110,7 +110,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();
|
||||
|
||||
@@ -122,7 +123,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();
|
||||
|
||||
@@ -66,6 +66,7 @@ const LearnersView = () => {
|
||||
courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && learnersTabEnabled && learners.map((learner) => (
|
||||
<LearnerCard learner={learner} key={learner.username} />
|
||||
))
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
) || <></>
|
||||
), [courseConfigLoadingStatus, learnersTabEnabled, learners]);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable default-param-last */
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
@@ -201,7 +202,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,
|
||||
}) => {
|
||||
@@ -227,7 +229,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();
|
||||
@@ -257,7 +260,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);
|
||||
@@ -274,7 +278,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;
|
||||
|
||||
@@ -74,6 +74,7 @@ const PostCommentsView = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
<PostCommentsContext.Provider value={{
|
||||
isClosed: closed,
|
||||
postType: type,
|
||||
|
||||
@@ -90,13 +90,15 @@ async function mockAxiosReturnPagedCommentsResponses() {
|
||||
};
|
||||
|
||||
[1, 2].forEach(async (page) => {
|
||||
axiosMock.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } }).reply(200,
|
||||
axiosMock.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } }).reply(
|
||||
200,
|
||||
Factory.build('commentsResult', null, {
|
||||
parentId,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
await executeThunk(fetchCommentResponses(parentId), store.dispatch, store.getState);
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
), [hasMorePages, isLoading, handleLoadMoreResponses]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{((hasMorePages && isLoading) || !isLoading) && (
|
||||
<>
|
||||
|
||||
@@ -24,12 +24,12 @@ import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
|
||||
import { addComment, editComment } from '../../data/thunks';
|
||||
import messages from '../../messages';
|
||||
|
||||
function CommentEditor({
|
||||
const CommentEditor = ({
|
||||
comment,
|
||||
edit,
|
||||
formClasses,
|
||||
onCloseEditor,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
id, threadId, parentId, rawBody, author, lastEdit,
|
||||
} = comment;
|
||||
@@ -60,6 +60,7 @@ function CommentEditor({
|
||||
|
||||
const initialValues = {
|
||||
comment: rawBody,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
editReasonCode: lastEdit?.reasonCode || (userIsStaff ? 'violates-guidelines' : ''),
|
||||
};
|
||||
|
||||
@@ -176,13 +177,13 @@ function CommentEditor({
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CommentEditor.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
author: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
lastEdit: PropTypes.object,
|
||||
lastEdit: PropTypes.shape({}),
|
||||
parentId: PropTypes.string,
|
||||
rawBody: PropTypes.string,
|
||||
threadId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -20,16 +20,14 @@ export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussi
|
||||
* @param enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getThreadComments(
|
||||
threadId, {
|
||||
endorsed,
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
enableInContextSidebar = false,
|
||||
signal,
|
||||
} = {},
|
||||
) {
|
||||
export const getThreadComments = async (threadId, {
|
||||
endorsed,
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
enableInContextSidebar = false,
|
||||
signal,
|
||||
} = {}) => {
|
||||
const params = snakeCaseObject({
|
||||
threadId,
|
||||
endorsed: EndorsementValue[endorsed],
|
||||
@@ -42,7 +40,7 @@ export async function getThreadComments(
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient().get(getCommentsApiUrl(), { params: { ...params, signal } });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a responses to a comment.
|
||||
@@ -51,13 +49,11 @@ export async function getThreadComments(
|
||||
* @param {number=} pageSize
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getCommentResponses(
|
||||
commentId, {
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
} = {},
|
||||
) {
|
||||
export const getCommentResponses = async (commentId, {
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
} = {}) => {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const params = snakeCaseObject({
|
||||
page,
|
||||
@@ -68,7 +64,7 @@ export async function getCommentResponses(
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(url, { params });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a comment.
|
||||
@@ -78,13 +74,13 @@ export async function getCommentResponses(
|
||||
* @param {boolean} enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function postComment(comment, threadId, parentId = null, enableInContextSidebar = false) {
|
||||
export const postComment = async (comment, threadId, parentId = null, enableInContextSidebar = false) => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getCommentsApiUrl(), snakeCaseObject({
|
||||
threadId, raw_body: comment, parentId, enableInContextSidebar,
|
||||
}));
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates existing comment.
|
||||
@@ -96,13 +92,13 @@ export async function postComment(comment, threadId, parentId = null, enableInCo
|
||||
* @param {string=} editReasonCode The moderation reason code for editing.
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function updateComment(commentId, {
|
||||
export const updateComment = async (commentId, {
|
||||
comment,
|
||||
voted,
|
||||
flagged,
|
||||
endorsed,
|
||||
editReasonCode,
|
||||
}) {
|
||||
}) => {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const postData = snakeCaseObject({
|
||||
raw_body: comment,
|
||||
@@ -115,14 +111,14 @@ export async function updateComment(commentId, {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(url, postData, { headers: { 'Content-Type': 'application/merge-patch+json' } });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes existing comment.
|
||||
* @param {string} commentId ID of comment to delete
|
||||
*/
|
||||
export async function deleteComment(commentId) {
|
||||
export const deleteComment = async (commentId) => {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 const getThreads = async (courseId, {
|
||||
topicIds,
|
||||
page,
|
||||
pageSize,
|
||||
textSearch,
|
||||
orderBy,
|
||||
following,
|
||||
view,
|
||||
author,
|
||||
flagged,
|
||||
threadType,
|
||||
countFlagged,
|
||||
cohort,
|
||||
} = {}) => {
|
||||
const params = snakeCaseObject({
|
||||
courseId,
|
||||
page,
|
||||
@@ -62,19 +60,19 @@ export async function getThreads(
|
||||
});
|
||||
const { data } = await getAuthenticatedHttpClient().get(getThreadsApiUrl(), { params });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a single thread.
|
||||
* @param {string} threadId
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getThread(threadId, courseId) {
|
||||
export const getThread = async (threadId, courseId) => {
|
||||
const params = { requested_fields: 'profile_image', course_id: courseId };
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url, { params });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a new thread.
|
||||
@@ -90,7 +88,7 @@ export async function getThread(threadId, courseId) {
|
||||
* @param {boolean} enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function postThread(
|
||||
export const postThread = async (
|
||||
courseId,
|
||||
topicId,
|
||||
type,
|
||||
@@ -103,7 +101,7 @@ export async function postThread(
|
||||
anonymousToPeers,
|
||||
} = {},
|
||||
enableInContextSidebar = false,
|
||||
) {
|
||||
) => {
|
||||
const postData = snakeCaseObject({
|
||||
courseId,
|
||||
topicId,
|
||||
@@ -119,7 +117,7 @@ export async function postThread(
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getThreadsApiUrl(), postData);
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an existing thread.
|
||||
@@ -138,7 +136,7 @@ export async function postThread(
|
||||
* @param {string} closeReasonCode
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function updateThread(threadId, {
|
||||
export const updateThread = async (threadId, {
|
||||
flagged,
|
||||
voted,
|
||||
read,
|
||||
@@ -151,7 +149,7 @@ export async function updateThread(threadId, {
|
||||
pinned,
|
||||
editReasonCode,
|
||||
closeReasonCode,
|
||||
} = {}) {
|
||||
} = {}) => {
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
const patchData = snakeCaseObject({
|
||||
topicId,
|
||||
@@ -170,17 +168,17 @@ export async function updateThread(threadId, {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(url, patchData, { headers: { 'Content-Type': 'application/merge-patch+json' } });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a thread.
|
||||
* @param {string} threadId
|
||||
*/
|
||||
export async function deleteThread(threadId) {
|
||||
export const deleteThread = async (threadId) => {
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a file.
|
||||
@@ -190,7 +188,7 @@ export async function deleteThread(threadId) {
|
||||
* @param {string} threadKey
|
||||
* @returns {Promise<{ location: string }>}
|
||||
*/
|
||||
export async function uploadFile(blob, filename, courseId, threadKey) {
|
||||
export const uploadFile = async (blob, filename, courseId, threadKey) => {
|
||||
const uploadUrl = `${getCoursesApiUrl()}${courseId}/upload`;
|
||||
const formData = new FormData();
|
||||
formData.append('thread_key', threadKey);
|
||||
@@ -200,4 +198,4 @@ export async function uploadFile(blob, filename, courseId, threadKey) {
|
||||
throw new Error(data.developer_message);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,8 +54,10 @@ const 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={handleAddPost}
|
||||
size={enableInContextSidebar ? 'md' : 'sm'}
|
||||
>
|
||||
|
||||
@@ -131,9 +131,11 @@ const PostEditor = ({
|
||||
}, [postId, topicId, post?.author, category, editExisting, commentsPagePath, location]);
|
||||
|
||||
// null stands for no cohort restriction ("All learners" option)
|
||||
const selectedCohort = useCallback((cohort) => (
|
||||
cohort === 'default' ? null : cohort),
|
||||
[]);
|
||||
const selectedCohort = useCallback(
|
||||
(cohort) => (
|
||||
cohort === 'default' ? null : cohort),
|
||||
[],
|
||||
);
|
||||
|
||||
const submitForm = useCallback(async (values, { resetForm }) => {
|
||||
if (editExisting) {
|
||||
@@ -288,18 +290,18 @@ const PostEditor = ({
|
||||
{enableInContext ? (
|
||||
<>
|
||||
{coursewareTopics?.map(section => (
|
||||
section?.children?.map(subsection => (
|
||||
<optgroup
|
||||
label={handleInContextSelectLabel(section, subsection)}
|
||||
key={subsection.id}
|
||||
>
|
||||
{subsection?.children?.map(unit => (
|
||||
<option key={unit.id} value={unit.id}>
|
||||
{unit.name || intl.formatMessage(messages.unnamedSubTopics)}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))
|
||||
section?.children?.map(subsection => (
|
||||
<optgroup
|
||||
label={handleInContextSelectLabel(section, subsection)}
|
||||
key={subsection.id}
|
||||
>
|
||||
{subsection?.children?.map(unit => (
|
||||
<option key={unit.id} value={unit.id}>
|
||||
{unit.name || intl.formatMessage(messages.unnamedSubTopics)}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))
|
||||
))}
|
||||
{(userIsStaff || userIsGroupTa || userHasModerationPrivileges) && (
|
||||
<optgroup label={intl.formatMessage(messages.archivedTopics)}>
|
||||
|
||||
@@ -81,9 +81,9 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
`${window.location.origin}/${courseId}/posts/${postId}`,
|
||||
), [window.location.origin, postId, courseId]);
|
||||
|
||||
const handlePostPin = useCallback(() => dispatch(updateExistingThread(
|
||||
postId, { pinned: !pinned },
|
||||
)), [postId, pinned]);
|
||||
const handlePostPin = useCallback(() => dispatch(
|
||||
updateExistingThread(postId, { pinned: !pinned }),
|
||||
), [postId, pinned]);
|
||||
|
||||
const handlePostReport = useCallback(() => {
|
||||
if (abuseFlagged) {
|
||||
@@ -188,8 +188,10 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
</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' }}>
|
||||
|
||||
@@ -51,105 +51,109 @@ const PostLink = ({
|
||||
const canSeeReportedBadge = abuseFlagged || abuseFlaggedCount;
|
||||
const isPostRead = read || (!read && commentCount !== unreadCommentCount);
|
||||
|
||||
const checkIsSelected = useMemo(() => (
|
||||
window.location.pathname.includes(postId)),
|
||||
[window.location.pathname]);
|
||||
const checkIsSelected = useMemo(
|
||||
() => (
|
||||
window.location.pathname.includes(postId)),
|
||||
[window.location.pathname],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
className={
|
||||
<Link
|
||||
className={
|
||||
classNames('discussion-post p-0 text-decoration-none text-gray-900', {
|
||||
'border-bottom border-light-400': showDivider,
|
||||
})
|
||||
}
|
||||
to={linkUrl}
|
||||
aria-current={checkIsSelected ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(checkIsSelected || idx === 0) ? 0 : -1}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
classNames('d-flex flex-row pt-2 pb-2 px-4 border-primary-500 position-relative',
|
||||
to={linkUrl}
|
||||
aria-current={checkIsSelected ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(checkIsSelected || 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': isPostRead },
|
||||
{ 'post-summary-card-selected': id === selectedPostId })
|
||||
{ 'post-summary-card-selected': id === selectedPostId },
|
||||
)
|
||||
}
|
||||
>
|
||||
<PostAvatar
|
||||
postType={type}
|
||||
author={author}
|
||||
authorLabel={authorLabel}
|
||||
fromPostLink
|
||||
read={isPostRead}
|
||||
/>
|
||||
<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
|
||||
postType={type}
|
||||
author={author}
|
||||
authorLabel={authorLabel}
|
||||
fromPostLink
|
||||
read={isPostRead}
|
||||
/>
|
||||
<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 },
|
||||
)
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span class="align-bottom"> </span>
|
||||
<span
|
||||
class="text-gray-700 font-weight-normal font-size-14 font-style align-bottom"
|
||||
>
|
||||
{isPostPreviewAvailable(previewBody)
|
||||
? 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>
|
||||
)}
|
||||
{pinned && (
|
||||
<Icon
|
||||
src={PushPin}
|
||||
className={`post-summary-icons-dimensions text-gray-700
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span class="align-bottom"> </span>
|
||||
<span
|
||||
class="text-gray-700 font-weight-normal font-size-14 font-style align-bottom"
|
||||
>
|
||||
{isPostPreviewAvailable(previewBody)
|
||||
? 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>
|
||||
)}
|
||||
{pinned && (
|
||||
<Icon
|
||||
src={PushPin}
|
||||
className={`post-summary-icons-dimensions text-gray-700
|
||||
${canSeeReportedBadge || showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<AuthorLabel
|
||||
author={author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
/>
|
||||
<PostSummaryFooter
|
||||
postId={id}
|
||||
voted={voted}
|
||||
voteCount={voteCount}
|
||||
following={following}
|
||||
commentCount={commentCount}
|
||||
unreadCommentCount={unreadCommentCount}
|
||||
groupId={groupId}
|
||||
groupName={groupName}
|
||||
createdAt={createdAt}
|
||||
preview
|
||||
showNewCountLabel={isPostRead}
|
||||
/>
|
||||
</div>
|
||||
<AuthorLabel
|
||||
author={author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
/>
|
||||
<PostSummaryFooter
|
||||
postId={id}
|
||||
voted={voted}
|
||||
voteCount={voteCount}
|
||||
following={following}
|
||||
commentCount={commentCount}
|
||||
unreadCommentCount={unreadCommentCount}
|
||||
groupId={groupId}
|
||||
groupName={groupName}
|
||||
createdAt={createdAt}
|
||||
preview
|
||||
showNewCountLabel={isPostRead}
|
||||
/>
|
||||
</div>
|
||||
{!showDivider && pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
</>
|
||||
</div>
|
||||
{!showDivider && pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const TopicGroupBase = ({
|
||||
|
||||
const renderFilteredTopics = useMemo(() => {
|
||||
if (!hasFilteredSubtopics) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable no-unused-vars, react/forbid-prop-types */
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function countFilteredTopics(topicsSelector, provider) {
|
||||
? item.name.toLowerCase().includes(query)
|
||||
: true
|
||||
));
|
||||
count += nonCoursewareTopicsList?.length;
|
||||
count += nonCoursewareTopicsList?.length ?? 0;
|
||||
// Counting legacy topics
|
||||
if (provider === DiscussionProvider.LEGACY) {
|
||||
const categories = topicsSelector?.categoryIds;
|
||||
|
||||
@@ -17,6 +17,7 @@ const DiscussionsProductTour = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{!isEmpty(config) && (
|
||||
<ProductTour
|
||||
|
||||
@@ -20,23 +20,21 @@ Object.defineProperty(window, 'matchMedia', {
|
||||
});
|
||||
|
||||
// Provides a mock editor component that functions like tinyMCE without the overhead
|
||||
function MockEditor({
|
||||
const MockEditor = ({
|
||||
onBlur,
|
||||
onEditorChange,
|
||||
}) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-filename-extension
|
||||
<textarea
|
||||
data-testid="tinymce-editor"
|
||||
onChange={(event) => {
|
||||
onEditorChange(event.currentTarget.value);
|
||||
}}
|
||||
onBlur={event => {
|
||||
onBlur(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
// eslint-disable-next-line react/jsx-filename-extension
|
||||
<textarea
|
||||
data-testid="tinymce-editor"
|
||||
onChange={(event) => {
|
||||
onEditorChange(event.currentTarget.value);
|
||||
}}
|
||||
onBlur={event => {
|
||||
onBlur(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
MockEditor.propTypes = {
|
||||
onBlur: PropTypes.func.isRequired,
|
||||
|
||||
Reference in New Issue
Block a user