Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71d04a5353 | ||
|
|
d2b2a2aff9 | ||
|
|
569ce49801 | ||
|
|
c67bc3e080 | ||
|
|
bcde4f5f87 | ||
|
|
eaa3ce16ea | ||
|
|
af5bc1a664 | ||
|
|
2fa0900a65 | ||
|
|
afbd894154 | ||
|
|
bfcb1282f0 | ||
|
|
f081e8dc77 |
24
.github/pull_request_template.md
vendored
24
.github/pull_request_template.md
vendored
@@ -1,24 +0,0 @@
|
||||
### Description
|
||||
|
||||
Include a description of your changes here, along with a link to any relevant Jira tickets and/or GitHub issues.
|
||||
|
||||
#### How Has This Been Tested?
|
||||
|
||||
Please describe in detail how you tested your changes.
|
||||
|
||||
#### Screenshots/sandbox (optional):
|
||||
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if it's not applicable.**
|
||||
|
||||
|Before|After|
|
||||
|-------|-----|
|
||||
| | |
|
||||
|
||||
#### Merge Checklist
|
||||
|
||||
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
|
||||
* [ ] Is there adequate test coverage for your changes?
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/edx-infinity** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
13
README.rst
13
README.rst
@@ -27,8 +27,7 @@ The dev server is running at `http://localhost:2002 <http://localhost:2002>`_.
|
||||
|
||||
Getting Help
|
||||
------------
|
||||
Please tag **@openedx/edx-infinity ** on any PRs or issues. Thanks.
|
||||
|
||||
Please tag **@edx/fedx-team** on any PRs or issues. Thanks.
|
||||
If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in the community.
|
||||
For anything non-trivial, the best path is to open an issue in this repository with as many details about the issue you are facing as you can provide.
|
||||
|
||||
@@ -42,22 +41,18 @@ How to Contribute
|
||||
-----------------
|
||||
Details about how to become a contributor to the Open edX project may be found in the wiki at `How to contribute`_
|
||||
|
||||
.. _How to contribute: https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/process/index.html
|
||||
.. _How to contribute: https://openedx.org/r/how-to-contribute
|
||||
|
||||
PR description template should be automatically applied if you are sending PR from github interface; otherwise you
|
||||
can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend-app-discussions/blob/master/.github/pull_request_template.md>`_
|
||||
|
||||
This project is currently accepting all types of contributions, bug fixes and security fixes
|
||||
|
||||
The Open edX Code of Conduct
|
||||
----------------------------
|
||||
All community members should familiarize themselves with the `Open edX Code of Conduct`_.
|
||||
All community members should familarize themselves with the `Open edX Code of Conduct`_.
|
||||
|
||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||
|
||||
People
|
||||
------
|
||||
The assigned maintainers for this component and other project details may be found in Backstage or from inspecting catalog-info.yaml.
|
||||
The assigned maintainers for this component and other project details may be found in Backstage or groked from inspecting catalog-info.yaml.
|
||||
|
||||
Reporting Security Issues
|
||||
-------------------------
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
# (Required) Acceptable Values: Component, Resource, System
|
||||
# A repo will almost certainly be a Component.
|
||||
kind: Component
|
||||
metadata:
|
||||
name: 'frontend-app-discussions'
|
||||
@@ -9,10 +11,28 @@ metadata:
|
||||
links:
|
||||
- url: "https://github.com/openedx/frontend-app-discussions"
|
||||
title: "Frontend app discussions"
|
||||
# Backstage uses the MaterialUI Icon Set.
|
||||
# https://mui.com/material-ui/material-icons/
|
||||
# The value will be the name of the icon.
|
||||
icon: "Web"
|
||||
annotations:
|
||||
# (Optional) Annotation keys and values can be whatever you want.
|
||||
# We use it in Open edX repos to have a comma-separated list of GitHub user
|
||||
# names that might be interested in changes to the architecture of this
|
||||
# component.
|
||||
openedx.org/arch-interest-groups: ""
|
||||
spec:
|
||||
owner: group:edx-infinity
|
||||
|
||||
# (Required) This can be a group (`group:<github_group_name>`) or a user (`user:<github_username>`).
|
||||
# Don't forget the "user:" or "group:" prefix. Groups must be GitHub team
|
||||
# names in the openedx GitHub organization: https://github.com/orgs/openedx/teams
|
||||
#
|
||||
# If you need a new team created, create an issue with tCRIL engineering:
|
||||
# https://github.com/openedx/tcril-engineering/issues/new/choose
|
||||
owner: group:infinity
|
||||
|
||||
# (Required) Acceptable Type Values: service, website, library
|
||||
type: 'website'
|
||||
lifecycle: 'production'
|
||||
|
||||
# (Required) Acceptable Lifecycle Values: experimental, production, deprecated
|
||||
lifecycle: 'production'
|
||||
@@ -5,8 +5,6 @@ import DOMPurify from 'dompurify';
|
||||
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { useDebounce } from '../discussions/data/hooks';
|
||||
|
||||
const defaultSanitizeOptions = {
|
||||
USE_PROFILES: { html: true },
|
||||
ADD_ATTR: ['columnalign'],
|
||||
@@ -18,21 +16,19 @@ function HTMLLoader({
|
||||
const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions });
|
||||
const previewRef = useRef();
|
||||
|
||||
const debouncedPostContent = useDebounce(htmlNode, 500);
|
||||
|
||||
useEffect(() => {
|
||||
let promise = Promise.resolve(); // Used to hold chain of typesetting calls
|
||||
|
||||
function typeset(code) {
|
||||
promise = promise.then(() => window.MathJax?.typesetPromise(code()))
|
||||
.catch((err) => logError(`Typeset failed: ${err.message}`));
|
||||
return promise;
|
||||
}
|
||||
if (debouncedPostContent) {
|
||||
typeset(() => {
|
||||
previewRef.current.innerHTML = sanitizedMath;
|
||||
});
|
||||
}
|
||||
}, [debouncedPostContent]);
|
||||
|
||||
typeset(() => {
|
||||
previewRef.current.innerHTML = sanitizedMath;
|
||||
});
|
||||
}, [htmlNode]);
|
||||
|
||||
return (
|
||||
<div ref={previewRef} className={cssClassName} id={componentId} data-testid={testId} />
|
||||
|
||||
@@ -33,12 +33,13 @@ function PostPreviewPane({
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex justify-content-end">
|
||||
{!showPreviewPane && (
|
||||
{!showPreviewPane
|
||||
&& (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => setShowPreviewPane(true)}
|
||||
className={`text-primary-500 font-style p-0 ${editExisting && 'mb-4.5'}`}
|
||||
className={`text-primary-500 p-0 ${editExisting && 'mb-4.5'}`}
|
||||
style={{ lineHeight: '26px' }}
|
||||
>
|
||||
{intl.formatMessage(messages.showPreviewButton)}
|
||||
|
||||
@@ -119,7 +119,6 @@ export default function TinyMCEEditor(props) {
|
||||
content_css: false,
|
||||
content_style: contentStyle,
|
||||
body_class: 'm-2 text-editor',
|
||||
convert_urls: false,
|
||||
relative_urls: false,
|
||||
default_link_target: '_blank',
|
||||
target_list: false,
|
||||
|
||||
317
src/discussions/comments/CommentsView.jsx
Normal file
317
src/discussions/comments/CommentsView.jsx
Normal file
@@ -0,0 +1,317 @@
|
||||
import React, {
|
||||
useContext, useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Icon, IconButton,
|
||||
Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
EndorsementStatus, PostsPages, ThreadType,
|
||||
} from '../../data/constants';
|
||||
import { useDispatchWithState } from '../../data/hooks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useIsOnDesktop, useUserCanAddThreadInBlackoutDate } from '../data/hooks';
|
||||
import { EmptyPage } from '../empty-posts';
|
||||
import { Post } from '../posts';
|
||||
import { selectThread } from '../posts/data/selectors';
|
||||
import { fetchThread, markThreadAsRead } from '../posts/data/thunks';
|
||||
import { discussionsPath, filterPosts, isLastElementOfList } from '../utils';
|
||||
import { selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages } from './data/selectors';
|
||||
import { fetchThreadComments } from './data/thunks';
|
||||
import { Comment, ResponseEditor } from './comment';
|
||||
import messages from './messages';
|
||||
|
||||
function usePost(postId) {
|
||||
const dispatch = useDispatch();
|
||||
const thread = useSelector(selectThread(postId));
|
||||
|
||||
useEffect(() => {
|
||||
if (thread && !thread.read) {
|
||||
dispatch(markThreadAsRead(postId));
|
||||
}
|
||||
}, [postId]);
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
function usePostComments(postId, endorsed = null) {
|
||||
const [isLoading, dispatch] = useDispatchWithState();
|
||||
const comments = useSelector(selectThreadComments(postId, endorsed));
|
||||
const hasMorePages = useSelector(selectThreadHasMorePages(postId, endorsed));
|
||||
const currentPage = useSelector(selectThreadCurrentPage(postId, endorsed));
|
||||
const handleLoadMoreResponses = async () => dispatch(fetchThreadComments(postId, {
|
||||
endorsed,
|
||||
page: currentPage + 1,
|
||||
}));
|
||||
useEffect(() => {
|
||||
dispatch(fetchThreadComments(postId, {
|
||||
endorsed,
|
||||
page: 1,
|
||||
}));
|
||||
}, [postId]);
|
||||
return {
|
||||
comments,
|
||||
hasMorePages,
|
||||
isLoading,
|
||||
handleLoadMoreResponses,
|
||||
};
|
||||
}
|
||||
|
||||
function DiscussionCommentsView({
|
||||
postType,
|
||||
postId,
|
||||
intl,
|
||||
endorsed,
|
||||
isClosed,
|
||||
}) {
|
||||
const {
|
||||
comments,
|
||||
hasMorePages,
|
||||
isLoading,
|
||||
handleLoadMoreResponses,
|
||||
} = usePostComments(postId, endorsed);
|
||||
|
||||
const endorsedComments = useMemo(() => [...filterPosts(comments, 'endorsed')], [comments]);
|
||||
const unEndorsedComments = useMemo(() => [...filterPosts(comments, 'unendorsed')], [comments]);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
|
||||
const handleDefinition = (message, commentsLength) => (
|
||||
<div
|
||||
className="mx-4 my-14px text-gray-700 font-style-normal font-family-inter"
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
style={{ lineHeight: '24px' }}
|
||||
>
|
||||
{intl.formatMessage(message, { num: commentsLength })}
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleComments = (postComments, showLoadMoreResponses = false) => (
|
||||
<div className="mx-4" role="list">
|
||||
{postComments.map((comment) => (
|
||||
<Comment
|
||||
comment={comment}
|
||||
key={comment.id}
|
||||
postType={postType}
|
||||
isClosedPost={isClosed}
|
||||
marginBottom={isLastElementOfList(postComments, comment)}
|
||||
/>
|
||||
|
||||
))}
|
||||
{hasMorePages && !isLoading && !showLoadMoreResponses && (
|
||||
<Button
|
||||
onClick={handleLoadMoreResponses}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="px-4 mt-3 py-0 mb-2 font-style-normal font-family-inter font-weight-500 font-size-14"
|
||||
style={{
|
||||
lineHeight: '24px',
|
||||
border: '0px',
|
||||
}}
|
||||
data-testid="load-more-comments"
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreResponses)}
|
||||
</Button>
|
||||
)}
|
||||
{isLoading && !showLoadMoreResponses && (
|
||||
<div className="mb-2 mt-3 d-flex justify-content-center">
|
||||
<Spinner animation="border" variant="primary" className="spinner-dimentions" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{((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 py-10px mt-2 font-style-normal font-family-inter font-weight-500 font-size-14 text-primary-500"
|
||||
style={{
|
||||
lineHeight: '24px',
|
||||
border: '0px',
|
||||
}}
|
||||
onClick={() => setAddingResponse(true)}
|
||||
data-testid="add-response"
|
||||
>
|
||||
{intl.formatMessage(messages.addResponse)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResponseEditor
|
||||
postId={postId}
|
||||
handleCloseEditor={() => setAddingResponse(false)}
|
||||
addingResponse={addingResponse}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
DiscussionCommentsView.propTypes = {
|
||||
postId: PropTypes.string.isRequired,
|
||||
postType: PropTypes.string.isRequired,
|
||||
isClosed: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
endorsed: PropTypes.oneOf([
|
||||
EndorsementStatus.ENDORSED, EndorsementStatus.UNENDORSED, EndorsementStatus.DISCUSSION,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
function CommentsView({ intl }) {
|
||||
const [isLoading, submitDispatch] = useDispatchWithState();
|
||||
const { postId } = useParams();
|
||||
const thread = usePost(postId);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const {
|
||||
courseId, learnerUsername, category, topicId, page, enableInContextSidebar,
|
||||
} = useContext(DiscussionContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread) { submitDispatch(fetchThread(postId, courseId, true)); }
|
||||
setAddingResponse(false);
|
||||
}, [postId]);
|
||||
|
||||
if (!thread) {
|
||||
if (!isLoading) {
|
||||
return (
|
||||
<EmptyPage title={intl.formatMessage(messages.noThreadFound)} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
}}
|
||||
>
|
||||
<Spinner animation="border" variant="primary" data-testid="loading-indicator" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOnDesktop && (
|
||||
enableInContextSidebar ? (
|
||||
<>
|
||||
<div className="px-4 py-1.5 bg-white">
|
||||
<Button
|
||||
variant="plain"
|
||||
className="px-0 font-weight-light text-primary-500"
|
||||
iconBefore={ArrowBack}
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
size="sm"
|
||||
>
|
||||
{intl.formatMessage(messages.backAlt)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border-bottom border-light-400" />
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
src={ArrowBack}
|
||||
iconAs={Icon}
|
||||
style={{ padding: '18px' }}
|
||||
size="inline"
|
||||
className="ml-4 mt-4"
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
alt={intl.formatMessage(messages.backAlt)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={classNames('discussion-comments d-flex flex-column card border-0', {
|
||||
'post-card-margin post-card-padding': !enableInContextSidebar,
|
||||
'post-card-padding rounded-0 border-0 mb-4': enableInContextSidebar,
|
||||
})}
|
||||
>
|
||||
<Post post={thread} handleAddResponseButton={() => setAddingResponse(true)} />
|
||||
{!thread.closed && (
|
||||
<ResponseEditor
|
||||
postId={postId}
|
||||
handleCloseEditor={() => setAddingResponse(false)}
|
||||
addingResponse={addingResponse}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
thread.type === ThreadType.DISCUSSION && (
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.DISCUSSION}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
thread.type === ThreadType.QUESTION && (
|
||||
<>
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.ENDORSED}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
<DiscussionCommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.UNENDORSED}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CommentsView.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CommentsView);
|
||||
@@ -19,7 +19,6 @@ import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import { getCommentsApiUrl } from './data/api';
|
||||
import { removeComment } from './data/thunks';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
import './data/__factories__';
|
||||
@@ -31,11 +30,9 @@ const discussionPostId = 'thread-1';
|
||||
const questionPostId = 'thread-2';
|
||||
const closedPostId = 'thread-2';
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const reverseOrder = false;
|
||||
let store;
|
||||
let axiosMock;
|
||||
let testLocation;
|
||||
let container;
|
||||
|
||||
function mockAxiosReturnPagedComments() {
|
||||
[null, false, true].forEach(endorsed => {
|
||||
@@ -49,7 +46,6 @@ function mockAxiosReturnPagedComments() {
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
endorsed,
|
||||
reverse_order: reverseOrder,
|
||||
},
|
||||
})
|
||||
.reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
@@ -86,7 +82,7 @@ function mockAxiosReturnPagedCommentsResponses() {
|
||||
}
|
||||
|
||||
function renderComponent(postId) {
|
||||
const wrapper = render(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
@@ -106,10 +102,9 @@ function renderComponent(postId) {
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
}
|
||||
|
||||
describe('ThreadView', () => {
|
||||
describe('CommentsView', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -167,12 +162,11 @@ describe('ThreadView', () => {
|
||||
|
||||
it('should show and hide the editor', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const post = screen.getByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
const addResponseButton = within(hoverCard).getByRole('button', { name: /Add response/i });
|
||||
await screen.findByTestId('thread-1');
|
||||
const addResponseButtons = screen.getAllByRole('button', { name: /add a response/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
addResponseButton,
|
||||
addResponseButtons[0],
|
||||
);
|
||||
});
|
||||
expect(screen.queryByTestId('tinymce-editor')).toBeInTheDocument();
|
||||
@@ -184,12 +178,11 @@ describe('ThreadView', () => {
|
||||
|
||||
it('should allow posting a response', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
const addResponseButton = within(hoverCard).getByRole('button', { name: /Add response/i });
|
||||
await screen.findByTestId('thread-1');
|
||||
const responseButtons = screen.getAllByRole('button', { name: /add a response/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
addResponseButton,
|
||||
responseButtons[0],
|
||||
);
|
||||
});
|
||||
await act(() => {
|
||||
@@ -207,46 +200,56 @@ describe('ThreadView', () => {
|
||||
|
||||
it('should not allow posting a response on a closed post', async () => {
|
||||
renderComponent(closedPostId);
|
||||
const post = screen.getByTestId('post-thread-2');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-2');
|
||||
expect(within(hoverCard).getByRole('button', { name: /Add response/i })).toBeDisabled();
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByText('Thread-2', { exact: false })));
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: /add response/i }, { hidden: false })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should allow posting a comment', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /Add comment/i }),
|
||||
screen.getAllByRole('button', { name: /add comment/i })[0],
|
||||
);
|
||||
});
|
||||
act(() => {
|
||||
fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
screen.getByText(/submit/i),
|
||||
);
|
||||
});
|
||||
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
|
||||
await waitFor(async () => expect(await screen.findByTestId('reply-comment-7')).toBeInTheDocument());
|
||||
await waitFor(async () => expect(await screen.findByTestId('comment-comment-1')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should not allow posting a comment on a closed post', async () => {
|
||||
renderComponent(closedPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-3'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-3');
|
||||
expect(within(hoverCard).getByRole('button', { name: /Add comment/i })).toBeDisabled();
|
||||
await screen.findByTestId('thread-2');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-3')));
|
||||
});
|
||||
|
||||
const addCommentButton = screen.getAllByRole('button', { name: /add comment/i }, { hidden: false })[0];
|
||||
expect(addCommentButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should allow editing an existing comment', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
// The first edit menu is for the post, the second will be for the first comment.
|
||||
screen.getAllByRole('button', { name: /actions menu/i })[1],
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
@@ -259,7 +262,7 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
|
||||
});
|
||||
await waitFor(async () => {
|
||||
expect(await screen.findByTestId('comment-comment-1')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('comment-1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -283,11 +286,13 @@ describe('ThreadView', () => {
|
||||
it('should show reason codes when editing an existing comment', async () => {
|
||||
setupCourseConfig();
|
||||
renderComponent(discussionPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
// The first edit menu is for the post, the second will be for the first comment.
|
||||
screen.getAllByRole('button', { name: /actions menu/i })[1],
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
@@ -315,11 +320,15 @@ describe('ThreadView', () => {
|
||||
it('should show reason codes when closing a post', async () => {
|
||||
setupCourseConfig();
|
||||
renderComponent(discussionPostId);
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
// The first edit menu is for the post
|
||||
screen.getAllByRole('button', {
|
||||
name: /actions menu/i,
|
||||
})[0],
|
||||
);
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
|
||||
@@ -342,11 +351,13 @@ describe('ThreadView', () => {
|
||||
it('should close the post directly if reason codes are not enabled', async () => {
|
||||
setupCourseConfig(false);
|
||||
renderComponent(discussionPostId);
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
// The first edit menu is for the post
|
||||
screen.getAllByRole('button', { name: /actions menu/i })[0],
|
||||
);
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
|
||||
@@ -362,11 +373,13 @@ describe('ThreadView', () => {
|
||||
async (reasonCodesEnabled) => {
|
||||
setupCourseConfig(reasonCodesEnabled);
|
||||
renderComponent(closedPostId);
|
||||
const post = screen.getByTestId('post-thread-2');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-2');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByText('Thread-2', { exact: false })));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
// The first edit menu is for the post
|
||||
screen.getAllByRole('button', { name: /actions menu/i })[0],
|
||||
);
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /close post/i })).not.toBeInTheDocument();
|
||||
@@ -381,11 +394,13 @@ describe('ThreadView', () => {
|
||||
it('should show the editor if the post is edited', async () => {
|
||||
setupCourseConfig(false);
|
||||
renderComponent(discussionPostId);
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
// The first edit menu is for the post
|
||||
screen.getAllByRole('button', { name: /actions menu/i })[0],
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
@@ -396,11 +411,13 @@ describe('ThreadView', () => {
|
||||
|
||||
it('should allow pinning the post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
// The first edit menu is for the post
|
||||
screen.getAllByRole('button', { name: /actions menu/i })[0],
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
@@ -411,11 +428,13 @@ describe('ThreadView', () => {
|
||||
|
||||
it('should allow reporting the post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('post-thread-1')));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
// The first edit menu is for the post
|
||||
screen.getAllByRole('button', { name: /actions menu/i })[0],
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
@@ -429,29 +448,18 @@ describe('ThreadView', () => {
|
||||
assertLastUpdateData({ abuse_flagged: true });
|
||||
});
|
||||
|
||||
it('handles liking a post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /like/i }),
|
||||
);
|
||||
});
|
||||
expect(axiosMock.history.patch).toHaveLength(2);
|
||||
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
|
||||
});
|
||||
|
||||
it('handles liking a comment', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
|
||||
// Wait for the content to load
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /like/i }),
|
||||
);
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
const view = screen.getByTestId('comment-comment-1');
|
||||
|
||||
const likeButton = within(view).getByRole('button', { name: /like/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(likeButton);
|
||||
});
|
||||
expect(axiosMock.history.patch).toHaveLength(2);
|
||||
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
|
||||
@@ -460,10 +468,12 @@ describe('ThreadView', () => {
|
||||
it('handles endorsing comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
|
||||
await act(async () => {
|
||||
fireEvent.click(within(hoverCard).getByRole('button', { name: /Endorse/i }));
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Endorse/i }));
|
||||
});
|
||||
expect(axiosMock.history.patch).toHaveLength(2);
|
||||
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
|
||||
@@ -472,12 +482,12 @@ describe('ThreadView', () => {
|
||||
it('handles reporting comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
);
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(actionButtons[0]);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -504,9 +514,9 @@ describe('ThreadView', () => {
|
||||
|
||||
it('initially loads only the first page', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
expect(await screen.findByTestId('comment-comment-1'))
|
||||
expect(await screen.findByTestId('comment-1'))
|
||||
.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('comment-comment-2'))
|
||||
expect(screen.queryByTestId('comment-2'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
@@ -517,8 +527,8 @@ describe('ThreadView', () => {
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
fireEvent.click(loadMoreButton);
|
||||
|
||||
await screen.findByTestId('comment-comment-1');
|
||||
await screen.findByTestId('comment-comment-2');
|
||||
await screen.findByTestId('comment-1');
|
||||
await screen.findByTestId('comment-2');
|
||||
});
|
||||
|
||||
it('newly loaded comments are appended to the old ones', async () => {
|
||||
@@ -527,9 +537,9 @@ describe('ThreadView', () => {
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
fireEvent.click(loadMoreButton);
|
||||
|
||||
await screen.findByTestId('comment-comment-1');
|
||||
await screen.findByTestId('comment-1');
|
||||
// check that comments from the first page are also displayed
|
||||
expect(screen.queryByTestId('comment-comment-2'))
|
||||
expect(screen.queryByTestId('comment-2'))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -542,7 +552,7 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
}
|
||||
|
||||
await screen.findByTestId('comment-comment-2');
|
||||
await screen.findByTestId('comment-2');
|
||||
await expect(findLoadMoreCommentsButton())
|
||||
.rejects
|
||||
.toThrow();
|
||||
@@ -554,11 +564,11 @@ describe('ThreadView', () => {
|
||||
|
||||
it('initially loads only the first page', async () => {
|
||||
act(() => renderComponent(questionPostId));
|
||||
expect(await screen.findByTestId('comment-comment-3'))
|
||||
expect(await screen.findByTestId('comment-3'))
|
||||
.toBeInTheDocument();
|
||||
expect(await screen.findByTestId('comment-comment-5'))
|
||||
expect(await screen.findByTestId('comment-5'))
|
||||
.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('comment-comment-4'))
|
||||
expect(screen.queryByTestId('comment-4'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
@@ -571,15 +581,15 @@ describe('ThreadView', () => {
|
||||
const [loadMoreButtonEndorsed, loadMoreButtonUnendorsed] = await findLoadMoreCommentsButtons();
|
||||
// Both load more buttons should show
|
||||
expect(await findLoadMoreCommentsButtons()).toHaveLength(2);
|
||||
expect(await screen.findByTestId('comment-comment-3'))
|
||||
expect(await screen.findByTestId('comment-3'))
|
||||
.toBeInTheDocument();
|
||||
expect(await screen.findByTestId('comment-comment-5'))
|
||||
expect(await screen.findByTestId('comment-5'))
|
||||
.toBeInTheDocument();
|
||||
// Comments from next page should not be loaded yet.
|
||||
expect(await screen.queryByTestId('comment-comment-6'))
|
||||
expect(await screen.queryByTestId('comment-6'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('comment-comment-4'))
|
||||
expect(await screen.queryByTestId('comment-4'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
|
||||
@@ -587,10 +597,10 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButtonEndorsed);
|
||||
});
|
||||
// Endorsed comment from next page should be loaded now.
|
||||
await waitFor(() => expect(screen.queryByTestId('comment-comment-6'))
|
||||
await waitFor(() => expect(screen.queryByTestId('comment-6'))
|
||||
.toBeInTheDocument());
|
||||
// Unendorsed comment from next page should not be loaded yet.
|
||||
expect(await screen.queryByTestId('comment-comment-4'))
|
||||
expect(await screen.queryByTestId('comment-4'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
// Now only one load more buttons should show, for unendorsed comments
|
||||
@@ -599,20 +609,20 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButtonUnendorsed);
|
||||
});
|
||||
// Unendorsed comment from next page should be loaded now.
|
||||
await waitFor(() => expect(screen.queryByTestId('comment-comment-4'))
|
||||
await waitFor(() => expect(screen.queryByTestId('comment-4'))
|
||||
.toBeInTheDocument());
|
||||
await expect(findLoadMoreCommentsButtons()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('for comments replies', () => {
|
||||
describe('comments responses', () => {
|
||||
const findLoadMoreCommentsResponsesButton = () => screen.findByTestId('load-more-comments-responses');
|
||||
|
||||
it('initially loads only the first page', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
expect(screen.queryByTestId('reply-comment-8')).not.toBeInTheDocument();
|
||||
await waitFor(() => screen.findByTestId('comment-7'));
|
||||
expect(screen.queryByTestId('comment-8')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of responses', async () => {
|
||||
@@ -623,7 +633,7 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
await screen.findByTestId('comment-8');
|
||||
});
|
||||
|
||||
it('newly loaded responses are appended to the old ones', async () => {
|
||||
@@ -634,9 +644,9 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
await screen.findByTestId('comment-8');
|
||||
// check that comments from the first page are also displayed
|
||||
expect(screen.queryByTestId('reply-comment-7')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('comment-7')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('load more button is hidden when no more responses pages to load', async () => {
|
||||
@@ -650,116 +660,101 @@ describe('ThreadView', () => {
|
||||
});
|
||||
}
|
||||
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
await screen.findByTestId('comment-8');
|
||||
await expect(findLoadMoreCommentsResponsesButton())
|
||||
.rejects
|
||||
.toThrow();
|
||||
});
|
||||
|
||||
it('handles liking a comment', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
// Wait for the content to load
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
const view = screen.getByTestId('comment-comment-1');
|
||||
|
||||
const likeButton = within(view).getByRole('button', { name: /like/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(likeButton);
|
||||
});
|
||||
expect(axiosMock.history.patch).toHaveLength(2);
|
||||
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
|
||||
});
|
||||
|
||||
it('handles endorsing comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Endorse/i }));
|
||||
});
|
||||
expect(axiosMock.history.patch).toHaveLength(2);
|
||||
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
|
||||
});
|
||||
|
||||
it('handles reporting comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
await act(async () => {
|
||||
fireEvent.mouseOver(await waitFor(() => screen.findByTestId('comment-7')));
|
||||
});
|
||||
|
||||
// There should be three buttons, one for the post, the second for the
|
||||
// comment and the third for a response to that comment
|
||||
const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(actionButtons[1]);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Report/i }));
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByRole('button', { name: /Confirm/i }));
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument();
|
||||
expect(axiosMock.history.patch).toHaveLength(2);
|
||||
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ component: 'post', testId: 'post-thread-1', cardId: 'hover-card-thread-1' },
|
||||
{ component: 'comment', testId: 'comment-comment-1', cardId: 'hover-card-comment-1' },
|
||||
{ component: 'post', testId: 'post-thread-1' },
|
||||
{ component: 'comment', testId: 'comment-comment-1' },
|
||||
{ component: 'reply', testId: 'reply-comment-7' },
|
||||
])('delete confirmation modal', ({
|
||||
component,
|
||||
testId,
|
||||
cardId,
|
||||
}) => {
|
||||
test(`for ${component}`, async () => {
|
||||
renderComponent(discussionPostId);
|
||||
// Wait for the content to load
|
||||
const post = await screen.findByTestId(testId);
|
||||
const hoverCard = within(post).getByTestId(cardId);
|
||||
expect(screen.queryByRole('dialog', { name: /Delete response/i, exact: false })).not.toBeInTheDocument();
|
||||
// await waitFor(() => expect(screen.findByTestId('post-thread-1')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.queryByText('This is Thread-1', { exact: false })).toBeInTheDocument());
|
||||
const content = screen.getByTestId(testId);
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
);
|
||||
fireEvent.mouseOver(content);
|
||||
});
|
||||
const actionsButton = within(content).getAllByRole('button', { name: /actions menu/i })[0];
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByRole('button', { name: /Delete/i }));
|
||||
fireEvent.click(actionsButton);
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /Delete/i, exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('for comments replies', () => {
|
||||
it('shows delete confirmation modal', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument();
|
||||
const deleteButton = within(content).queryByRole('button', { name: /delete/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(reply).getByRole('button', { name: /actions menu/i }),
|
||||
);
|
||||
fireEvent.click(deleteButton);
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).toBeInTheDocument();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByRole('button', { name: /Delete/i }));
|
||||
fireEvent.click(screen.queryByRole('button', { name: /delete/i }));
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /Delete/i, exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('for comments sort', () => {
|
||||
it('should show sort dropdown if there are endorse or unendorsed comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const sortWrapper = container.querySelector('.comments-sort');
|
||||
const sortDropDown = within(sortWrapper).getByRole('button', { name: /Oldest first/i });
|
||||
|
||||
expect(comment).toBeInTheDocument();
|
||||
expect(sortDropDown).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show sort dropdown if there is no response', async () => {
|
||||
const commentId = 'comment-1';
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
axiosMock.onDelete(`${commentsApiUrl}${commentId}/`).reply(201);
|
||||
await executeThunk(removeComment(commentId, discussionPostId), store.dispatch, store.getState);
|
||||
|
||||
expect(await waitFor(() => screen.findByText('No responses', { exact: true }))).toBeInTheDocument();
|
||||
expect(container.querySelector('.comments-sort')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have only two options', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Oldest first/i })); });
|
||||
const dropdown = await waitFor(() => screen.findByTestId('comment-sort-dropdown-modal-popup'));
|
||||
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
expect(await within(dropdown).getAllByRole('button')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should be selected Oldest first and auto focus', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Oldest first/i })); });
|
||||
const dropdown = await waitFor(() => screen.findByTestId('comment-sort-dropdown-modal-popup'));
|
||||
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
expect(within(dropdown).getByRole('button', { name: /Oldest first/i })).toBeInTheDocument();
|
||||
expect(within(dropdown).getByRole('button', { name: /Oldest first/i })).toHaveFocus();
|
||||
expect(within(dropdown).getByRole('button', { name: /Newest first/i })).not.toHaveFocus();
|
||||
});
|
||||
|
||||
test('successfully handles sort state update', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
expect(store.getState().comments.sortOrder).toBeFalsy();
|
||||
|
||||
await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Oldest first/i })); });
|
||||
const dropdown = await waitFor(() => screen.findByTestId('comment-sort-dropdown-modal-popup'));
|
||||
await act(async () => {
|
||||
fireEvent.click(within(dropdown).getByRole('button', { name: /Newest first/i }));
|
||||
});
|
||||
|
||||
expect(store.getState().comments.sortOrder).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog', { name: /delete \w+/i, exact: false })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
44
src/discussions/comments/comment-icons/CommentIcons.jsx
Normal file
44
src/discussions/comments/comment-icons/CommentIcons.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import timeLocale from '../../common/time-locale';
|
||||
import LikeButton from '../../posts/post/LikeButton';
|
||||
import { editComment } from '../data/thunks';
|
||||
|
||||
function CommentIcons({
|
||||
comment,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
timeago.register('time-locale', timeLocale);
|
||||
|
||||
const handleLike = () => dispatch(editComment(comment.id, { voted: !comment.voted }));
|
||||
if (comment.voteCount === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="ml-n1.5 mt-10px">
|
||||
<LikeButton
|
||||
count={comment.voteCount}
|
||||
onClick={handleLike}
|
||||
voted={comment.voted}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CommentIcons.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
voteCount: PropTypes.number,
|
||||
following: PropTypes.bool,
|
||||
voted: PropTypes.bool,
|
||||
createdAt: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CommentIcons);
|
||||
@@ -7,18 +7,18 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, useToggle } from '@edx/paragon';
|
||||
|
||||
import HTMLLoader from '../../../../components/HTMLLoader';
|
||||
import { ContentActions, EndorsementStatus } from '../../../../data/constants';
|
||||
import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../../common';
|
||||
import { DiscussionContext } from '../../../common/context';
|
||||
import HoverCard from '../../../common/HoverCard';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../../data/hooks';
|
||||
import { fetchThread } from '../../../posts/data/thunks';
|
||||
import LikeButton from '../../../posts/post/LikeButton';
|
||||
import { useActions } from '../../../utils';
|
||||
import { selectCommentCurrentPage, selectCommentHasMorePages, selectCommentResponses } from '../../data/selectors';
|
||||
import { editComment, fetchCommentResponses, removeComment } from '../../data/thunks';
|
||||
import messages from '../../messages';
|
||||
import HTMLLoader from '../../../components/HTMLLoader';
|
||||
import { ContentActions, EndorsementStatus } from '../../../data/constants';
|
||||
import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../common';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import HoverCard from '../../common/HoverCard';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import { fetchThread } from '../../posts/data/thunks';
|
||||
import { useActions } from '../../utils';
|
||||
import CommentIcons from '../comment-icons/CommentIcons';
|
||||
import { selectCommentCurrentPage, selectCommentHasMorePages, selectCommentResponses } from '../data/selectors';
|
||||
import { editComment, fetchCommentResponses, removeComment } from '../data/thunks';
|
||||
import messages from '../messages';
|
||||
import CommentEditor from './CommentEditor';
|
||||
import CommentHeader from './CommentHeader';
|
||||
import { commentShape } from './proptypes';
|
||||
@@ -43,6 +43,7 @@ function Comment({
|
||||
const hasMorePages = useSelector(selectCommentHasMorePages(comment.id));
|
||||
const currentPage = useSelector(selectCommentCurrentPage(comment.id));
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const [showHoverCard, setShowHoverCard] = useState(false);
|
||||
const {
|
||||
courseId,
|
||||
} = useContext(DiscussionContext);
|
||||
@@ -89,7 +90,6 @@ function Comment({
|
||||
const handleLoadMoreComments = () => (
|
||||
dispatch(fetchCommentResponses(comment.id, { page: currentPage + 1 }))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames({ 'mb-3': (showFullThread && !marginBottom) })}>
|
||||
{/* eslint-disable jsx-a11y/no-noninteractive-tabindex */}
|
||||
@@ -119,16 +119,25 @@ function Comment({
|
||||
/>
|
||||
)}
|
||||
<EndorsedAlertBanner postType={postType} content={comment} />
|
||||
<div className="d-flex flex-column post-card-comment px-4 pt-3.5 pb-10px" aria-level={5}>
|
||||
<HoverCard
|
||||
commentOrPost={comment}
|
||||
actionHandlers={actionHandlers}
|
||||
handleResponseCommentButton={() => setReplying(true)}
|
||||
onLike={() => dispatch(editComment(comment.id, { voted: !comment.voted }))}
|
||||
addResponseCommentButtonMessage={intl.formatMessage(messages.addComment)}
|
||||
isClosedPost={isClosedPost}
|
||||
endorseIcons={endorseIcons}
|
||||
/>
|
||||
<div
|
||||
className="d-flex flex-column post-card-comment px-4 pt-3.5 pb-10px"
|
||||
aria-level={5}
|
||||
onMouseEnter={() => setShowHoverCard(true)}
|
||||
onMouseLeave={() => setShowHoverCard(false)}
|
||||
onFocus={() => setShowHoverCard(true)}
|
||||
onBlur={() => setShowHoverCard(false)}
|
||||
>
|
||||
{showHoverCard && (
|
||||
<HoverCard
|
||||
commentOrPost={comment}
|
||||
actionHandlers={actionHandlers}
|
||||
handleResponseCommentButton={() => setReplying(true)}
|
||||
onLike={() => dispatch(editComment(comment.id, { voted: !comment.voted }))}
|
||||
addResponseCommentButtonMessage={intl.formatMessage(messages.addComment)}
|
||||
isClosedPost={isClosedPost}
|
||||
endorseIcons={endorseIcons}
|
||||
/>
|
||||
)}
|
||||
<AlertBanner content={comment} />
|
||||
<CommentHeader comment={comment} />
|
||||
{isEditing
|
||||
@@ -137,21 +146,18 @@ function Comment({
|
||||
)
|
||||
: (
|
||||
<HTMLLoader
|
||||
cssClassName="comment-body html-loader text-break mt-14px font-style text-primary-500"
|
||||
cssClassName="comment-body html-loader text-break mt-14px font-style-normal font-family-inter text-primary-500"
|
||||
componentId="comment"
|
||||
htmlNode={comment.renderedBody}
|
||||
testId={comment.id}
|
||||
/>
|
||||
)}
|
||||
{comment.voted && (
|
||||
<div className="ml-n1.5 mt-10px">
|
||||
<LikeButton
|
||||
count={comment.voteCount}
|
||||
onClick={() => dispatch(editComment(comment.id, { voted: !comment.voted }))}
|
||||
voted={comment.voted}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<CommentIcons
|
||||
comment={comment}
|
||||
following={comment.following}
|
||||
onLike={() => dispatch(editComment(comment.id, { voted: !comment.voted }))}
|
||||
createdAt={comment.createdAt}
|
||||
/>
|
||||
{inlineReplies.length > 0 && (
|
||||
<div className="d-flex flex-column mt-0.5" role="list">
|
||||
{/* Pass along intl since component used here is the one before it's injected with `injectIntl` */}
|
||||
@@ -170,8 +176,11 @@ function Comment({
|
||||
onClick={handleLoadMoreComments}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="font-size-14 line-height-24 font-style pt-10px border-0 font-weight-500 pb-0"
|
||||
className="font-size-14 font-style-normal font-family-inter pt-10px border-0 font-weight-500 pb-0"
|
||||
data-testid="load-more-comments-responses"
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreComments)}
|
||||
</Button>
|
||||
@@ -180,7 +189,10 @@ function Comment({
|
||||
isReplying ? (
|
||||
<div className="mt-2.5">
|
||||
<CommentEditor
|
||||
comment={{ threadId: comment.threadId, parentId: comment.id }}
|
||||
comment={{
|
||||
threadId: comment.threadId,
|
||||
parentId: comment.id,
|
||||
}}
|
||||
edit={false}
|
||||
onCloseEditor={() => setReplying(false)}
|
||||
/>
|
||||
@@ -190,9 +202,12 @@ function Comment({
|
||||
{!isClosedPost && userCanAddThreadInBlackoutDate && (inlineReplies.length >= 5)
|
||||
&& (
|
||||
<Button
|
||||
className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500"
|
||||
className="d-flex flex-grow mt-2 font-size-14 font-style-normal font-family-inter font-weight-500 text-primary-500"
|
||||
variant="plain"
|
||||
style={{ height: '36px' }}
|
||||
style={{
|
||||
lineHeight: '24px',
|
||||
height: '36px',
|
||||
}}
|
||||
onClick={() => setReplying(true)}
|
||||
>
|
||||
{intl.formatMessage(messages.addComment)}
|
||||
@@ -9,19 +9,19 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Button, Form, StatefulButton } from '@edx/paragon';
|
||||
|
||||
import { TinyMCEEditor } from '../../../../components';
|
||||
import FormikErrorFeedback from '../../../../components/FormikErrorFeedback';
|
||||
import PostPreviewPane from '../../../../components/PostPreviewPane';
|
||||
import { useDispatchWithState } from '../../../../data/hooks';
|
||||
import { TinyMCEEditor } from '../../../components';
|
||||
import FormikErrorFeedback from '../../../components/FormikErrorFeedback';
|
||||
import PostPreviewPane from '../../../components/PostPreviewPane';
|
||||
import { useDispatchWithState } from '../../../data/hooks';
|
||||
import {
|
||||
selectModerationSettings,
|
||||
selectUserHasModerationPrivileges,
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsStaff,
|
||||
} from '../../../data/selectors';
|
||||
import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
|
||||
import { addComment, editComment } from '../../data/thunks';
|
||||
import messages from '../../messages';
|
||||
} from '../../data/selectors';
|
||||
import { formikCompatibleHandler, isFormikFieldInvalid } from '../../utils';
|
||||
import { addComment, editComment } from '../data/thunks';
|
||||
import messages from '../messages';
|
||||
|
||||
function CommentEditor({
|
||||
intl,
|
||||
@@ -6,10 +6,10 @@ import { useSelector } from 'react-redux';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Avatar } from '@edx/paragon';
|
||||
|
||||
import { AvatarOutlineAndLabelColors } from '../../../../data/constants';
|
||||
import { AuthorLabel } from '../../../common';
|
||||
import { useAlertBannerVisible } from '../../../data/hooks';
|
||||
import { selectAuthorAvatars } from '../../../posts/data/selectors';
|
||||
import { AvatarOutlineAndLabelColors } from '../../../data/constants';
|
||||
import { AuthorLabel } from '../../common';
|
||||
import { useAlertBannerVisible } from '../../data/hooks';
|
||||
import { selectAuthorAvatars } from '../../posts/data/selectors';
|
||||
import { commentShape } from './proptypes';
|
||||
|
||||
function CommentHeader({
|
||||
@@ -7,16 +7,16 @@ import * as timeago from 'timeago.js';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Avatar, useToggle } from '@edx/paragon';
|
||||
|
||||
import HTMLLoader from '../../../../components/HTMLLoader';
|
||||
import { AvatarOutlineAndLabelColors, ContentActions } from '../../../../data/constants';
|
||||
import HTMLLoader from '../../../components/HTMLLoader';
|
||||
import { AvatarOutlineAndLabelColors, ContentActions } from '../../../data/constants';
|
||||
import {
|
||||
ActionsDropdown, AlertBanner, AuthorLabel, Confirmation,
|
||||
} from '../../../common';
|
||||
import timeLocale from '../../../common/time-locale';
|
||||
import { useAlertBannerVisible } from '../../../data/hooks';
|
||||
import { selectAuthorAvatars } from '../../../posts/data/selectors';
|
||||
import { editComment, removeComment } from '../../data/thunks';
|
||||
import messages from '../../messages';
|
||||
} from '../../common';
|
||||
import timeLocale from '../../common/time-locale';
|
||||
import { useAlertBannerVisible } from '../../data/hooks';
|
||||
import { selectAuthorAvatars } from '../../posts/data/selectors';
|
||||
import { editComment, removeComment } from '../data/thunks';
|
||||
import messages from '../messages';
|
||||
import CommentEditor from './CommentEditor';
|
||||
import { commentShape } from './proptypes';
|
||||
|
||||
@@ -120,7 +120,7 @@ function Reply({
|
||||
postCreatedAt={reply.createdAt}
|
||||
postOrComment
|
||||
/>
|
||||
<div className="ml-auto d-flex">
|
||||
<div className="ml-auto d-flex" style={{ lineHeight: '24px' }}>
|
||||
<ActionsDropdown
|
||||
commentOrPost={{
|
||||
...reply,
|
||||
@@ -137,7 +137,7 @@ function Reply({
|
||||
<HTMLLoader
|
||||
componentId="reply"
|
||||
htmlNode={reply.renderedBody}
|
||||
cssClassName="html-loader text-break font-style text-primary-500"
|
||||
cssClassName="html-loader text-break font-style-normal pb-1 font-family-inter text-primary-500"
|
||||
testId={reply.id}
|
||||
/>
|
||||
)}
|
||||
@@ -19,7 +19,7 @@ function ResponseEditor({
|
||||
|
||||
return addingResponse
|
||||
&& (
|
||||
<div className={classNames({ 'bg-white p-4 mb-4 rounded mt-2': addWrappingDiv })}>
|
||||
<div className={classNames({ 'bg-white p-4 mb-4 rounded': addWrappingDiv })}>
|
||||
<CommentEditor
|
||||
comment={{ threadId: postId }}
|
||||
edit={false}
|
||||
@@ -23,7 +23,6 @@ export async function getThreadComments(
|
||||
endorsed,
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
} = {},
|
||||
) {
|
||||
const params = snakeCaseObject({
|
||||
@@ -31,7 +30,6 @@ export async function getThreadComments(
|
||||
endorsed: EndorsementValue[endorsed],
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
requestedFields: 'profile_image',
|
||||
});
|
||||
|
||||
@@ -276,7 +276,8 @@ describe('Comments/Responses data layer tests', () => {
|
||||
const commentId = 'comment-1';
|
||||
|
||||
// This will generate 3 comments, so the responses will start at id = 'comment-4'
|
||||
axiosMock.onGet(commentsApiUrl).reply(200, Factory.build('commentsResult'));
|
||||
axiosMock.onGet(commentsApiUrl)
|
||||
.reply(200, Factory.build('commentsResult'));
|
||||
await executeThunk(fetchThreadComments(threadId), store.dispatch, store.getState);
|
||||
|
||||
// Build all comments first, so we can paginate over them and they
|
||||
@@ -300,7 +301,8 @@ describe('Comments/Responses data layer tests', () => {
|
||||
parent_id: commentId,
|
||||
});
|
||||
allResponses.push(comment);
|
||||
axiosMock.onPost(commentsApiUrl).reply(200, comment);
|
||||
axiosMock.onPost(commentsApiUrl)
|
||||
.reply(200, comment);
|
||||
await executeThunk(addComment('Test Comment', threadId, null), store.dispatch, store.getState);
|
||||
|
||||
// Someone else posted a new response now
|
||||
@@ -314,14 +316,15 @@ describe('Comments/Responses data layer tests', () => {
|
||||
});
|
||||
await executeThunk(fetchCommentResponses(commentId, { page: 2 }), store.dispatch, store.getState);
|
||||
|
||||
// sorting is implemented on backend
|
||||
expect(store.getState().comments.commentsInComments[commentId])
|
||||
.toEqual([
|
||||
'comment-4',
|
||||
'comment-5',
|
||||
'comment-6',
|
||||
'comment-8',
|
||||
'comment-7',
|
||||
// our comment was pushed down
|
||||
'comment-8',
|
||||
// the newer comment is placed correctly
|
||||
'comment-9',
|
||||
]);
|
||||
});
|
||||
@@ -353,7 +356,8 @@ describe('Comments/Responses data layer tests', () => {
|
||||
// Post new comment
|
||||
const comment = Factory.build('comment', { thread_id: threadId });
|
||||
allComments.push(comment);
|
||||
axiosMock.onPost(commentsApiUrl).reply(200, comment);
|
||||
axiosMock.onPost(commentsApiUrl)
|
||||
.reply(200, comment);
|
||||
await executeThunk(addComment('Test Comment', threadId, null), store.dispatch, store.getState);
|
||||
|
||||
// Somebody else posted a new response now
|
||||
@@ -367,14 +371,15 @@ describe('Comments/Responses data layer tests', () => {
|
||||
});
|
||||
await executeThunk(fetchThreadComments(threadId, { page: 2, endorsed }), store.dispatch, store.getState);
|
||||
|
||||
// sorting is implemented on backend
|
||||
expect(store.getState().comments.commentsInThreads[threadId][endorsed])
|
||||
.toEqual([
|
||||
'comment-1',
|
||||
'comment-2',
|
||||
'comment-3',
|
||||
'comment-5',
|
||||
'comment-4',
|
||||
// our comment was pushed down
|
||||
'comment-5',
|
||||
// the newer comment is placed correctly
|
||||
'comment-6',
|
||||
]);
|
||||
});
|
||||
@@ -36,6 +36,4 @@ export const selectCommentCurrentPage = commentId => (
|
||||
state => state.comments.responsesPagination[commentId]?.currentPage || null
|
||||
);
|
||||
|
||||
export const selectCommentsStatus = state => state.comments.status;
|
||||
|
||||
export const selectCommentSortOrder = state => state.comments.sortOrder;
|
||||
export const commentsStatus = state => state.comments.status;
|
||||
@@ -22,7 +22,6 @@ const commentsSlice = createSlice({
|
||||
postStatus: RequestStatus.SUCCESSFUL,
|
||||
pagination: {},
|
||||
responsesPagination: {},
|
||||
sortOrder: false,
|
||||
},
|
||||
reducers: {
|
||||
fetchCommentsRequest: (state) => {
|
||||
@@ -57,6 +56,15 @@ const commentsSlice = createSlice({
|
||||
hasMorePages: Boolean(payload.pagination.next),
|
||||
};
|
||||
state.commentsById = { ...state.commentsById, ...payload.commentsById };
|
||||
// We sort the comments by creation time.
|
||||
// This way our new comments are pushed down to the correct
|
||||
// position when more pages of older comments are loaded.
|
||||
state.commentsInThreads[threadId][endorsed].sort(
|
||||
(a, b) => (
|
||||
Date.parse(state.commentsById[a].createdAt)
|
||||
- Date.parse(state.commentsById[b].createdAt)
|
||||
),
|
||||
);
|
||||
},
|
||||
fetchCommentsFailed: (state) => {
|
||||
state.status = RequestStatus.FAILED;
|
||||
@@ -82,6 +90,12 @@ const commentsSlice = createSlice({
|
||||
]),
|
||||
];
|
||||
state.commentsById = { ...state.commentsById, ...payload.commentsById };
|
||||
state.commentsInComments[payload.commentId].sort(
|
||||
(a, b) => (
|
||||
Date.parse(state.commentsById[a].createdAt)
|
||||
- Date.parse(state.commentsById[b].createdAt)
|
||||
),
|
||||
);
|
||||
state.responsesPagination[payload.commentId] = {
|
||||
currentPage: payload.page,
|
||||
totalPages: payload.pagination.numPages,
|
||||
@@ -167,9 +181,6 @@ const commentsSlice = createSlice({
|
||||
}
|
||||
delete state.commentsById[commentId];
|
||||
},
|
||||
setCommentSortOrder: (state, { payload }) => {
|
||||
state.sortOrder = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -195,7 +206,6 @@ export const {
|
||||
deleteCommentFailed,
|
||||
deleteCommentRequest,
|
||||
deleteCommentSuccess,
|
||||
setCommentSortOrder,
|
||||
} = commentsSlice.actions;
|
||||
|
||||
export const commentsReducer = commentsSlice.reducer;
|
||||
@@ -74,18 +74,11 @@ function normaliseComments(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchThreadComments(
|
||||
threadId,
|
||||
{
|
||||
page = 1,
|
||||
reverseOrder,
|
||||
endorsed = EndorsementStatus.DISCUSSION,
|
||||
} = {},
|
||||
) {
|
||||
export function fetchThreadComments(threadId, { page = 1, endorsed = EndorsementStatus.DISCUSSION } = {}) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchCommentsRequest());
|
||||
const data = await getThreadComments(threadId, { page, reverseOrder, endorsed });
|
||||
const data = await getThreadComments(threadId, { page, endorsed });
|
||||
dispatch(fetchCommentsSuccess({
|
||||
...normaliseComments(camelCaseObject(data)),
|
||||
endorsed,
|
||||
2
src/discussions/comments/index.js
Normal file
2
src/discussions/comments/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as CommentsView } from './CommentsView';
|
||||
@@ -212,15 +212,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Thread not found',
|
||||
description: 'message to show on screen if the request thread is not found in course',
|
||||
},
|
||||
commentSort: {
|
||||
id: 'discussions.comment.sortFilterStatus',
|
||||
defaultMessage: `{sort, select,
|
||||
false {Oldest first}
|
||||
true {Newest first}
|
||||
other {{sort}}
|
||||
}`,
|
||||
description: 'sort message showing current sorting',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
import { MoreHoriz } from '@edx/paragon/icons';
|
||||
|
||||
import { ContentActions } from '../../data/constants';
|
||||
import { commentShape } from '../comments/comment/proptypes';
|
||||
import { selectBlackoutDate } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { commentShape } from '../post-comments/comments/comment/proptypes';
|
||||
import { postShape } from '../posts/post/proptypes';
|
||||
import { inBlackoutDateRange, useActions } from '../utils';
|
||||
import { DiscussionContext } from './context';
|
||||
|
||||
@@ -14,7 +14,7 @@ import messages from '../messages';
|
||||
import { ACTIONS_LIST } from '../utils';
|
||||
import ActionsDropdown from './ActionsDropdown';
|
||||
|
||||
import '../post-comments/data/__factories__';
|
||||
import '../comments/data/__factories__';
|
||||
import '../posts/data/__factories__';
|
||||
|
||||
let store;
|
||||
|
||||
@@ -8,11 +8,11 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Report } from '@edx/paragon/icons';
|
||||
|
||||
import { commentShape } from '../comments/comment/proptypes';
|
||||
import messages from '../comments/messages';
|
||||
import {
|
||||
selectModerationSettings, selectUserHasModerationPrivileges, selectUserIsGroupTa, selectUserIsStaff,
|
||||
} from '../data/selectors';
|
||||
import { commentShape } from '../post-comments/comments/comment/proptypes';
|
||||
import messages from '../post-comments/messages';
|
||||
import { postShape } from '../posts/post/proptypes';
|
||||
import AuthorLabel from './AuthorLabel';
|
||||
|
||||
@@ -41,13 +41,13 @@ function AlertBanner({
|
||||
<>
|
||||
{content.lastEdit?.reason && (
|
||||
<Alert variant="info" className="px-3 shadow-none mb-1 py-10px bg-light-200">
|
||||
<div className="d-flex align-items-center flex-wrap text-gray-700 font-style">
|
||||
<div className="d-flex align-items-center flex-wrap text-gray-700 font-family-inter">
|
||||
{intl.formatMessage(messages.editedBy)}
|
||||
<span className="ml-1 mr-3">
|
||||
<AuthorLabel author={content.lastEdit.editorUsername} linkToProfile postOrComment />
|
||||
</span>
|
||||
<span
|
||||
className="mx-1.5 font-size-8 font-style text-light-700"
|
||||
className="mx-1.5 font-family-inter font-size-8 font-style-normal text-light-700"
|
||||
style={{ lineHeight: '15px' }}
|
||||
>
|
||||
{intl.formatMessage(messages.fullStop)}
|
||||
@@ -58,13 +58,13 @@ function AlertBanner({
|
||||
)}
|
||||
{content.closed && (
|
||||
<Alert variant="info" className="px-3 shadow-none mb-1 py-10px bg-light-200">
|
||||
<div className="d-flex align-items-center flex-wrap text-gray-700 font-style">
|
||||
<div className="d-flex align-items-center flex-wrap text-gray-700 font-family-inter">
|
||||
{intl.formatMessage(messages.closedBy)}
|
||||
<span className="ml-1 ">
|
||||
<AuthorLabel author={content.closedBy} linkToProfile postOrComment />
|
||||
</span>
|
||||
<span
|
||||
className="mx-1.5 font-size-8 font-style text-light-700"
|
||||
className="mx-1.5 font-family-inter font-size-8 font-style-normal text-light-700"
|
||||
style={{ lineHeight: '15px' }}
|
||||
>
|
||||
{intl.formatMessage(messages.fullStop)}
|
||||
|
||||
@@ -7,11 +7,11 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { ThreadType } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import messages from '../post-comments/messages';
|
||||
import messages from '../comments/messages';
|
||||
import AlertBanner from './AlertBanner';
|
||||
import { DiscussionContext } from './context';
|
||||
|
||||
import '../post-comments/data/__factories__';
|
||||
import '../comments/data/__factories__';
|
||||
import '../posts/data/__factories__';
|
||||
|
||||
let store;
|
||||
|
||||
@@ -51,10 +51,10 @@ function AuthorLabel({
|
||||
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
|
||||
|
||||
const labelContents = (
|
||||
<div className={className}>
|
||||
<div className={className} style={{ lineHeight: '24px' }}>
|
||||
{!alert && (
|
||||
<span
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
|
||||
className={classNames('mr-1.5 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-gray-700': isRetiredUser,
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser,
|
||||
})}
|
||||
@@ -91,7 +91,7 @@ function AuthorLabel({
|
||||
</OverlayTrigger>
|
||||
{authorLabelMessage && (
|
||||
<span
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
|
||||
className={classNames('mr-1.5 font-size-14 font-style-normal font-family-inter font-weight-500', {
|
||||
'text-primary-500': showTextPrimary,
|
||||
'text-gray-700': isRetiredUser,
|
||||
})}
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Alert, Icon } from '@edx/paragon';
|
||||
import { CheckCircle, Verified } from '@edx/paragon/icons';
|
||||
|
||||
import { ThreadType } from '../../data/constants';
|
||||
import { commentShape } from '../post-comments/comments/comment/proptypes';
|
||||
import messages from '../post-comments/messages';
|
||||
import { commentShape } from '../comments/comment/proptypes';
|
||||
import messages from '../comments/messages';
|
||||
import AuthorLabel from './AuthorLabel';
|
||||
import timeLocale from './time-locale';
|
||||
|
||||
@@ -39,8 +39,11 @@ function EndorsedAlertBanner({
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
<strong className="ml-2 font-family-inter">
|
||||
{intl.formatMessage(isQuestion ? messages.answer : messages.endorsed)}
|
||||
<strong className="ml-2 font-family-inter">{intl.formatMessage(
|
||||
isQuestion
|
||||
? messages.answer
|
||||
: messages.endorsed,
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
<span className="d-flex align-items-center align-items-center flex-wrap" style={{ marginRight: '-1px' }}>
|
||||
|
||||
@@ -7,11 +7,11 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { ThreadType } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import messages from '../post-comments/messages';
|
||||
import messages from '../comments/messages';
|
||||
import { DiscussionContext } from './context';
|
||||
import EndorsedAlertBanner from './EndorsedAlertBanner';
|
||||
|
||||
import '../post-comments/data/__factories__';
|
||||
import '../comments/data/__factories__';
|
||||
import '../posts/data/__factories__';
|
||||
|
||||
let store;
|
||||
|
||||
@@ -4,13 +4,16 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon, IconButton } from '@edx/paragon';
|
||||
import {
|
||||
Button,
|
||||
Icon, IconButton,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import {
|
||||
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
|
||||
} from '../../components/icons';
|
||||
import { commentShape } from '../comments/comment/proptypes';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../data/hooks';
|
||||
import { commentShape } from '../post-comments/comments/comment/proptypes';
|
||||
import { postShape } from '../posts/post/proptypes';
|
||||
import ActionsDropdown from './ActionsDropdown';
|
||||
import { DiscussionContext } from './context';
|
||||
@@ -27,26 +30,29 @@ function HoverCard({
|
||||
}) {
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-fill justify-content-end align-items-center hover-card mr-n4 position-absolute"
|
||||
data-testid={`hover-card-${commentOrPost.id}`}
|
||||
id={`hover-card-${commentOrPost.id}`}
|
||||
className="d-flex flex-fill justify-content-end align-items-center hover-card mr-n4 position-absolute"
|
||||
data-testid="hover-card"
|
||||
>
|
||||
{userCanAddThreadInBlackoutDate && (
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className={classNames('px-2.5 py-2 border-0 font-style text-gray-700 font-size-12',
|
||||
className={classNames('px-2.5 py-2 border-0 font-style-normal font-family-inter text-gray-700 font-size-12',
|
||||
{ 'w-100': enableInContextSidebar })}
|
||||
onClick={() => handleResponseCommentButton()}
|
||||
disabled={isClosedPost}
|
||||
style={{ lineHeight: '20px' }}
|
||||
style={{
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
{addResponseCommentButtonMessage}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{endorseIcons && (
|
||||
<div className="hover-button">
|
||||
<IconButton
|
||||
@@ -86,6 +92,7 @@ function HoverCard({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onFollow();
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
render, screen, waitFor,
|
||||
within,
|
||||
render, screen, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
@@ -13,21 +13,20 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { getCommentsApiUrl } from '../comments/data/api';
|
||||
import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getCommentsApiUrl } from '../post-comments/data/api';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import { DiscussionContext } from './context';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
import '../post-comments/data/__factories__';
|
||||
import '../comments/data/__factories__';
|
||||
|
||||
const commentsApiUrl = getCommentsApiUrl();
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const discussionPostId = 'thread-1';
|
||||
const questionPostId = 'thread-2';
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const reverseOrder = false;
|
||||
let store;
|
||||
let axiosMock;
|
||||
let container;
|
||||
@@ -44,7 +43,6 @@ function mockAxiosReturnPagedComments() {
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
endorsed,
|
||||
reverse_order: reverseOrder,
|
||||
},
|
||||
})
|
||||
.reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
@@ -152,22 +150,30 @@ describe('HoverCard', () => {
|
||||
mockAxiosReturnPagedCommentsResponses();
|
||||
});
|
||||
|
||||
test('it should have hover card on post', async () => {
|
||||
test('it should show hover card when hovered on post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const post = screen.getByTestId('post-thread-1');
|
||||
expect(within(post).getByTestId('hover-card-thread-1')).toBeInTheDocument();
|
||||
userEvent.hover(post);
|
||||
expect(screen.getByTestId('hover-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it should have hover card on comment', async () => {
|
||||
test('it should show hover card when hovered on comment', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
expect(within(comment).getByTestId('hover-card-comment-1')).toBeInTheDocument();
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-1'));
|
||||
userEvent.hover(comment);
|
||||
expect(screen.getByTestId('hover-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it should not show hover card when post and comment not hovered', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
expect(screen.queryByTestId('hover-card')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('it should show add response, like, follow and actions menu for hovered post', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
const post = screen.getByTestId('post-thread-1');
|
||||
const view = within(post).getByTestId('hover-card-thread-1');
|
||||
userEvent.hover(post);
|
||||
const view = screen.getByTestId('hover-card');
|
||||
expect(within(view).queryByRole('button', { name: /Add response/i })).toBeInTheDocument();
|
||||
expect(within(view).getByRole('button', { name: /like/i })).toBeInTheDocument();
|
||||
expect(within(view).queryByRole('button', { name: /follow/i })).toBeInTheDocument();
|
||||
@@ -176,8 +182,10 @@ describe('HoverCard', () => {
|
||||
|
||||
test('it should show add comment, Endorse, like and actions menu Buttons for hovered comment', async () => {
|
||||
renderComponent(questionPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-3'));
|
||||
const view = within(comment).getByTestId('hover-card-comment-3');
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-3'));
|
||||
userEvent.hover(comment);
|
||||
const view = screen.getByTestId('hover-card');
|
||||
expect(screen.getByTestId('hover-card')).toBeInTheDocument();
|
||||
expect(within(view).queryByRole('button', { name: /Add comment/i })).toBeInTheDocument();
|
||||
expect(within(view).getByRole('button', { name: /Endorse/i })).toBeInTheDocument();
|
||||
expect(within(view).queryByRole('button', { name: /like/i })).toBeInTheDocument();
|
||||
|
||||
@@ -224,24 +224,3 @@ export const useTourConfiguration = (intl) => {
|
||||
}
|
||||
));
|
||||
};
|
||||
|
||||
export const useDebounce = (value, delay) => {
|
||||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
useEffect(
|
||||
() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
},
|
||||
[value, delay], // Only re-call effect if value or delay changes
|
||||
);
|
||||
return debouncedValue;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Route, Switch } from 'react-router';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Routes } from '../../data/constants';
|
||||
import { PostCommentsView } from '../post-comments';
|
||||
import { CommentsView } from '../comments';
|
||||
import { PostEditor } from '../posts';
|
||||
|
||||
function DiscussionContent() {
|
||||
@@ -25,7 +25,7 @@ function DiscussionContent() {
|
||||
<PostEditor editExisting />
|
||||
</Route>
|
||||
<Route path={Routes.COMMENTS.PATH}>
|
||||
<PostCommentsView />
|
||||
<CommentsView />
|
||||
</Route>
|
||||
</Switch>
|
||||
)}
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function DiscussionsHome() {
|
||||
>
|
||||
<div
|
||||
className={classNames('d-flex flex-row justify-content-between navbar fixed-top', {
|
||||
'pl-4 pr-3 py-0': enableInContextSidebar,
|
||||
'pl-4 pr-2.5 py-1.5': enableInContextSidebar,
|
||||
})}
|
||||
>
|
||||
{!enableInContextSidebar && <Route path={Routes.DISCUSSIONS.PATH} component={NavigationBar} />}
|
||||
@@ -120,7 +120,7 @@ export default function DiscussionsHome() {
|
||||
path={[Routes.POSTS.PATH, Routes.POSTS.ALL_POSTS, Routes.LEARNERS.POSTS]}
|
||||
render={routeProps => <EmptyPosts {...routeProps} subTitleMessage={messages.emptyAllPosts} />}
|
||||
/>
|
||||
{isRedirectToLearners && <Route path={Routes.LEARNERS.PATH} component={EmptyLearners} />}
|
||||
{isRedirectToLearners && <Route path={Routes.LEARNERS.PATH} component={EmptyLearners} /> }
|
||||
</Switch>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { getApiBaseUrl } from '../../../../data/constants';
|
||||
|
||||
Factory.define('topic')
|
||||
.sequence('id', ['topicPrefix'], (idx, topicPrefix) => `${topicPrefix}-${idx}`)
|
||||
.sequence('enabled-in-context', ['enabledInContext'], (idx, enabledInContext) => enabledInContext)
|
||||
.sequence('name', ['topicNamePrefix'], (idx, topicNamePrefix) => `${topicNamePrefix}-${idx}`)
|
||||
.sequence('usage-key', ['usageKey'], (idx, usageKey) => usageKey)
|
||||
.sequence('courseware', ['courseware'], (idx, courseware) => courseware)
|
||||
|
||||
.attr('thread_counts', ['discussionCount', 'questionCount'], (discCount, questCount) => {
|
||||
Factory.reset('thread-counts');
|
||||
return Factory.build('thread-counts', null, { discussionCount: discCount, questionCount: questCount });
|
||||
});
|
||||
|
||||
Factory.define('sub-section')
|
||||
.sequence('block_id', (idx) => `${idx}`)
|
||||
.option('topicPrefix', null, '')
|
||||
.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}`)
|
||||
.attr('type', null, 'sequential')
|
||||
.attr('children', ['id', 'display-name', 'courseId'], (id, name, courseId) => {
|
||||
Factory.reset('topic');
|
||||
return Factory.buildList('topic', 2, null, {
|
||||
topicPrefix: `${id}`,
|
||||
enabledInContext: true,
|
||||
topicNamePrefix: `${name}`,
|
||||
usageKey: `${courseId.replace('course-v1:', 'block-v1:')} +type@vertical+block@vertical_`,
|
||||
discussionCount: 1,
|
||||
questionCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
Factory.define('section')
|
||||
.sequence('block_id', (idx) => `${idx}`)
|
||||
.option('topicPrefix', null, '')
|
||||
.sequence('id', ['topicPrefix'], (idx, topicPrefix) => `${topicPrefix}-topic-${idx}`)
|
||||
.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}`)
|
||||
.attr('type', null, 'chapter')
|
||||
.attr('children', ['display-name'], (name) => {
|
||||
Factory.reset('sub-section');
|
||||
return Factory.buildList('sub-section', 2, null, { sectionPrefix: `${name}-`, topicPrefix: 'section' });
|
||||
});
|
||||
|
||||
Factory.define('thread-counts')
|
||||
.sequence('discussion', ['discussionCount'], (idx, discussionCount) => discussionCount)
|
||||
.sequence('question', ['questionCount'], (idx, questionCount) => questionCount);
|
||||
|
||||
Factory.define('archived-topics')
|
||||
.attr('id', null, 'archived')
|
||||
.option('courseId', null, 'course-v1:edX+DemoX+Demo_Course')
|
||||
.attr('children', ['id', 'courseId'], (id, courseId) => {
|
||||
Factory.reset('topic');
|
||||
return Factory.buildList('topic', 2, null, {
|
||||
topicPrefix: `${id}`,
|
||||
enabledInContext: false,
|
||||
topicNamePrefix: `${id}`,
|
||||
usageKey: `${courseId.replace('course-v1:', 'block-v1:')} +type@vertical+block@`,
|
||||
discussionCount: 1,
|
||||
questionCount: 1,
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
import './inContextTopics.factory';
|
||||
@@ -4,8 +4,6 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { getApiBaseUrl } from '../../../data/constants';
|
||||
|
||||
export const getCourseTopicsApiUrl = () => `${getApiBaseUrl()}/api/discussion/v3/course_topics/`;
|
||||
|
||||
export async function getCourseTopicsV3(courseId) {
|
||||
const url = `${getApiBaseUrl()}/api/discussion/v3/course_topics/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { getCourseTopicsApiUrl, getCourseTopicsV3 } from './api';
|
||||
import { fetchCourseTopicsV3 } from './thunks';
|
||||
|
||||
import './__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const courseId2 = 'course-v1:edX+TestX+Test_Course2';
|
||||
const courseTopicsApiUrl = getCourseTopicsApiUrl();
|
||||
|
||||
let axiosMock = null;
|
||||
let store;
|
||||
|
||||
describe('In context topic api tests', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
test('successfully get topics', async () => {
|
||||
axiosMock.onGet(`${courseTopicsApiUrl}${courseId}`)
|
||||
.reply(200, (Factory.buildList('topic', 1, null, {
|
||||
topicPrefix: 'noncourseware-topic',
|
||||
enabledInContext: true,
|
||||
topicNamePrefix: 'general-topic',
|
||||
usageKey: '',
|
||||
courseware: false,
|
||||
discussionCount: 1,
|
||||
questionCount: 1,
|
||||
}).concat(Factory.buildList('section', 2, null, { topicPrefix: 'courseware' })))
|
||||
.concat(Factory.buildList('archived-topics', 2, null)));
|
||||
|
||||
const response = await getCourseTopicsV3(courseId);
|
||||
|
||||
expect(response).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('failed to fetch topics', async () => {
|
||||
axiosMock.onGet(`${courseTopicsApiUrl}${courseId2}`)
|
||||
.reply(404);
|
||||
await executeThunk(fetchCourseTopicsV3(courseId2), store.dispatch, store.getState);
|
||||
|
||||
expect(store.getState().inContextTopics.status).toEqual('failed');
|
||||
});
|
||||
|
||||
it('denied to fetch topics', async () => {
|
||||
axiosMock.onGet(`${courseTopicsApiUrl}${courseId}`)
|
||||
.reply(403, {});
|
||||
await executeThunk(fetchCourseTopicsV3(courseId), store.dispatch);
|
||||
|
||||
expect(store.getState().inContextTopics.status).toEqual('denied');
|
||||
});
|
||||
});
|
||||
@@ -1,186 +0,0 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { getCourseTopicsApiUrl } from './api';
|
||||
import { fetchCourseTopicsV3 } from './thunks';
|
||||
|
||||
import './__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const courseTopicsApiUrl = getCourseTopicsApiUrl();
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
describe('Redux in context topics tests', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
Factory.resetAll();
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
async function setupMockData() {
|
||||
axiosMock.onGet(`${courseTopicsApiUrl}${courseId}`)
|
||||
.reply(200, (Factory.buildList('topic', 1, null, {
|
||||
topicPrefix: 'noncourseware-topic',
|
||||
enabledInContext: true,
|
||||
topicNamePrefix: 'general-topic',
|
||||
usageKey: '',
|
||||
courseware: false,
|
||||
discussionCount: 1,
|
||||
questionCount: 1,
|
||||
}).concat(Factory.buildList('section', 2, null, { topicPrefix: 'courseware' })))
|
||||
.concat(Factory.buildList('archived-topics', 2, null)));
|
||||
|
||||
await executeThunk(fetchCourseTopicsV3(courseId), store.dispatch, store.getState);
|
||||
const state = store.getState();
|
||||
return state;
|
||||
}
|
||||
|
||||
test('successfully load initial states in redux', async () => {
|
||||
executeThunk(fetchCourseTopicsV3(courseId), store.dispatch, store.getState);
|
||||
const state = store.getState();
|
||||
|
||||
expect(state.inContextTopics.status).toEqual('in-progress');
|
||||
expect(state.inContextTopics.topics).toHaveLength(0);
|
||||
expect(state.inContextTopics.coursewareTopics).toHaveLength(0);
|
||||
expect(state.inContextTopics.nonCoursewareTopics).toHaveLength(0);
|
||||
expect(state.inContextTopics.nonCoursewareIds).toHaveLength(0);
|
||||
expect(state.inContextTopics.units).toHaveLength(0);
|
||||
expect(state.inContextTopics.archivedTopics).toHaveLength(0);
|
||||
expect(state.inContextTopics.filter).toEqual('');
|
||||
});
|
||||
|
||||
test('successfully store all api data of courseware and noncourseware in redux', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const { coursewareTopics, nonCoursewareTopics } = state.inContextTopics;
|
||||
|
||||
expect(coursewareTopics).toHaveLength(2);
|
||||
expect(nonCoursewareTopics).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('successfully store the combined list of courseware and noncourseware topics in topics', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const {
|
||||
coursewareTopics, nonCoursewareTopics, archivedTopics, topics,
|
||||
} = state.inContextTopics;
|
||||
|
||||
expect(topics).toHaveLength(coursewareTopics.length + nonCoursewareTopics.length + archivedTopics.length);
|
||||
});
|
||||
});
|
||||
|
||||
test('successfully get the posts ', async () => {
|
||||
setupMockData().then((state) => {
|
||||
expect(state?.inContextTopics?.status).toEqual('successful');
|
||||
});
|
||||
});
|
||||
|
||||
test('successfully checked that the coursewaretopic has three levels', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const { coursewareTopics } = state.inContextTopics;
|
||||
|
||||
// contain chapter at first level
|
||||
coursewareTopics.forEach((chapter, index) => {
|
||||
expect(chapter.courseware).toEqual(true);
|
||||
expect(chapter.id).toEqual(`courseware-topic-${index + 1}`);
|
||||
expect(chapter.type).toEqual('chapter');
|
||||
expect(chapter).toHaveProperty('blockId');
|
||||
expect(chapter).toHaveProperty('lmsWebUrl');
|
||||
expect(chapter).toHaveProperty('legacyWebUrl');
|
||||
expect(chapter).toHaveProperty('studentViewUrl');
|
||||
|
||||
// contain section at second level
|
||||
chapter.children.forEach((section, secIndex) => {
|
||||
expect(section.id).toEqual(`section-topic-${secIndex + 1}`);
|
||||
expect(section.type).toEqual('sequential');
|
||||
expect(section).toHaveProperty('blockId');
|
||||
expect(section).toHaveProperty('lmsWebUrl');
|
||||
expect(section).toHaveProperty('legacyWebUrl');
|
||||
expect(section).toHaveProperty('studentViewUrl');
|
||||
|
||||
// contain sub section at third level
|
||||
section.children.forEach((subSection, subSecIndex) => {
|
||||
expect(subSection.enabledInContext).toEqual(true);
|
||||
expect(subSection.id).toEqual(`${section.id}-${subSecIndex + 1}`);
|
||||
expect(subSection).toHaveProperty('usageKey');
|
||||
expect(subSection).not.toHaveProperty('blockId');
|
||||
expect(subSection?.threadCounts?.discussion).toEqual(1);
|
||||
expect(subSection?.threadCounts?.question).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('successfully checked that the noncoursewaretopic have proper attributes', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const { nonCoursewareTopics } = state.inContextTopics;
|
||||
|
||||
nonCoursewareTopics.forEach((topic, index) => {
|
||||
expect(topic.usageKey).toEqual('');
|
||||
expect(topic.id).toEqual(`noncourseware-topic-${index + 1}`);
|
||||
expect(topic.name).toEqual(`general-topic-${index + 1}`);
|
||||
expect(topic.enabledInContext).toEqual(true);
|
||||
expect(topic?.threadCounts?.discussion).toEqual(1);
|
||||
expect(topic?.threadCounts?.question).toEqual(1);
|
||||
expect(topic).not.toHaveProperty('blockId');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('nonCoursewareIds successfully contains ids of noncourseware topics', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const { nonCoursewareIds, nonCoursewareTopics } = state.inContextTopics;
|
||||
|
||||
nonCoursewareIds.forEach((nonCoursewareId, index) => {
|
||||
expect(nonCoursewareTopics[index].id).toEqual(nonCoursewareId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('selectUnits successfully contains all sub sections', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const subSections = state.inContextTopics.coursewareTopics?.map(x => x.children)
|
||||
?.flat()?.map(x => x.children)?.flat();
|
||||
const { units } = state.inContextTopics;
|
||||
|
||||
units.forEach(unit => {
|
||||
const subSection = subSections.find(x => x.id === unit.id);
|
||||
expect(subSection?.id).toEqual(unit.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('successfully stored archived data in redux', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const { archivedTopics } = state.inContextTopics;
|
||||
|
||||
archivedTopics.forEach((archivedTopic, index) => {
|
||||
expect(archivedTopic?.enabledInContext).toEqual(false);
|
||||
expect(archivedTopic?.id).toEqual(`archived-${index + 1}`);
|
||||
expect(archivedTopic?.usageKey).not.toBeNull();
|
||||
expect(archivedTopic?.threadCounts?.discussion).toEqual(1);
|
||||
expect(archivedTopic?.threadCounts?.question).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,147 +0,0 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { getCourseTopicsApiUrl } from './api';
|
||||
import {
|
||||
selectArchivedTopics,
|
||||
selectCoursewareTopics,
|
||||
selectLoadingStatus,
|
||||
selectNonCoursewareIds,
|
||||
selectNonCoursewareTopics,
|
||||
selectTopics,
|
||||
selectUnits,
|
||||
} from './selectors';
|
||||
import { fetchCourseTopicsV3 } from './thunks';
|
||||
|
||||
import './__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const courseTopicsApiUrl = getCourseTopicsApiUrl();
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
describe('In Context Topics Selector test cases', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
Factory.resetAll();
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
async function setupMockData() {
|
||||
axiosMock.onGet(`${courseTopicsApiUrl}${courseId}`)
|
||||
.reply(200, (Factory.buildList('topic', 1, null, {
|
||||
topicPrefix: 'noncourseware-topic',
|
||||
enabledInContext: true,
|
||||
topicNamePrefix: 'general-topic',
|
||||
usageKey: '',
|
||||
courseware: false,
|
||||
discussionCount: 1,
|
||||
questionCount: 1,
|
||||
}).concat(Factory.buildList('section', 2, null, { topicPrefix: 'courseware' })))
|
||||
.concat(Factory.buildList('archived-topics', 2, null)));
|
||||
await executeThunk(fetchCourseTopicsV3(courseId), store.dispatch, store.getState);
|
||||
|
||||
const state = store.getState();
|
||||
return state;
|
||||
}
|
||||
|
||||
test('should return topics list', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const topics = selectTopics(state);
|
||||
|
||||
expect(topics).not.toBeUndefined();
|
||||
topics.forEach(data => {
|
||||
const topicFunc = jest.fn((topic) => {
|
||||
if (topic.id.includes('noncourseware-topic')) { return true; }
|
||||
if (topic.id.includes('courseware-topic')) { return true; }
|
||||
if (topic.id.includes('archived')) { return true; }
|
||||
return false;
|
||||
});
|
||||
topicFunc(data);
|
||||
expect(topicFunc).toHaveReturnedWith(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should return courseware topics list', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const coursewareTopics = selectCoursewareTopics(state);
|
||||
|
||||
expect(coursewareTopics).not.toBeUndefined();
|
||||
coursewareTopics.forEach((topic, index) => {
|
||||
expect(topic?.id).toEqual(`courseware-topic-${index + 1}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should return noncourseware topics list', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const nonCoursewareTopics = selectNonCoursewareTopics(state);
|
||||
|
||||
expect(nonCoursewareTopics).not.toBeUndefined();
|
||||
nonCoursewareTopics.forEach((topic, index) => {
|
||||
expect(topic?.id).toEqual(`noncourseware-topic-${index + 1}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should return noncourseware ids list', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const nonCoursewareIds = selectNonCoursewareIds(state);
|
||||
|
||||
expect(nonCoursewareIds).not.toBeUndefined();
|
||||
nonCoursewareIds.forEach((id, index) => {
|
||||
expect(id).toEqual(`noncourseware-topic-${index + 1}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should return units list', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const units = selectUnits(state);
|
||||
|
||||
expect(units).not.toBeUndefined();
|
||||
units.forEach(unit => {
|
||||
expect(unit?.usageKey).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should return archived topics list', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const archivedTopics = selectArchivedTopics(state);
|
||||
|
||||
expect(archivedTopics).not.toBeUndefined();
|
||||
archivedTopics.forEach((topic, index) => {
|
||||
expect(topic.id).toEqual(`archived-${index + 1}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should return loading status successful', async () => {
|
||||
setupMockData().then((state) => {
|
||||
const status = selectLoadingStatus(state);
|
||||
|
||||
expect(status).toEqual('successful');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,7 +44,6 @@ export const {
|
||||
fetchCourseTopicsRequest,
|
||||
fetchCourseTopicsSuccess,
|
||||
fetchCourseTopicsFailed,
|
||||
fetchCourseTopicsDenied,
|
||||
setFilter,
|
||||
setSortBy,
|
||||
} = topicsSlice.actions;
|
||||
|
||||
@@ -3,11 +3,8 @@ import { reduce } from 'lodash';
|
||||
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { getHttpErrorStatus } from '../../utils';
|
||||
import { getCourseTopicsV3 } from './api';
|
||||
import {
|
||||
fetchCourseTopicsDenied, fetchCourseTopicsFailed, fetchCourseTopicsRequest, fetchCourseTopicsSuccess,
|
||||
} from './slices';
|
||||
import { fetchCourseTopicsFailed, fetchCourseTopicsRequest, fetchCourseTopicsSuccess } from './slices';
|
||||
|
||||
function normalizeTopicsV3(topics) {
|
||||
const coursewareUnits = reduce(topics, (arrayOfUnits, chapter) => {
|
||||
@@ -60,11 +57,7 @@ export function fetchCourseTopicsV3(courseId) {
|
||||
const data = await getCourseTopicsV3(courseId);
|
||||
dispatch(fetchCourseTopicsSuccess(normalizeTopicsV3(data)));
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
dispatch(fetchCourseTopicsDenied());
|
||||
} else {
|
||||
dispatch(fetchCourseTopicsFailed());
|
||||
}
|
||||
dispatch(fetchCourseTopicsFailed());
|
||||
logError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './comments';
|
||||
export * from './discussions-home';
|
||||
export * from './post-comments';
|
||||
export * from './posts';
|
||||
export * from './topics';
|
||||
|
||||
@@ -88,7 +88,7 @@ function LearnerPostsView({ intl }) {
|
||||
onClick={() => history.push(discussionsPath(Routes.LEARNERS.PATH, { courseId })(location))}
|
||||
alt={intl.formatMessage(messages.back)}
|
||||
/>
|
||||
<div className="text-primary-500 font-style font-weight-bold py-2.5">
|
||||
<div className="text-primary-500 font-style-normal font-family-inter font-weight-bold py-2.5">
|
||||
{intl.formatMessage(messages.activityForLearner, { username: capitalize(username) })}
|
||||
</div>
|
||||
<div style={{ padding: '18px' }} />
|
||||
|
||||
@@ -40,7 +40,7 @@ function LearnerCard({
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill">
|
||||
<div className="d-flex align-items-center flex-fill">
|
||||
<div
|
||||
className="text-truncate font-weight-500 font-size-14 text-primary-500 font-style"
|
||||
className="text-truncate font-weight-500 font-size-14 text-primary-500 font-style-normal font-family-inter"
|
||||
>
|
||||
{learner.username}
|
||||
</div>
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Icon, IconButton, Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
EndorsementStatus, PostsPages, RequestStatus, ThreadType,
|
||||
} from '../../data/constants';
|
||||
import { useDispatchWithState } from '../../data/hooks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useIsOnDesktop } from '../data/hooks';
|
||||
import { EmptyPage } from '../empty-posts';
|
||||
import { Post } from '../posts';
|
||||
import { fetchThread } from '../posts/data/thunks';
|
||||
import { discussionsPath } from '../utils';
|
||||
import { ResponseEditor } from './comments/comment';
|
||||
import CommentsSort from './comments/CommentsSort';
|
||||
import CommentsView from './comments/CommentsView';
|
||||
import { useCommentsCount, usePost } from './data/hooks';
|
||||
import { selectCommentsStatus } from './data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
function PostCommentsView({ intl }) {
|
||||
const [isLoading, submitDispatch] = useDispatchWithState();
|
||||
const { postId } = useParams();
|
||||
const thread = usePost(postId);
|
||||
const commentsStatus = useSelector(selectCommentsStatus);
|
||||
const commentsCount = useCommentsCount(postId);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const {
|
||||
courseId, learnerUsername, category, topicId, page, enableInContextSidebar,
|
||||
} = useContext(DiscussionContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread) { submitDispatch(fetchThread(postId, courseId, true)); }
|
||||
setAddingResponse(false);
|
||||
}, [postId]);
|
||||
|
||||
if (!thread) {
|
||||
if (!isLoading) {
|
||||
return (
|
||||
<EmptyPage title={intl.formatMessage(messages.noThreadFound)} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
}}
|
||||
>
|
||||
<Spinner animation="border" variant="primary" data-testid="loading-indicator" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOnDesktop && (
|
||||
enableInContextSidebar ? (
|
||||
<>
|
||||
<div className="px-4 py-1.5 bg-white">
|
||||
<Button
|
||||
variant="plain"
|
||||
className="px-0 line-height-24 py-0 my-1.5 border-0 font-weight-normal font-style text-primary-500"
|
||||
iconBefore={ArrowBack}
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
size="sm"
|
||||
>
|
||||
{intl.formatMessage(messages.backAlt)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border-bottom border-light-400" />
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
src={ArrowBack}
|
||||
iconAs={Icon}
|
||||
style={{ padding: '18px' }}
|
||||
size="inline"
|
||||
className="ml-4 mt-4"
|
||||
onClick={() => history.push(discussionsPath(PostsPages[page], {
|
||||
courseId, learnerUsername, category, topicId,
|
||||
})(location))}
|
||||
alt={intl.formatMessage(messages.backAlt)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className="discussion-comments d-flex flex-column card border-0 post-card-margin post-card-padding"
|
||||
>
|
||||
<Post post={thread} handleAddResponseButton={() => setAddingResponse(true)} />
|
||||
{!thread.closed && (
|
||||
<ResponseEditor
|
||||
postId={postId}
|
||||
handleCloseEditor={() => setAddingResponse(false)}
|
||||
addingResponse={addingResponse}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!!commentsCount && commentsStatus === RequestStatus.SUCCESSFUL && <CommentsSort />}
|
||||
{thread.type === ThreadType.DISCUSSION && (
|
||||
<CommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.DISCUSSION}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
)}
|
||||
{thread.type === ThreadType.QUESTION && (
|
||||
<>
|
||||
<CommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.ENDORSED}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
<CommentsView
|
||||
postId={postId}
|
||||
intl={intl}
|
||||
postType={thread.type}
|
||||
endorsed={EndorsementStatus.UNENDORSED}
|
||||
isClosed={thread.closed}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PostCommentsView.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PostCommentsView);
|
||||
@@ -1,92 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Dropdown, ModalPopup, useToggle,
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
ExpandLess, ExpandMore,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import { selectCommentSortOrder } from '../data/selectors';
|
||||
import { setCommentSortOrder } from '../data/slices';
|
||||
import messages from '../messages';
|
||||
|
||||
function CommentSortDropdown({
|
||||
intl,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const sortedOrder = useSelector(selectCommentSortOrder);
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
|
||||
const handleActions = (reverseOrder) => {
|
||||
close();
|
||||
dispatch(setCommentSortOrder(reverseOrder));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="comments-sort d-flex justify-content-end mx-4 mt-2">
|
||||
<Button
|
||||
alt={intl.formatMessage(messages.actionsAlt)}
|
||||
ref={setTarget}
|
||||
variant="tertiary"
|
||||
onClick={open}
|
||||
size="sm"
|
||||
iconAfter={isOpen ? ExpandLess : ExpandMore}
|
||||
>
|
||||
{intl.formatMessage(messages.commentSort, {
|
||||
sort: sortedOrder,
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="actions-dropdown">
|
||||
<ModalPopup
|
||||
onClose={close}
|
||||
positionRef={target}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<div
|
||||
className="bg-white p-1 shadow d-flex flex-column"
|
||||
data-testid="comment-sort-dropdown-modal-popup"
|
||||
>
|
||||
<Dropdown.Item
|
||||
className="d-flex justify-content-start py-1.5 mb-1"
|
||||
as={Button}
|
||||
variant="tertiary"
|
||||
size="inline"
|
||||
onClick={() => handleActions(false)}
|
||||
autoFocus={sortedOrder === false}
|
||||
>
|
||||
{intl.formatMessage(messages.commentSort, {
|
||||
sort: false,
|
||||
})}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
className="d-flex justify-content-start py-1.5"
|
||||
as={Button}
|
||||
variant="tertiary"
|
||||
size="inline"
|
||||
onClick={() => handleActions(true)}
|
||||
autoFocus={sortedOrder === true}
|
||||
>
|
||||
{intl.formatMessage(messages.commentSort, {
|
||||
sort: true,
|
||||
})}
|
||||
</Dropdown.Item>
|
||||
</div>
|
||||
</ModalPopup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CommentSortDropdown.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
};
|
||||
|
||||
export default injectIntl(CommentSortDropdown);
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Spinner } from '@edx/paragon';
|
||||
|
||||
import { EndorsementStatus } from '../../../data/constants';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import { filterPosts, isLastElementOfList } from '../../utils';
|
||||
import { usePostComments } from '../data/hooks';
|
||||
import messages from '../messages';
|
||||
import { Comment, ResponseEditor } from './comment';
|
||||
|
||||
function CommentsView({
|
||||
postType,
|
||||
postId,
|
||||
intl,
|
||||
endorsed,
|
||||
isClosed,
|
||||
}) {
|
||||
const {
|
||||
comments,
|
||||
hasMorePages,
|
||||
isLoading,
|
||||
handleLoadMoreResponses,
|
||||
} = usePostComments(postId, endorsed);
|
||||
|
||||
const endorsedComments = useMemo(() => [...filterPosts(comments, 'endorsed')], [comments]);
|
||||
const unEndorsedComments = useMemo(() => [...filterPosts(comments, 'unendorsed')], [comments]);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
|
||||
const handleDefinition = (message, commentsLength) => (
|
||||
<div
|
||||
className="mx-4 my-14px text-gray-700 font-style"
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
>
|
||||
{intl.formatMessage(message, { num: commentsLength })}
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleComments = (postComments, showLoadMoreResponses = false) => (
|
||||
<div className="mx-4" role="list">
|
||||
{postComments.map((comment) => (
|
||||
<Comment
|
||||
comment={comment}
|
||||
key={comment.id}
|
||||
postType={postType}
|
||||
isClosedPost={isClosed}
|
||||
marginBottom={isLastElementOfList(postComments, comment)}
|
||||
/>
|
||||
))}
|
||||
{hasMorePages && !isLoading && !showLoadMoreResponses && (
|
||||
<Button
|
||||
onClick={handleLoadMoreResponses}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="px-4 mt-3 border-0 line-height-24 py-0 mb-2 font-style font-weight-500 font-size-14"
|
||||
data-testid="load-more-comments"
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreResponses)}
|
||||
</Button>
|
||||
)}
|
||||
{isLoading && !showLoadMoreResponses && (
|
||||
<div className="mb-2 mt-3 d-flex justify-content-center">
|
||||
<Spinner animation="border" variant="primary" className="spinner-dimentions" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{((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
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CommentsView.propTypes = {
|
||||
postId: PropTypes.string.isRequired,
|
||||
postType: PropTypes.string.isRequired,
|
||||
isClosed: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
endorsed: PropTypes.oneOf([
|
||||
EndorsementStatus.ENDORSED, EndorsementStatus.UNENDORSED, EndorsementStatus.DISCUSSION,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CommentsView);
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { EndorsementStatus } from '../../../data/constants';
|
||||
import { useDispatchWithState } from '../../../data/hooks';
|
||||
import { selectThread } from '../../posts/data/selectors';
|
||||
import { markThreadAsRead } from '../../posts/data/thunks';
|
||||
import {
|
||||
selectCommentSortOrder, selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages,
|
||||
} from './selectors';
|
||||
import { fetchThreadComments } from './thunks';
|
||||
|
||||
export function usePost(postId) {
|
||||
const dispatch = useDispatch();
|
||||
const thread = useSelector(selectThread(postId));
|
||||
|
||||
useEffect(() => {
|
||||
if (thread && !thread.read) {
|
||||
dispatch(markThreadAsRead(postId));
|
||||
}
|
||||
}, [postId]);
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
export function usePostComments(postId, endorsed = null) {
|
||||
const [isLoading, dispatch] = useDispatchWithState();
|
||||
const comments = useSelector(selectThreadComments(postId, endorsed));
|
||||
const reverseOrder = useSelector(selectCommentSortOrder);
|
||||
const hasMorePages = useSelector(selectThreadHasMorePages(postId, endorsed));
|
||||
const currentPage = useSelector(selectThreadCurrentPage(postId, endorsed));
|
||||
|
||||
const handleLoadMoreResponses = async () => dispatch(fetchThreadComments(postId, {
|
||||
endorsed,
|
||||
page: currentPage + 1,
|
||||
reverseOrder,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchThreadComments(postId, {
|
||||
endorsed,
|
||||
page: 1,
|
||||
reverseOrder,
|
||||
}));
|
||||
}, [postId, reverseOrder]);
|
||||
|
||||
return {
|
||||
comments,
|
||||
hasMorePages,
|
||||
isLoading,
|
||||
handleLoadMoreResponses,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCommentsCount(postId) {
|
||||
const discussions = useSelector(selectThreadComments(postId, EndorsementStatus.DISCUSSION));
|
||||
const endorsedQuestions = useSelector(selectThreadComments(postId, EndorsementStatus.ENDORSED));
|
||||
const unendorsedQuestions = useSelector(selectThreadComments(postId, EndorsementStatus.UNENDORSED));
|
||||
|
||||
return [...discussions, ...endorsedQuestions, ...unendorsedQuestions].length;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as PostCommentsView } from './PostCommentsView';
|
||||
@@ -50,7 +50,8 @@ function PostsList({
|
||||
topicIds,
|
||||
isFilterChanged,
|
||||
};
|
||||
if (showOwnPosts && filters.search === '') {
|
||||
|
||||
if (showOwnPosts) {
|
||||
dispatch(fetchUserPosts(courseId, params));
|
||||
} else {
|
||||
dispatch(fetchThreads(courseId, params));
|
||||
|
||||
@@ -13,7 +13,10 @@ import { fetchCourseTopicsV3 } from '../in-context-topics/data/thunks';
|
||||
import { selectTopics } from '../topics/data/selectors';
|
||||
import { fetchCourseTopics } from '../topics/data/thunks';
|
||||
import { handleKeyDown } from '../utils';
|
||||
import { selectAllThreads, selectTopicThreads } from './data/selectors';
|
||||
import {
|
||||
selectAllThreads,
|
||||
selectTopicThreads,
|
||||
} from './data/selectors';
|
||||
import { setSearchQuery } from './data/slices';
|
||||
import PostFilterBar from './post-filter-bar/PostFilterBar';
|
||||
import PostsList from './PostsList';
|
||||
|
||||
@@ -42,17 +42,17 @@ function PostActionsBar({
|
||||
: <Search />
|
||||
)}
|
||||
{enableInContextSidebar && (
|
||||
<h4 className="d-flex flex-grow-1 font-weight-bold font-style my-0 py-10px align-self-center">
|
||||
<h4 className="d-flex flex-grow-1 font-weight-bold my-0 py-0 align-self-center">
|
||||
{intl.formatMessage(messages.title)}
|
||||
</h4>
|
||||
)}
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && userCanAddThreadInBlackoutDate && (
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && userCanAddThreadInBlackoutDate
|
||||
&& (
|
||||
<>
|
||||
{!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', { 'p-0': enableInContextSidebar })}
|
||||
onClick={() => dispatch(showPostEditor())}
|
||||
size={enableInContextSidebar ? 'md' : 'sm'}
|
||||
>
|
||||
@@ -62,17 +62,13 @@ function PostActionsBar({
|
||||
)}
|
||||
{enableInContextSidebar && (
|
||||
<>
|
||||
<div className="border-right border-light-300 mr-3 ml-1.5 my-10px" />
|
||||
<div className="justify-content-center mt-2.5 mx-3px">
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
onClick={handleCloseInContext}
|
||||
alt={intl.formatMessage(messages.close)}
|
||||
iconClassNames="spinner-dimentions"
|
||||
className="spinner-dimentions"
|
||||
/>
|
||||
</div>
|
||||
<div className="border-right border-light-300 mr-2 ml-3.5 my-2" />
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
onClick={handleCloseInContext}
|
||||
alt={intl.formatMessage(messages.close)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -54,9 +54,9 @@ function DiscussionPostType({
|
||||
value,
|
||||
type,
|
||||
selected,
|
||||
description,
|
||||
icon,
|
||||
}) {
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
// Need to use regular label since Form.Label doesn't support overriding htmlFor
|
||||
return (
|
||||
<label htmlFor={`post-type-${value}`} className="d-flex p-0 my-0 mr-3">
|
||||
@@ -66,11 +66,12 @@ function DiscussionPostType({
|
||||
'border-primary': selected,
|
||||
'border-light-400': !selected,
|
||||
})}
|
||||
style={{ cursor: 'pointer', width: `${enableInContextSidebar ? '10.021rem' : '14.25rem'}` }}
|
||||
style={{ cursor: 'pointer', width: '14.25rem' }}
|
||||
>
|
||||
<Card.Section className="px-4 py-3 d-flex flex-column align-items-center">
|
||||
<Card.Section className="py-3 px-10px d-flex flex-column align-items-center">
|
||||
<span className="text-primary-300 mb-0.5">{icon}</span>
|
||||
<span className="text-gray-700">{type}</span>
|
||||
<span className="text-gray-700 mb-0.5">{type}</span>
|
||||
<span className="x-small text-gray-500">{description}</span>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</label>
|
||||
@@ -81,6 +82,7 @@ DiscussionPostType.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
icon: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
@@ -268,7 +270,7 @@ function PostEditor({
|
||||
resetForm,
|
||||
}) => (
|
||||
<Form className="m-4 card p-4 post-form" onSubmit={handleSubmit}>
|
||||
<h4 className="mb-4 font-style font-size-16" style={{ lineHeight: '16px' }}>
|
||||
<h4 className="mb-4" style={{ lineHeight: '16px' }}>
|
||||
{editExisting
|
||||
? intl.formatMessage(messages.editPostHeading)
|
||||
: intl.formatMessage(messages.addPostHeading)}
|
||||
@@ -442,7 +444,7 @@ function PostEditor({
|
||||
|
||||
<PostPreviewPane htmlNode={values.comment} isPost editExisting={editExisting} />
|
||||
|
||||
<div className="d-flex flex-row mt-n4 w-75 text-primary font-style">
|
||||
<div className="d-flex flex-row mt-n4 w-75 text-primary">
|
||||
{!editExisting && (
|
||||
<>
|
||||
<Form.Group>
|
||||
@@ -459,18 +461,18 @@ function PostEditor({
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
{allowAnonymousToPeers && (
|
||||
<Form.Group>
|
||||
<Form.Checkbox
|
||||
name="anonymousToPeers"
|
||||
checked={values.anonymousToPeers}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<span className="font-size-14">
|
||||
{intl.formatMessage(messages.anonymousToPeersPost)}
|
||||
</span>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Checkbox
|
||||
name="anonymousToPeers"
|
||||
checked={values.anonymousToPeers}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<span className="font-size-14">
|
||||
{intl.formatMessage(messages.anonymousToPeersPost)}
|
||||
</span>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -496,7 +498,7 @@ function PostEditor({
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ function PostFilterBar({
|
||||
const currentFilters = useSelector(selectThreadFilters());
|
||||
const { status } = useSelector(state => state.cohorts);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const selectedCohort = useMemo(() => cohorts.find(cohort => (
|
||||
@@ -137,7 +138,7 @@ function PostFilterBar({
|
||||
className="filter-bar collapsible-card-lg border-0"
|
||||
>
|
||||
<Collapsible.Trigger className="collapsible-trigger border-0">
|
||||
<span className="text-primary-500 pr-4 font-style">
|
||||
<span className="text-primary-700 pr-4">
|
||||
{intl.formatMessage(messages.sortFilterStatus, {
|
||||
own: false,
|
||||
type: currentFilters.postType,
|
||||
|
||||
@@ -41,7 +41,7 @@ function LikeButton({
|
||||
iconClassNames="like-icon-dimentions"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
<div className="font-style">
|
||||
<div className="font-family-inter font-style-normal">
|
||||
{(count && count > 0) ? count : null}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -33,7 +33,7 @@ function Post({
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const courseId = useSelector((state) => state.config.id);
|
||||
const { courseId } = useSelector((state) => state.courseTabs);
|
||||
const topic = useSelector(selectTopic(post.topicId));
|
||||
const getTopicSubsection = useSelector(selectorForUnitSubsection);
|
||||
const topicContext = useSelector(selectTopicContext(post.topicId));
|
||||
@@ -41,6 +41,7 @@ function Post({
|
||||
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
|
||||
const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
|
||||
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
|
||||
const [showHoverCard, setShowHoverCard] = useState(false);
|
||||
const handleAbusedFlag = () => {
|
||||
if (post.abuseFlagged) {
|
||||
dispatch(updateExistingThread(post.id, { flagged: !post.abuseFlagged }));
|
||||
@@ -63,6 +64,12 @@ function Post({
|
||||
hideReportConfirmation();
|
||||
};
|
||||
|
||||
const handleBlurEvent = (e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
setShowHoverCard(false);
|
||||
}
|
||||
};
|
||||
|
||||
const actionHandlers = {
|
||||
[ContentActions.EDIT_CONTENT]: () => history.push({
|
||||
...location,
|
||||
@@ -92,6 +99,10 @@ function Post({
|
||||
className="d-flex flex-column w-100 mw-100 post-card-comment"
|
||||
aria-level={5}
|
||||
data-testid={`post-${post.id}`}
|
||||
onMouseEnter={() => setShowHoverCard(true)}
|
||||
onMouseLeave={() => setShowHoverCard(false)}
|
||||
onFocus={() => setShowHoverCard(true)}
|
||||
onBlur={(e) => handleBlurEvent(e)}
|
||||
>
|
||||
<Confirmation
|
||||
isOpen={isDeleting}
|
||||
@@ -112,23 +123,25 @@ function Post({
|
||||
confirmButtonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
<HoverCard
|
||||
commentOrPost={post}
|
||||
actionHandlers={actionHandlers}
|
||||
handleResponseCommentButton={handleAddResponseButton}
|
||||
addResponseCommentButtonMessage={intl.formatMessage(messages.addResponse)}
|
||||
onLike={() => dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
|
||||
onFollow={() => dispatch(updateExistingThread(post.id, { following: !post.following }))}
|
||||
isClosedPost={post.closed}
|
||||
/>
|
||||
{showHoverCard && (
|
||||
<HoverCard
|
||||
commentOrPost={post}
|
||||
actionHandlers={actionHandlers}
|
||||
handleResponseCommentButton={handleAddResponseButton}
|
||||
addResponseCommentButtonMessage={intl.formatMessage(messages.addResponse)}
|
||||
onLike={() => dispatch(updateExistingThread(post.id, { voted: !post.voted }))}
|
||||
onFollow={() => dispatch(updateExistingThread(post.id, { following: !post.following }))}
|
||||
isClosedPost={post.closed}
|
||||
/>
|
||||
)}
|
||||
<AlertBanner content={post} />
|
||||
<PostHeader post={post} />
|
||||
<div className="d-flex mt-14px text-break font-style text-primary-500">
|
||||
<div className="d-flex mt-14px text-break font-style-normal font-family-inter text-primary-500">
|
||||
<HTMLLoader htmlNode={post.renderedBody} componentId="post" cssClassName="html-loader" testId={post.id} />
|
||||
</div>
|
||||
{topicContext && (
|
||||
{topicContext && topic && (
|
||||
<div
|
||||
className={classNames('mt-14px mb-1 font-style font-size-12',
|
||||
className={classNames('mt-14px mb-1 font-style-normal font-family-inter font-size-12',
|
||||
{ 'w-100': enableInContextSidebar })}
|
||||
style={{ lineHeight: '20px' }}
|
||||
>
|
||||
@@ -137,7 +150,7 @@ function Post({
|
||||
destination={topicContext.unitLink}
|
||||
target="_top"
|
||||
>
|
||||
{(topicContext && !topic)
|
||||
{enableInContextSidebar
|
||||
? (
|
||||
<>
|
||||
<span className="w-auto">{topicContext.chapterName}</span>
|
||||
|
||||
@@ -6,9 +6,15 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon, IconButton, OverlayTrigger, Tooltip,
|
||||
} from '@edx/paragon';
|
||||
import { Locked, People } from '@edx/paragon/icons';
|
||||
import {
|
||||
Locked,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import { StarFilled, StarOutline } from '../../../components/icons';
|
||||
import {
|
||||
People,
|
||||
StarFilled,
|
||||
StarOutline,
|
||||
} from '../../../components/icons';
|
||||
import { selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import { updateExistingThread } from '../data/thunks';
|
||||
import LikeButton from './LikeButton';
|
||||
@@ -54,22 +60,23 @@ function PostFooter({
|
||||
)}
|
||||
<div className="d-flex flex-fill justify-content-end align-items-center">
|
||||
{post.groupId && userHasModerationPrivileges && (
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`visibility-${post.id}-tooltip`}>{post.groupName}</Tooltip>
|
||||
)}
|
||||
>
|
||||
<span data-testid="cohort-icon">
|
||||
<Icon
|
||||
src={People}
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '20px',
|
||||
}}
|
||||
className="text-gray-500"
|
||||
/>
|
||||
<>
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`visibility-${post.id}-tooltip`}>{post.groupName}</Tooltip>
|
||||
)}
|
||||
>
|
||||
<span data-testid="cohort-icon">
|
||||
<People />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
<span
|
||||
className="text-gray-700 mx-1.5 font-weight-500"
|
||||
style={{ fontSize: '16px' }}
|
||||
>
|
||||
·
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</>
|
||||
)}
|
||||
|
||||
{post.closed
|
||||
@@ -86,8 +93,8 @@ function PostFooter({
|
||||
style={{
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
marginLeft: '19.5px',
|
||||
}}
|
||||
className="ml-3"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
|
||||
@@ -108,17 +108,7 @@ function PostHeader({
|
||||
&& <Badge variant="success">{intl.formatMessage(messages.answered)}</Badge>}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<h5
|
||||
className="mb-0 font-style text-primary-500"
|
||||
style={{ lineHeight: '21px' }}
|
||||
aria-level="1"
|
||||
tabIndex="-1"
|
||||
accessKey="h"
|
||||
>
|
||||
{post.title}
|
||||
</h5>
|
||||
)}
|
||||
: <h5 className="mb-0 font-style-normal font-family-inter text-primary-500" style={{ lineHeight: '21px' }} aria-level="1" tabIndex="-1" accessKey="h">{post.title}</h5>}
|
||||
<AuthorLabel
|
||||
author={post.author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={post.authorLabel}
|
||||
|
||||
@@ -74,15 +74,15 @@ function PostLink({
|
||||
<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 })
|
||||
}
|
||||
classNames('font-weight-500 font-size-14 text-primary-500 font-style-normal font-family-inter 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"
|
||||
class="text-gray-700 font-weight-normal font-size-14 font-style-normal font-family-inter align-bottom"
|
||||
>
|
||||
{isPostPreviewAvailable(post.previewBody)
|
||||
? post.previewBody
|
||||
@@ -107,11 +107,10 @@ function PostLink({
|
||||
)}
|
||||
|
||||
{post.pinned && (
|
||||
<Icon
|
||||
src={PushPin}
|
||||
className={`post-summary-icons-dimensions text-gray-700
|
||||
${canSeeReportedBadge || showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
/>
|
||||
<Icon
|
||||
src={PushPin}
|
||||
className={`post-summary-icons-dimensions text-gray-700 ${canSeeReportedBadge || showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,16 @@ import {
|
||||
Badge, Icon, OverlayTrigger, Tooltip,
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
|
||||
Locked,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import { People, QuestionAnswer, QuestionAnswerOutline } from '../../../components/icons';
|
||||
import {
|
||||
People,
|
||||
QuestionAnswer,
|
||||
QuestionAnswerOutline,
|
||||
StarFilled,
|
||||
StarOutline, ThumbUpFilled, ThumbUpOutline,
|
||||
} from '../../../components/icons';
|
||||
import timeLocale from '../../common/time-locale';
|
||||
import { selectUserHasModerationPrivileges } from '../../data/selectors';
|
||||
import messages from './messages';
|
||||
@@ -26,8 +32,9 @@ function PostSummaryFooter({
|
||||
}) {
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
timeago.register('time-locale', timeLocale);
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center text-gray-700" style={{ height: '24px' }}>
|
||||
<div className="d-flex align-items-center text-gray-700">
|
||||
<div className="d-flex align-items-center mr-4.5">
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
@@ -36,11 +43,11 @@ function PostSummaryFooter({
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon src={post.voted ? ThumbUpFilled : ThumbUpOutline} className="post-summary-like-dimensions mr-0.5">
|
||||
<Icon src={post.voted ? ThumbUpFilled : ThumbUpOutline} className="post-summary-icons-dimensions mr-0.5">
|
||||
<span className="sr-only">{' '}{intl.formatMessage(post.voted ? messages.likedPost : messages.postLikes)}</span>
|
||||
</Icon>
|
||||
</OverlayTrigger>
|
||||
<div className="font-style">
|
||||
<div className="font-family-inter font-style-normal">
|
||||
{(post.voteCount && post.voteCount > 0) ? post.voteCount : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,14 +60,12 @@ function PostSummaryFooter({
|
||||
)}
|
||||
>
|
||||
<Icon src={post.following ? StarFilled : StarOutline} className="post-summary-icons-dimensions mr-0.5">
|
||||
<span className="sr-only">
|
||||
{' '}{intl.formatMessage(post.following ? messages.srOnlyFollowDescription : messages.srOnlyUnFollowDescription)}
|
||||
</span>
|
||||
<span className="sr-only">{' '}{ intl.formatMessage(post.following ? messages.srOnlyFollowDescription : messages.srOnlyUnFollowDescription)}</span>
|
||||
</Icon>
|
||||
</OverlayTrigger>
|
||||
|
||||
{preview && post.commentCount > 1 && (
|
||||
<div className="d-flex align-items-center ml-4.5 text-gray-700 font-style font-size-12">
|
||||
<div className="d-flex align-items-center ml-4.5">
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`follow-${post.id}-tooltip`}>
|
||||
@@ -68,10 +73,7 @@ function PostSummaryFooter({
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
src={post.unreadCommentCount ? QuestionAnswer : QuestionAnswerOutline}
|
||||
className="post-summary-comment-count-dimensions mr-0.5"
|
||||
>
|
||||
<Icon src={post.unreadCommentCount ? QuestionAnswer : QuestionAnswerOutline} className="post-summary-icons-dimensions mr-0.5">
|
||||
<span className="sr-only">{' '} {intl.formatMessage(messages.activity)}</span>
|
||||
</Icon>
|
||||
</OverlayTrigger>
|
||||
@@ -85,22 +87,46 @@ function PostSummaryFooter({
|
||||
)}
|
||||
<div className="d-flex flex-fill justify-content-end align-items-center">
|
||||
{post.groupId && userHasModerationPrivileges && (
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`visibility-${post.id}-tooltip`}>{post.groupName}</Tooltip>
|
||||
)}
|
||||
>
|
||||
<span data-testid="cohort-icon" className="mr-2">
|
||||
<Icon
|
||||
src={People}
|
||||
className="text-gray-500 post-summary-icons-dimensions"
|
||||
/>
|
||||
<>
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`visibility-${post.id}-tooltip`}>{post.groupName}</Tooltip>
|
||||
)}
|
||||
>
|
||||
<span data-testid="cohort-icon" className="post-summary-icons-dimensions">
|
||||
<People />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
<span
|
||||
className="text-gray-700 mx-1.5 font-weight-500"
|
||||
style={{ fontSize: '16px' }}
|
||||
>
|
||||
·
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</>
|
||||
)}
|
||||
<span title={post.createdAt} className="text-gray-700 post-summary-timestamp ml-0.5">
|
||||
<span title={post.createdAt} className="text-gray-700 post-summary-timestamp">
|
||||
{timeago.format(post.createdAt, 'time-locale')}
|
||||
</span>
|
||||
{!preview && post.closed
|
||||
&& (
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip id={`closed-${post.id}-tooltip`}>
|
||||
{intl.formatMessage(messages.postClosed)}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
src={Locked}
|
||||
style={{
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
}}
|
||||
className="ml-3 post-summary-icons-dimensions"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,9 @@ import { generatePath, useRouteMatch } from 'react-router';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
CheckCircle, CheckCircleOutline, Delete, Edit, Pin, QuestionAnswer, Report, Verified, VerifiedOutline,
|
||||
CheckCircle,
|
||||
CheckCircleOutline,
|
||||
Delete, Edit, Pin, QuestionAnswer, Report, Verified, VerifiedOutline,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import { InsertLink } from '../components/icons';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "مواد المساق",
|
||||
"learn.course.tabs.navigation.overflow.menu": "المزيد...",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "إضافة رد",
|
||||
"discussions.comments.comment.addComment": "إضافة تعليق ",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "تم إبلاغ الطاقم عن هذا المحتوى لمراجعته.",
|
||||
"discussions.actions.back.alt": "العودة إلى القائمة",
|
||||
"discussions.comments.comment.responseCount": "{num، plural, =0 {دون رد} one {تم إظهار ردّ واحد} two {تم إظهار ردّين} few {تم إظهار # ردود} many {تم إظهار # ردًا} other {تم إظهار # ردود}}",
|
||||
@@ -36,9 +36,9 @@
|
||||
"discussions.editor.comments.editReasonCode": "سبب التعديل",
|
||||
"discussions.editor.posts.editReasonCode.error": "حدد سبب التعديل",
|
||||
"discussions.comment.comments.editedBy": "عدّله",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "السبب",
|
||||
"discussions.post.closedBy": "تم إقفال المنشور من طرف",
|
||||
"discussion.comment.repliesHeading": "{count, plural, =0 {لم يضف أي رد} one {أضيف رد واحد} two {أضيف ردان} few {أضيفت # ردود} many {أضيف # ردًا} other {أضيفت # ردود} على الرد",
|
||||
"discussion.comment.time": "منذ {time}",
|
||||
"discussion.thread.notFound": "المناقشة غير موجودة",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
@@ -141,7 +141,7 @@
|
||||
"discussions.post.editor.anonymousPost": "النشر كمجهول",
|
||||
"discussions.post.editor.anonymousToPeersPost": "انشر ﻷقرانك كمجهول",
|
||||
"discussions.editor.posts.editReasonCode": "سبب التعديل",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.editor.posts.showPreview.button": "عرض المعاينة",
|
||||
"discussions.topic.noName.label": "تصنيف دون اسم",
|
||||
"discussions.subtopic.noName.label": "تصنيف فرعي دون اسم",
|
||||
"discussions.posts.filter.showALl": "عرض الكل",
|
||||
@@ -163,7 +163,6 @@
|
||||
"discussions.posts.sort.voteCount": "الأكثر إعجابًا",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {جميع}\n true {ما كتبته من}\n other {{own}}\n } \n {type, select,\ndiscussion {المناقشات}\nquestion {الأسئلة}\nall {المنشورات}\nother {{type}}\n}\n {status, select,\n statusAll {}\n statusUnread {غير المقروءة}\n statusFollowing {التي تتابعها}\n statusReported {المُبلّغ عنها}\n statusUnanswered {دون إجابة}\n statusUnresponded {دون رد}\n other {{status}}\n } و المنشورة {cohortType, select,\n all {}\n group {ضمن {cohort}}\n other {{cohortType}}\n }، مرتبة حسب {sort, select,\n lastActivityAt {أحدث نشاط}\n commentCount {أكثر نشاط}\n voteCount {أكثر إعجاب}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "مجهول",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "آخر رد {time}",
|
||||
"discussions.post.postedOn": "منشور في {time} من طرف {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "تم الإبلاغ",
|
||||
|
||||
@@ -1,211 +1,210 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Kursmaterial",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Mehr...",
|
||||
"discussions.comments.comment.addComment": "Kommentar hinzufügen",
|
||||
"discussions.comments.comment.addResponse": "Fügen Sie eine Antwort hinzu",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Inhalte, die den Kursmitarbeitern zur Überprüfung gemeldet wurden",
|
||||
"discussions.actions.back.alt": "Zurück zur Liste",
|
||||
"discussions.comments.comment.responseCount": "{num, plural, =0 {Keine Antworten} one {# Antwort wird angezeigt} other {# Antworten werden angezeigt} }",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural, =0 {Keine empfohlenen Antworten} one {# empfohlene Antworten werden angezeigt} other {# empfohlene Antworten werden angezeigt} }",
|
||||
"discussions.comments.comment.loadMoreComments": "Weitere Kommentare laden",
|
||||
"discussions.comments.comment.loadMoreResponses": "Weitere Antworten laden",
|
||||
"discussions.comments.comment.visibility": "Dieser Beitrag ist sichtbar für {group, select, null {Jeder} other {{group}} }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select, discussion {Diskussion} question {Frage} other {{postType}} } gepostet {a0917e9bee14} von.c5z0",
|
||||
"discussions.comments.comment.commentTime": "Gepostet {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Antwort",
|
||||
"discussions.comments.comment.answeredlabel": "Als beantwortet von markiert",
|
||||
"discussions.comments.comment.endorsed": "Bestätigt",
|
||||
"discussions.comments.comment.endorsedlabel": "Bestätigt von",
|
||||
"discussions.actions.label": "Aktionsmenü",
|
||||
"discussions.actions.edit": "Bearbeiten",
|
||||
"discussions.actions.pin": "Veröffentlichen",
|
||||
"discussions.actions.delete": "Löschen",
|
||||
"discussions.editor.submit": "Einreichen",
|
||||
"discussions.editor.submitting": "Übermitteln, einreichen",
|
||||
"discussions.editor.cancel": "Löschen",
|
||||
"discussions.editor.error.empty": "Der Beitragsinhalt darf nicht leer sein.",
|
||||
"discussions.editor.delete.response.title": "Antwort löschen",
|
||||
"discussions.editor.delete.response.description": "Möchten Sie diese Antwort wirklich dauerhaft löschen?",
|
||||
"discussions.editor.delete.comment.title": "Kommentar löschen",
|
||||
"discussions.editor.delete.comment.description": "Möchten Sie diesen Kommentar wirklich dauerhaft löschen?",
|
||||
"discussions.delete.confirmation.button.delete": "Löschen",
|
||||
"discussions.editor.response.response.title": "Unangemessene Inhalte melden?",
|
||||
"discussions.editor.response.description": "Das Diskussionsmoderationsteam überprüft diesen Inhalt und ergreift entsprechende Maßnahmen.",
|
||||
"discussions.editor.report.comment.title": "Unangemessene Inhalte melden?",
|
||||
"discussions.editor.report.comment.description": "Das Diskussionsmoderationsteam überprüft diesen Inhalt und ergreift entsprechende Maßnahmen.",
|
||||
"discussions.editor.comments.editReasonCode": "Grund für die Bearbeitung",
|
||||
"discussions.editor.posts.editReasonCode.error": "Grund für die Bearbeitung auswählen",
|
||||
"discussions.comment.comments.editedBy": "Bearbeitet von",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Grund",
|
||||
"discussions.post.closedBy": "Post geschlossen von",
|
||||
"discussion.comment.time": "{time} vor",
|
||||
"discussion.thread.notFound": "Thema nicht gefunden",
|
||||
"discussions.topics.backAlt": "Zurück zur Themenliste",
|
||||
"discussions.topics.discussions": "{count, plural, =0 {Diskussion} one {# Diskussion} other {# Diskussionen} }",
|
||||
"discussions.topics.questions": "{count, plural, =0 {Frage} one {# Frage} other {# Fragen} }",
|
||||
"discussions.topics.reported": "{reported} gemeldet",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} zuvor gemeldet",
|
||||
"discussions.topics.find.label": "Themen suchen",
|
||||
"discussions.topics.unnamed.section.label": "Unbenannter Abschnitt",
|
||||
"discussions.topics.unnamed.subsection.label": "Unbenannter Unterabschnitt",
|
||||
"discussions.subtopics.unnamed.topic.label": "Unbenanntes Thema",
|
||||
"discussions.topics.title": "Kein Thema vorhanden",
|
||||
"discussions.topics.createTopic": "Bitte kontaktieren Sie Ihren Administrator, um ein Thema zu erstellen",
|
||||
"discussions.topics.nothing": "Hier noch nichts",
|
||||
"discussions.topics.archived.label": "Archiviert",
|
||||
"discussions.learner.reported": "{reported} gemeldet",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} zuvor gemeldet",
|
||||
"discussions.learner.lastLogin": "Zuletzt aktiv {lastActiveTime}",
|
||||
"discussions.learner.loadMostLearners": "Lade weiteres",
|
||||
"discussions.learner.back": "Zurück",
|
||||
"discussions.learner.activityForLearner": "Aktivität für {username}",
|
||||
"discussions.learner.mostActivity": "Die meisten Aktivitäten",
|
||||
"discussions.learner.reportedActivity": "Gemeldete Aktivität",
|
||||
"discussions.learner.recentActivity": "Letzte Aktivität",
|
||||
"discussions.learner.sortFilterStatus": "Alle Lernenden sortiert nach {sort, select, flagged {gemeldete Aktivität} activity {höchste Aktivität} other {{sort}} }",
|
||||
"discussion.learner.allActivity": "Alle Aktivitäten",
|
||||
"discussion.learner.posts": "Beiträge",
|
||||
"discussions.actions.button.alt": "Aktionsmenü",
|
||||
"discussions.actions.copylink": "Link kopieren",
|
||||
"discussions.actions.unpin": "Ablösen",
|
||||
"discussions.confirmation.button.confirm": "Bestätigen",
|
||||
"discussions.actions.close": "Schließen",
|
||||
"discussions.actions.reopen": "Wieder öffnen",
|
||||
"discussions.actions.report": "Melden",
|
||||
"discussions.actions.unreport": "Meldung aufheben",
|
||||
"discussions.actions.endorse": "Befürworten",
|
||||
"discussions.actions.unendorse": "Nicht Befürworten",
|
||||
"discussions.actions.markAnswered": "Als beantwortet markieren",
|
||||
"discussions.actions.unMarkAnswered": "Markierung als beantwortet aufheben",
|
||||
"discussions.modal.confirmation.button.cancel": "Löschen",
|
||||
"discussions.empty.allTopics": "Alle Diskussionsaktivitäten zu diesen Themen werden hier angezeigt.",
|
||||
"discussions.empty.allPosts": "Alle Diskussionsaktivitäten für Ihren Kurs werden hier angezeigt.",
|
||||
"discussions.empty.myPosts": "Beiträge, mit denen Sie interagiert haben, werden hier angezeigt.",
|
||||
"discussions.empty.topic": "Alle Diskussionsaktivitäten zu diesem Thema werden hier angezeigt.",
|
||||
"discussions.empty.title": "Hier noch nichts",
|
||||
"discussions.empty.noPostSelected": "Kein Beitrag ausgewählt",
|
||||
"discussions.empty.noTopicSelected": "Kein Thema ausgewählt",
|
||||
"discussions.sidebar.noResultsFound": "Keine Ergebnisse gefunden",
|
||||
"discussions.sidebar.differentKeywords": "Versuchen Sie, nach anderen Schlüsselwörtern zu suchen",
|
||||
"discussions.sidebar.removeKeywords": "Versuchen Sie, nach anderen Schlüsselwörtern zu suchen oder einige Filter zu entfernen",
|
||||
"discussions.sidebar.removeKeywordsOnly": "Versuchen Sie, nach anderen Schlüsselwörtern zu suchen",
|
||||
"discussions.sidebar.removeFilters": "Versuchen Sie, einige Filter zu entfernen",
|
||||
"discussions.empty.iconAlt": "Leer",
|
||||
"discussions.authors.label.staff": "Betreuung",
|
||||
"navigation.course.tabs.label": "Course Material",
|
||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||
"discussions.comments.comment.addResponse": "Add a response",
|
||||
"discussions.comments.comment.addComment": "Add a comment",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Content reported for staff to review",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n =0 {No responses}\n one {Showing # response}\n other {Showing # responses}\n }",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural,\n =0 {No endorsed responses}\n one {Showing # endorsed response}\n other {Showing # endorsed responses}\n }",
|
||||
"discussions.comments.comment.loadMoreComments": "Load more comments",
|
||||
"discussions.comments.comment.loadMoreResponses": "Load more responses",
|
||||
"discussions.comments.comment.visibility": "This post is visible to {group, select,\n null {Everyone}\n other {{group}}\n }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select,\n discussion {Discussion}\n question {Question}\n other {{postType}}\n } posted {relativeTime} by",
|
||||
"discussions.comments.comment.commentTime": "Posted {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Answer",
|
||||
"discussions.comments.comment.answeredlabel": "Marked as answered by",
|
||||
"discussions.comments.comment.endorsed": "Endorsed",
|
||||
"discussions.comments.comment.endorsedlabel": "Endorsed by",
|
||||
"discussions.actions.label": "Actions menu",
|
||||
"discussions.actions.edit": "Edit",
|
||||
"discussions.actions.pin": "Pin",
|
||||
"discussions.actions.delete": "Delete",
|
||||
"discussions.editor.submit": "Submit",
|
||||
"discussions.editor.submitting": "Submitting",
|
||||
"discussions.editor.cancel": "Cancel",
|
||||
"discussions.editor.error.empty": "Post content cannot be empty.",
|
||||
"discussions.editor.delete.response.title": "Delete response",
|
||||
"discussions.editor.delete.response.description": "Are you sure you want to permanently delete this response?",
|
||||
"discussions.editor.delete.comment.title": "Delete comment",
|
||||
"discussions.editor.delete.comment.description": "Are you sure you want to permanently delete this comment?",
|
||||
"discussions.delete.confirmation.button.delete": "Delete",
|
||||
"discussions.editor.response.response.title": "Report inappropriate content?",
|
||||
"discussions.editor.response.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.editor.report.comment.title": "Report inappropriate content?",
|
||||
"discussions.editor.report.comment.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.editor.comments.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.editReasonCode.error": "Select reason for editing",
|
||||
"discussions.comment.comments.editedBy": "Edited by",
|
||||
"discussions.comment.comments.reason": "Reason",
|
||||
"discussions.post.closedBy": "Post closed by",
|
||||
"discussion.comment.repliesHeading": "{count} replies for the response added",
|
||||
"discussion.comment.time": "{time} ago",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
"discussions.topics.discussions": "{count, plural,\n =0 {Discussion}\n one {# Discussion}\n other {# Discussions}\n }",
|
||||
"discussions.topics.questions": "{count, plural,\n =0 {Question}\n one {# Question}\n other {# Questions}\n }",
|
||||
"discussions.topics.reported": "{reported} reported",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} previously reported",
|
||||
"discussions.topics.find.label": "Search topics",
|
||||
"discussions.topics.unnamed.section.label": "Unnamed Section",
|
||||
"discussions.topics.unnamed.subsection.label": "Unnamed Subsection",
|
||||
"discussions.subtopics.unnamed.topic.label": "Unnamed Topic",
|
||||
"discussions.topics.title": "No topic exists",
|
||||
"discussions.topics.createTopic": "Please contact you admin to create a topic",
|
||||
"discussions.topics.nothing": "Nothing here yet",
|
||||
"discussions.topics.archived.label": "Archived",
|
||||
"discussions.learner.reported": "{reported} reported",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} previously reported",
|
||||
"discussions.learner.lastLogin": "Last active {lastActiveTime}",
|
||||
"discussions.learner.loadMostLearners": "Load more",
|
||||
"discussions.learner.back": "Back",
|
||||
"discussions.learner.activityForLearner": "Activity for {username}",
|
||||
"discussions.learner.mostActivity": "Most activity",
|
||||
"discussions.learner.reportedActivity": "Reported activity",
|
||||
"discussions.learner.recentActivity": "Recent activity",
|
||||
"discussions.learner.sortFilterStatus": "All learners sorted by {sort, select,\n flagged {reported activity}\n activity {most activity}\n other {{sort}}\n }",
|
||||
"discussion.learner.allActivity": "All activity",
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.actions.button.alt": "Actions menu",
|
||||
"discussions.actions.copylink": "Copy link",
|
||||
"discussions.actions.unpin": "Unpin",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Close",
|
||||
"discussions.actions.reopen": "Reopen",
|
||||
"discussions.actions.report": "Report",
|
||||
"discussions.actions.unreport": "Unreport",
|
||||
"discussions.actions.endorse": "Endorse",
|
||||
"discussions.actions.unendorse": "Unendorse",
|
||||
"discussions.actions.markAnswered": "Mark as answered",
|
||||
"discussions.actions.unMarkAnswered": "Unmark as answered",
|
||||
"discussions.modal.confirmation.button.cancel": "Cancel",
|
||||
"discussions.empty.allTopics": "All discussion activity for these topics will show up here.",
|
||||
"discussions.empty.allPosts": "All discussion activity for your course will show up here.",
|
||||
"discussions.empty.myPosts": "Posts you've interacted with will show up here.",
|
||||
"discussions.empty.topic": "All discussion activity for this topic will show up here.",
|
||||
"discussions.empty.title": "Nothing here yet",
|
||||
"discussions.empty.noPostSelected": "No post selected",
|
||||
"discussions.empty.noTopicSelected": "No topic selected",
|
||||
"discussions.sidebar.noResultsFound": "No results found",
|
||||
"discussions.sidebar.differentKeywords": "Try searching different keywords",
|
||||
"discussions.sidebar.removeKeywords": "Try searching different keywords or removing some filters",
|
||||
"discussions.sidebar.removeKeywordsOnly": "Try searching different keywords",
|
||||
"discussions.sidebar.removeFilters": "Try removing some filters",
|
||||
"discussions.empty.iconAlt": "Empty",
|
||||
"discussions.authors.label.staff": "Staff",
|
||||
"discussions.authors.label.ta": "TA",
|
||||
"discussions.learner.loadMostPosts": "Mehr Beiträge laden",
|
||||
"discussions.post.anonymous.author": "Anonym",
|
||||
"discussion.banner.welcomeMessage": "🎉 Willkommen beim neuen und verbesserten Diskussionserlebnis!",
|
||||
"discussion.banner.learnMore": "Lernen Sie mehr",
|
||||
"discussion.banner.shareFeedback": "Feedback teilen",
|
||||
"discussion.blackoutBanner.information": "Das Posten in Diskussionen wird vom Kursteam vorübergehend deaktiviert",
|
||||
"discussions.editor.image.warning.message": "Bilder mit einer Breite oder Höhe von mehr als 999 Pixel sind nicht sichtbar, wenn der Beitrag, die Antwort oder der Kommentar über Inline-Kursdiskussionen angezeigt werden",
|
||||
"discussions.editor.image.warning.title": "Warnung!",
|
||||
"discussions.learner.loadMostPosts": "Load more posts",
|
||||
"discussions.post.anonymous.author": "anonymous",
|
||||
"discussion.banner.welcomeMessage": "🎉 Welcome to the new and improved discussions experience!",
|
||||
"discussion.banner.learnMore": "Learn more",
|
||||
"discussion.banner.shareFeedback": "Share feedback",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is temporarily disabled by the course team",
|
||||
"discussions.editor.image.warning.message": "Images having width or height greater than 999px will not be visible when the post, response or comment is viewed using in-line course discussions",
|
||||
"discussions.editor.image.warning.title": "Warning!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Themen",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Alles anzeigen",
|
||||
"discussions.navigation.navigationBar.allPosts": "Alle Artikel",
|
||||
"discussions.navigation.navigationBar.allTopics": "Themen",
|
||||
"discussions.navigation.navigationBar.myPosts": "Meine Posts",
|
||||
"discussions.navigation.navigationBar.learners": "Lernende",
|
||||
"discussions.app.title": "Diskussionen",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Einträge durchsuchen",
|
||||
"discussions.posts.actionBar.search": "{page, select, topics {Suchthemen} posts {Alle Beiträge durchsuchen} learners {Lernende suchen} myPosts {Alle Beiträge durchsuchen} a00a14c5d87{d9fz}09 {d9fz}09 {d9fz}09 {d9fz}",
|
||||
"discussions.actionBar.searchInfo": "{count} Ergebnisse für "{text}" werden angezeigt",
|
||||
"discussions.actionBar.searchRewriteInfo": "Keine Ergebnisse gefunden für "{searchString}". {count} Ergebnisse für "{textSearchRewrite}" werden angezeigt.",
|
||||
"discussions.actionBar.searchInfoSearching": "Suche...",
|
||||
"discussions.actionBar.clearSearch": "Klare Ergebnisse",
|
||||
"discussion.posts.actionBar.add": "Fügen Sie einen Beitrag hinzu",
|
||||
"discussion.posts.actionBar.close": "Schließen",
|
||||
"discussions.post.editor.type": "Beitragsart",
|
||||
"discussions.post.editor.addPostHeading": "Fügen Sie einen Beitrag hinzu",
|
||||
"discussions.post.editor.editPostHeading": "Beitrag bearbeiten",
|
||||
"discussions.post.editor.typeDescription": "Wenn Sie eine konkrete Antwort für ein Problem suchen, stellen Sie eine Frage. Um sich mit anderen Nutzern über ein Thema auszutauschen und Ideen zu teilen, nutzen Sie die Diskussion. ",
|
||||
"discussions.post.editor.required": "Erforderlich",
|
||||
"discussions.post.editor.questionType": "Frage",
|
||||
"discussions.post.editor.questionDescription": "Sprechen Sie Probleme an, die Antworten erfordern",
|
||||
"discussions.post.editor.discussionType": "Diskussion",
|
||||
"discussions.post.editor.discussionDescription": "Teilen Sie Ideen und beginnen Sie Gespräche",
|
||||
"discussions.post.editor.topicArea": "Themenbereich",
|
||||
"discussions.post.editor.topicAreaDescription": "Fügen Sie Ihren Beitrag zu einem entsprechenden Thema hinzu, um andern das Auffinden zu erleichtern.",
|
||||
"discussions.post.editor.cohortVisibility": "Kohortensichtbarkeit",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "Alle Teilnehmer",
|
||||
"discussions.post.editor.title": "Titel des Beitrags",
|
||||
"discussions.post.editor.titleDescription": "Um zur Teilnahme zu motivieren, fügen Sie bitte einen klaren und beschreibenden Titel hinzu.",
|
||||
"discussions.post.editor.title.error": "Beitragstitel darf nicht leer sein.",
|
||||
"discussions.post.editor.content.error": "Der Beitragsinhalt darf nicht leer sein.",
|
||||
"discussions.post.editor.questionText": "Ihre Frage oder Idee (*)",
|
||||
"discussions.post.editor.preview": "Vorschau",
|
||||
"discussions.post.editor.followPost": "Diesem Eintrag folgen",
|
||||
"discussions.post.editor.anonymousPost": "Anonym posten",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Posten Sie anonym an Kollegen",
|
||||
"discussions.editor.posts.editReasonCode": "Bearbeitungsgrund",
|
||||
"discussions.editor.posts.showPreview.button": "Vorschau zeigen",
|
||||
"discussions.topic.noName.label": "Unbenannte Kategorie",
|
||||
"discussions.subtopic.noName.label": "Unbenannte Unterkategorie",
|
||||
"discussions.posts.filter.showALl": "Alles anzeigen",
|
||||
"discussions.posts.filter.discussions": "Diskussionen",
|
||||
"discussions.posts.filter.questions": "Fragen",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Topics",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Show all",
|
||||
"discussions.navigation.navigationBar.allPosts": "All posts",
|
||||
"discussions.navigation.navigationBar.allTopics": "Topics",
|
||||
"discussions.navigation.navigationBar.myPosts": "My posts",
|
||||
"discussions.navigation.navigationBar.learners": "Learners",
|
||||
"discussions.app.title": "Discussions",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Search all posts",
|
||||
"discussions.posts.actionBar.search": "{page, select,\n topics {Search topics}\n posts {Search all posts}\n learners {Search learners}\n myPosts {Search all posts}\n other {{page}}\n }",
|
||||
"discussions.actionBar.searchInfo": "Showing {count} results for \"{text}\"",
|
||||
"discussions.actionBar.searchRewriteInfo": "No results found for \"{searchString}\". Showing {count} results for \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchInfoSearching": "Searching...",
|
||||
"discussions.actionBar.clearSearch": "Clear results",
|
||||
"discussion.posts.actionBar.add": "Add a post",
|
||||
"discussion.posts.actionBar.close": "Close",
|
||||
"discussions.post.editor.type": "Post type",
|
||||
"discussions.post.editor.addPostHeading": "Add a post",
|
||||
"discussions.post.editor.editPostHeading": "Edit post",
|
||||
"discussions.post.editor.typeDescription": "Questions raise issues that need answers. Discussions share ideas and start conversations.",
|
||||
"discussions.post.editor.required": "Required",
|
||||
"discussions.post.editor.questionType": "Question",
|
||||
"discussions.post.editor.questionDescription": "Raise issues that need answers",
|
||||
"discussions.post.editor.discussionType": "Discussion",
|
||||
"discussions.post.editor.discussionDescription": "Share ideas and start conversations",
|
||||
"discussions.post.editor.topicArea": "Topic area",
|
||||
"discussions.post.editor.topicAreaDescription": "Add your post to a relevant topic to help others find it.",
|
||||
"discussions.post.editor.cohortVisibility": "Cohort visibility",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "All learners",
|
||||
"discussions.post.editor.title": "Post title",
|
||||
"discussions.post.editor.titleDescription": "Add a clear and descriptive title to encourage participation.",
|
||||
"discussions.post.editor.title.error": "Post title cannot be empty.",
|
||||
"discussions.post.editor.content.error": "Post content cannot be empty.",
|
||||
"discussions.post.editor.questionText": "Your question or idea (required)",
|
||||
"discussions.post.editor.preview": "Preview",
|
||||
"discussions.post.editor.followPost": "Follow this post",
|
||||
"discussions.post.editor.anonymousPost": "Post anonymously",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Post anonymously to peers",
|
||||
"discussions.editor.posts.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.showPreview.button": "Show Preview",
|
||||
"discussions.topic.noName.label": "Unnamed category",
|
||||
"discussions.subtopic.noName.label": "Unnamed subcategory",
|
||||
"discussions.posts.filter.showALl": "Show all",
|
||||
"discussions.posts.filter.discussions": "Discussions",
|
||||
"discussions.posts.filter.questions": "Questions",
|
||||
"discussions.posts.filter.message": "Status: {filterBy}",
|
||||
"discussions.posts.status.filter.anyStatus": "Jeder Status",
|
||||
"discussions.posts.status.filter.unread": "Ungelesen",
|
||||
"discussions.posts.status.filter.following": "Folge",
|
||||
"discussions.posts.status.filter.reported": "Gemeldet",
|
||||
"discussions.posts.status.filter.unanswered": "Unbeantwortet",
|
||||
"discussions.posts.status.filter.unresponded": "Nicht geantwortet",
|
||||
"discussions.posts.filter.myPosts": "Meine Posts",
|
||||
"discussions.posts.filter.myDiscussions": "Meine Diskussionen",
|
||||
"discussions.posts.filter.myQuestions": "Meine Fragen",
|
||||
"discussions.posts.sort.message": "Sortiert nach {sortBy}",
|
||||
"discussions.posts.sort.lastActivity": "Letzte Aktivität",
|
||||
"discussions.posts.sort.commentCount": "Die meisten Aktivitäten",
|
||||
"discussions.posts.sort.voteCount": "Die meisten Likes",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select, false {All} true {Own} other {{own}} } {status, select, statusAll {} statusUnread {unread} statusFollowing {followed} statusReported {reported} statusUnanswered {unanswered} statusUnresponded {unresponded} other { {status}} } {type, select, discussion {discussions} question {questions} all {posts} other {{type}} } {cohortType, select, all {} group {in {cohort}} other {{cohortType}} } sortiert nach {sort, select, lastActivityAt {neueste Aktivität} commentCount {meiste Aktivität} voteCount {meiste Likes} other {{a0fc8413bba} 10cz",
|
||||
"discussions.post.author.anonymous": "Anonym",
|
||||
"discussions.post.addResponse": "Antwort hinzufügen",
|
||||
"discussions.post.lastResponse": "Letzte Antwort {time}",
|
||||
"discussions.post.postedOn": "Gepostet {time} von {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Gemeldet",
|
||||
"discussions.post.following": "Folge",
|
||||
"discussions.post.follow": "Folgen",
|
||||
"discussions.post.followed": "Gefolgt",
|
||||
"discussions.post.notFollowed": "Nicht gefolgt",
|
||||
"discussions.post.answered": "Beantwortet",
|
||||
"discussions.post.unFollow": "Verlassen",
|
||||
"discussions.post.like": "Wie",
|
||||
"discussions.post.removeLike": "nicht wie",
|
||||
"discussions.post.liked": "gefallen",
|
||||
"discussions.post.likes": "Likes",
|
||||
"discussions.post.viewActivity": "Aktivität anzeigen",
|
||||
"discussions.post.activity": "Aktivität",
|
||||
"discussions.post.closed": "Beitrag für Antworten und Kommentare geschlossen",
|
||||
"discussions.post.relatedTo": "Im Zusammenhang mit",
|
||||
"discussions.editor.delete.post.title": "Beitrag entfernen",
|
||||
"discussions.editor.delete.post.description": "Möchten Sie diesen Beitrag wirklich dauerhaft löschen?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Löschen",
|
||||
"discussions.editor.report.post.title": "Unangemessene Inhalte melden?",
|
||||
"discussions.editor.report.post.description": "Das Diskussionsmoderationsteam überprüft diesen Inhalt und ergreift entsprechende Maßnahmen.",
|
||||
"discussions.post.closePostModal.title": "Beitrag schließen",
|
||||
"discussions.post.closePostModal.text": "Geben Sie einen Grund für das Schließen dieses Beitrags ein. Dies wird nur anderen Moderatoren angezeigt.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "Grund",
|
||||
"discussions.post.closePostModal.cancel": "Löschen",
|
||||
"discussions.post.closePostModal.confirm": "Beitrag schließen",
|
||||
"discussions.post.label.new": "{count} Neu",
|
||||
"discussions.post.editedBy": "Bearbeitet von",
|
||||
"discussions.post.editReason": "Grund",
|
||||
"discussions.post.postWithoutPreview": "Keine Vorschau vorhanden",
|
||||
"discussions.post.follow.description": "Sie folgen diesem Beitrag",
|
||||
"discussions.post.unfollow.description": "Sie folgen diesem Beitrag nicht",
|
||||
"discussions.topics.sort.message": "Sortiert nach {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "Letzte Aktivität",
|
||||
"discussions.topics.sort.commentCount": "Die meisten Aktivitäten",
|
||||
"discussions.topics.sort.courseStructure": "Kursstruktur",
|
||||
"discussions.topics.unnamed.label": "Unbenannte Kategorie",
|
||||
"discussions.subtopics.unnamed.label": "Unbenannte Unterkategorie",
|
||||
"tour.action.advance": "Weiter",
|
||||
"tour.action.dismiss": "Abgewiesen",
|
||||
"tour.action.end": "okay",
|
||||
"tour.body.notRespondedFilter": "Jetzt können Sie Diskussionen filtern, um Beiträge ohne Antwort zu finden.",
|
||||
"tour.title.notRespondedFilter": "Neue Filteroption!"
|
||||
"discussions.posts.status.filter.anyStatus": "Any status",
|
||||
"discussions.posts.status.filter.unread": "Unread",
|
||||
"discussions.posts.status.filter.following": "Following",
|
||||
"discussions.posts.status.filter.reported": "Reported",
|
||||
"discussions.posts.status.filter.unanswered": "Unanswered",
|
||||
"discussions.posts.status.filter.unresponded": "Not responded",
|
||||
"discussions.posts.filter.myPosts": "My posts",
|
||||
"discussions.posts.filter.myDiscussions": "My discussions",
|
||||
"discussions.posts.filter.myQuestions": "My questions",
|
||||
"discussions.posts.sort.message": "Sorted by {sortBy}",
|
||||
"discussions.posts.sort.lastActivity": "Recent activity",
|
||||
"discussions.posts.sort.commentCount": "Most activity",
|
||||
"discussions.posts.sort.voteCount": "Most likes",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonymous",
|
||||
"discussions.post.lastResponse": "Last response {time}",
|
||||
"discussions.post.postedOn": "Posted {time} by {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Reported",
|
||||
"discussions.post.following": "Following",
|
||||
"discussions.post.follow": "Follow",
|
||||
"discussions.post.followed": "Followed",
|
||||
"discussions.post.notFollowed": "Not Followed",
|
||||
"discussions.post.answered": "Answered",
|
||||
"discussions.post.unFollow": "Unfollow",
|
||||
"discussions.post.like": "Like",
|
||||
"discussions.post.removeLike": "Unlike",
|
||||
"discussions.post.liked": "liked",
|
||||
"discussions.post.likes": "likes",
|
||||
"discussions.post.viewActivity": "View activity",
|
||||
"discussions.post.activity": "Activity",
|
||||
"discussions.post.closed": "Post closed for responses and comments",
|
||||
"discussions.post.relatedTo": "Related to",
|
||||
"discussions.editor.delete.post.title": "Delete post",
|
||||
"discussions.editor.delete.post.description": "Are you sure you want to permanently delete this post?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Delete",
|
||||
"discussions.editor.report.post.title": "Report inappropriate content?",
|
||||
"discussions.editor.report.post.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.post.closePostModal.title": "Close post",
|
||||
"discussions.post.closePostModal.text": "Enter a reason for closing this post. This will only be displayed to other moderators.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "Reason",
|
||||
"discussions.post.closePostModal.cancel": "Cancel",
|
||||
"discussions.post.closePostModal.confirm": "Close post",
|
||||
"discussions.post.label.new": "{count} New",
|
||||
"discussions.post.editedBy": "Edited by",
|
||||
"discussions.post.editReason": "Reason",
|
||||
"discussions.post.postWithoutPreview": "No preview available",
|
||||
"discussions.post.follow.description": "you are following this post",
|
||||
"discussions.post.unfollow.description": "you are not following this post",
|
||||
"discussions.topics.sort.message": "Sorted by {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "Recent activity",
|
||||
"discussions.topics.sort.commentCount": "Most activity",
|
||||
"discussions.topics.sort.courseStructure": "Course Structure",
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Material del Curso",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Más...",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Agregar una respuesta",
|
||||
"discussions.comments.comment.addComment": "Agregar un comentario",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Contenido informado para que el personal lo revise",
|
||||
"discussions.actions.back.alt": "Volver a la lista",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.comments.comment.responseCount": "{num, plural, =0 {No responses} one {Showing # response} other {Showing # responses} }",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural, =0 {Sin respuestas respaldadas} one {Mostrando # respuesta respaldada} other {Mostrando # respuestas respaldadas} }",
|
||||
"discussions.comments.comment.loadMoreComments": "Cargar más comentarios",
|
||||
@@ -29,30 +29,30 @@
|
||||
"discussions.editor.delete.comment.title": "Eliminar comentario",
|
||||
"discussions.editor.delete.comment.description": "¿Estás seguro de que quieres eliminar este comentario de forma permanente?",
|
||||
"discussions.delete.confirmation.button.delete": "Borrar",
|
||||
"discussions.editor.response.response.title": "¿Denunciar contenido inapropiado?",
|
||||
"discussions.editor.response.description": "El equipo de moderación de debates revisará este contenido y tomará las medidas adecuadas.",
|
||||
"discussions.editor.report.comment.title": "¿Denunciar contenido inapropiado?",
|
||||
"discussions.editor.report.comment.description": "El equipo de moderación de debates revisará este contenido y tomará las medidas adecuadas.",
|
||||
"discussions.editor.response.response.title": "Report inappropriate content?",
|
||||
"discussions.editor.response.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.editor.report.comment.title": "Report inappropriate content?",
|
||||
"discussions.editor.report.comment.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.editor.comments.editReasonCode": "Razón de la edición",
|
||||
"discussions.editor.posts.editReasonCode.error": "Seleccione el motivo de la edición",
|
||||
"discussions.comment.comments.editedBy": "Editado por",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Motivo",
|
||||
"discussions.post.closedBy": "Publicación cerrada por",
|
||||
"discussion.comment.repliesHeading": "{count} respuestas para la respuesta añadida",
|
||||
"discussion.comment.time": "hace {time}",
|
||||
"discussion.thread.notFound": "Hilo no encontrado",
|
||||
"discussions.topics.backAlt": "Volver a la lista de temas",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
"discussions.topics.discussions": "{count, plural,\n =0 {Discussion}\n one {# Discussion}\n other {# Discussions}\n }",
|
||||
"discussions.topics.questions": "{count, plural,\n =0 {Question}\n one {# Question}\n other {# Questions}\n }",
|
||||
"discussions.topics.reported": "{reported} informado",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} informado anteriormente",
|
||||
"discussions.topics.find.label": "Buscar temas",
|
||||
"discussions.topics.unnamed.section.label": "Sección sin nombre",
|
||||
"discussions.topics.unnamed.subsection.label": "Subsección sin nombre",
|
||||
"discussions.subtopics.unnamed.topic.label": "Tema sin nombre",
|
||||
"discussions.topics.title": "No existe ningún tema",
|
||||
"discussions.topics.createTopic": "Póngase en contacto con su administrador para crear un tema",
|
||||
"discussions.topics.nothing": "Nada aquí todavía",
|
||||
"discussions.topics.unnamed.section.label": "Unnamed Section",
|
||||
"discussions.topics.unnamed.subsection.label": "Unnamed Subsection",
|
||||
"discussions.subtopics.unnamed.topic.label": "Unnamed Topic",
|
||||
"discussions.topics.title": "No topic exists",
|
||||
"discussions.topics.createTopic": "Please contact you admin to create a topic",
|
||||
"discussions.topics.nothing": "Nothing here yet",
|
||||
"discussions.topics.archived.label": "Archivado",
|
||||
"discussions.learner.reported": "{reported} informado",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} informado anteriormente",
|
||||
@@ -62,23 +62,23 @@
|
||||
"discussions.learner.activityForLearner": "Actividad para {username}",
|
||||
"discussions.learner.mostActivity": "La mayoría de la actividad",
|
||||
"discussions.learner.reportedActivity": "Actividad reportada",
|
||||
"discussions.learner.recentActivity": "Actividad reciente",
|
||||
"discussions.learner.sortFilterStatus": "Todos los alumnos ordenados por {sort, select, flagged {actividad notificada} activity {mayor actividad} other {{sort}} }",
|
||||
"discussions.learner.recentActivity": "Recent activity",
|
||||
"discussions.learner.sortFilterStatus": "All learners sorted by {sort, select,\n flagged {reported activity}\n activity {most activity}\n other {{sort}}\n }",
|
||||
"discussion.learner.allActivity": "Toda la actividad",
|
||||
"discussion.learner.posts": "Publicaciones",
|
||||
"discussions.actions.button.alt": "Menú de acciones",
|
||||
"discussions.actions.copylink": "Copiar link",
|
||||
"discussions.actions.unpin": "Desmarcar",
|
||||
"discussions.confirmation.button.confirm": "Confirmar",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Cerrar",
|
||||
"discussions.actions.reopen": "Reabrir",
|
||||
"discussions.actions.report": "Informar",
|
||||
"discussions.actions.unreport": "Dejar de denunciar",
|
||||
"discussions.actions.endorse": "Validar",
|
||||
"discussions.actions.unendorse": "Invalidar",
|
||||
"discussions.actions.markAnswered": "Marcar como respondida",
|
||||
"discussions.actions.markAnswered": "Mark as answered",
|
||||
"discussions.actions.unMarkAnswered": "Desmarcar como respondida",
|
||||
"discussions.modal.confirmation.button.cancel": "Cancelar",
|
||||
"discussions.modal.confirmation.button.cancel": "Cancel",
|
||||
"discussions.empty.allTopics": "Toda la actividad de debate de estos temas se mostrará aquí.",
|
||||
"discussions.empty.allPosts": "Toda la actividad de debate de su curso se mostrará aquí.",
|
||||
"discussions.empty.myPosts": "Las publicaciones con las que has interactuado se mostrarán aquí.",
|
||||
@@ -99,10 +99,10 @@
|
||||
"discussion.banner.welcomeMessage": "🎉 ¡Bienvenido a la nueva y mejorada experiencia de debates!",
|
||||
"discussion.banner.learnMore": "Aprender más",
|
||||
"discussion.banner.shareFeedback": "Compartir comentarios",
|
||||
"discussion.blackoutBanner.information": "El equipo del curso deshabilita temporalmente la publicación en discusiones",
|
||||
"discussions.editor.image.warning.message": "Las imágenes que tengan un ancho o alto superior a 999 px no serán visibles cuando la publicación, la respuesta o el comentario se vean mediante debates en línea del curso.",
|
||||
"discussions.editor.image.warning.title": "¡Advertencia!",
|
||||
"discussions.editor.image.warning.dismiss": "Aceptar",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is temporarily disabled by the course team",
|
||||
"discussions.editor.image.warning.message": "Images having width or height greater than 999px will not be visible when the post, response or comment is viewed using in-line course discussions",
|
||||
"discussions.editor.image.warning.title": "Warning!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Temas",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Mostrar todo",
|
||||
"discussions.navigation.navigationBar.allPosts": "Todos los mensajes",
|
||||
@@ -141,9 +141,9 @@
|
||||
"discussions.post.editor.anonymousPost": "Publicar de forma anónima",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Publicar de forma anónima para tus compañeros",
|
||||
"discussions.editor.posts.editReasonCode": "Motivo de la edición",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.topic.noName.label": "Categoría sin nombre",
|
||||
"discussions.subtopic.noName.label": "Subcategoría sin nombre",
|
||||
"discussions.editor.posts.showPreview.button": "Mostrar vista previa",
|
||||
"discussions.topic.noName.label": "Unnamed category",
|
||||
"discussions.subtopic.noName.label": "Unnamed subcategory",
|
||||
"discussions.posts.filter.showALl": "Mostrar todo",
|
||||
"discussions.posts.filter.discussions": "Debates\n",
|
||||
"discussions.posts.filter.questions": "Preguntas",
|
||||
@@ -153,7 +153,7 @@
|
||||
"discussions.posts.status.filter.following": "Siguiendo",
|
||||
"discussions.posts.status.filter.reported": "Informado",
|
||||
"discussions.posts.status.filter.unanswered": "Sin responder",
|
||||
"discussions.posts.status.filter.unresponded": "Sin respuesta",
|
||||
"discussions.posts.status.filter.unresponded": "Not responded",
|
||||
"discussions.posts.filter.myPosts": "Mis publicaciones",
|
||||
"discussions.posts.filter.myDiscussions": "Mis debates",
|
||||
"discussions.posts.filter.myQuestions": "Mis preguntas",
|
||||
@@ -161,31 +161,30 @@
|
||||
"discussions.posts.sort.lastActivity": "Actividad reciente",
|
||||
"discussions.posts.sort.commentCount": "La mayoría de la actividad",
|
||||
"discussions.posts.sort.voteCount": "La mayoría me gusta",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select, false {All} true {Own} other {{own}} } {status, select, statusAll {} statusUnread {unread} statusFollowing {followed} statusReported {reported} statusUnanswered {unanswered} statusUnresponded {unresponded} other { {status}} } {type, select, discussion {discussions} question {questions} all {posts} other {{type}} } {cohortType, select, all {} group {in {cohort}} other {{cohortType}} } ordenado por {sort, select, lastActivityAt {actividad reciente} commentCount {mayor actividad} voteCount {mayor cantidad de Me gusta} other {{a0fc841}bba10}",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anónimo",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "Última respuesta {time}",
|
||||
"discussions.post.postedOn": "Publicado {time} por {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Informado",
|
||||
"discussions.post.following": "Siguiendo",
|
||||
"discussions.post.follow": "Seguir",
|
||||
"discussions.post.followed": "Seguido",
|
||||
"discussions.post.notFollowed": "No seguido",
|
||||
"discussions.post.followed": "Followed",
|
||||
"discussions.post.notFollowed": "Not Followed",
|
||||
"discussions.post.answered": "Respondido",
|
||||
"discussions.post.unFollow": "Dejar de seguir",
|
||||
"discussions.post.like": "Me gusta",
|
||||
"discussions.post.removeLike": "Dejar de gustar",
|
||||
"discussions.post.liked": "Me gusta",
|
||||
"discussions.post.likes": "Me gustan",
|
||||
"discussions.post.liked": "liked",
|
||||
"discussions.post.likes": "likes",
|
||||
"discussions.post.viewActivity": "Ver actividad",
|
||||
"discussions.post.activity": "Actividad",
|
||||
"discussions.post.activity": "Activity",
|
||||
"discussions.post.closed": "Publicación cerrada por respuestas y comentarios.",
|
||||
"discussions.post.relatedTo": "Relacionado con",
|
||||
"discussions.post.relatedTo": "Related to",
|
||||
"discussions.editor.delete.post.title": "Eliminar mensaje",
|
||||
"discussions.editor.delete.post.description": "¿Seguro que quieres eliminar esta publicación de forma permanente?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Borrar",
|
||||
"discussions.editor.report.post.title": "¿Denunciar contenido inapropiado?",
|
||||
"discussions.editor.report.post.description": "El equipo de moderación de debates revisará este contenido y tomará las medidas adecuadas.",
|
||||
"discussions.post.delete.confirmation.button.delete": "Delete",
|
||||
"discussions.editor.report.post.title": "Report inappropriate content?",
|
||||
"discussions.editor.report.post.description": "The discussion moderation team will review this content and take appropriate action.",
|
||||
"discussions.post.closePostModal.title": "Cerrar publicación",
|
||||
"discussions.post.closePostModal.text": "Escribe un motivo para cerrar esta publicación. Esto solo se mostrará a otros moderadores.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "Motivo",
|
||||
@@ -195,17 +194,17 @@
|
||||
"discussions.post.editedBy": "Editado por",
|
||||
"discussions.post.editReason": "Motivo",
|
||||
"discussions.post.postWithoutPreview": "No hay vista previa disponible",
|
||||
"discussions.post.follow.description": "estás siguiendo esta publicación",
|
||||
"discussions.post.unfollow.description": "No estás siguiendo esta publicación",
|
||||
"discussions.post.follow.description": "you are following this post",
|
||||
"discussions.post.unfollow.description": "you are not following this post",
|
||||
"discussions.topics.sort.message": "Ordenado por {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "Actividad reciente",
|
||||
"discussions.topics.sort.commentCount": "La mayoría de la actividad",
|
||||
"discussions.topics.sort.courseStructure": "Estructura del curso",
|
||||
"discussions.topics.unnamed.label": "Categoría sin nombre",
|
||||
"discussions.subtopics.unnamed.label": "Subcategoría sin nombre",
|
||||
"tour.action.advance": "Siguiente",
|
||||
"tour.action.dismiss": "Descartar",
|
||||
"tour.action.end": "Okey",
|
||||
"tour.body.notRespondedFilter": "Ahora puede filtrar debates para encontrar publicaciones sin respuesta.",
|
||||
"tour.title.notRespondedFilter": "¡Nueva opción de filtrado!"
|
||||
"discussions.topics.unnamed.label": "Unnamed category",
|
||||
"discussions.subtopics.unnamed.label": "Unnamed subcategory",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Course Material",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Plus...",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Ajouter une réponse",
|
||||
"discussions.comments.comment.addComment": "Ajouter un commentaire",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Contenu signalé au personnel pour examen",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n =0 {No responses}\n one {Showing # response}\n other {Showing # responses}\n }",
|
||||
@@ -36,9 +36,9 @@
|
||||
"discussions.editor.comments.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.editReasonCode.error": "Select reason for editing",
|
||||
"discussions.comment.comments.editedBy": "Édité par",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Motif",
|
||||
"discussions.post.closedBy": "Message fermé par",
|
||||
"discussion.comment.repliesHeading": "{count} réponses pour la réponse ajoutée",
|
||||
"discussion.comment.time": "il y a {time}",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
@@ -141,7 +141,7 @@
|
||||
"discussions.post.editor.anonymousPost": "Post anonymously",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Post anonymously to peers",
|
||||
"discussions.editor.posts.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.editor.posts.showPreview.button": "Show Preview",
|
||||
"discussions.topic.noName.label": "Unnamed category",
|
||||
"discussions.subtopic.noName.label": "Unnamed subcategory",
|
||||
"discussions.posts.filter.showALl": "Show all",
|
||||
@@ -163,7 +163,6 @@
|
||||
"discussions.posts.sort.voteCount": "Most likes",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonymous",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "Last response {time}",
|
||||
"discussions.post.postedOn": "Posted {time} by {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Reported",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Matériel de cours",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Plus...",
|
||||
"discussions.comments.comment.addComment": "Ajouter un commentaire",
|
||||
"discussions.comments.comment.addResponse": "Ajouter une réponse",
|
||||
"discussions.comments.comment.addComment": "Ajouter un commentaire",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Contenu signalé au personnel pour examen",
|
||||
"discussions.actions.back.alt": "Retour à la liste",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n =0 {Aucune réponse}\n one {Affiche # réponse}\n other {Affiche # réponses}\n }",
|
||||
@@ -36,9 +36,9 @@
|
||||
"discussions.editor.comments.editReasonCode": "Raison de la modification",
|
||||
"discussions.editor.posts.editReasonCode.error": "Sélectionnez la raison de la modification",
|
||||
"discussions.comment.comments.editedBy": "Édité par",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Raison",
|
||||
"discussions.post.closedBy": "Message fermé par",
|
||||
"discussion.comment.repliesHeading": "{count} réponses pour la réponse ajoutée",
|
||||
"discussion.comment.time": "il y a {time}",
|
||||
"discussion.thread.notFound": "Sujet introuvable",
|
||||
"discussions.topics.backAlt": "Retour à la liste des sujets",
|
||||
@@ -163,7 +163,6 @@
|
||||
"discussions.posts.sort.voteCount": "La plupart des aimés",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonyme",
|
||||
"discussions.post.addResponse": "Ajouter une réponse",
|
||||
"discussions.post.lastResponse": "Dernière réponse {time}",
|
||||
"discussions.post.postedOn": "Publié {time} par {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Signalé",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Course Material",
|
||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Add a response",
|
||||
"discussions.comments.comment.addComment": "Add a comment",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Content reported for staff to review",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n =0 {No responses}\n one {Showing # response}\n other {Showing # responses}\n }",
|
||||
@@ -36,9 +36,9 @@
|
||||
"discussions.editor.comments.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.editReasonCode.error": "Select reason for editing",
|
||||
"discussions.comment.comments.editedBy": "Edited by",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Reason",
|
||||
"discussions.post.closedBy": "Post closed by",
|
||||
"discussion.comment.repliesHeading": "{count} replies for the response added",
|
||||
"discussion.comment.time": "{time} ago",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
@@ -141,7 +141,7 @@
|
||||
"discussions.post.editor.anonymousPost": "Post anonymously",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Post anonymously to peers",
|
||||
"discussions.editor.posts.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.editor.posts.showPreview.button": "Show Preview",
|
||||
"discussions.topic.noName.label": "Unnamed category",
|
||||
"discussions.subtopic.noName.label": "Unnamed subcategory",
|
||||
"discussions.posts.filter.showALl": "Show all",
|
||||
@@ -163,7 +163,6 @@
|
||||
"discussions.posts.sort.voteCount": "Most likes",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonymous",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "Last response {time}",
|
||||
"discussions.post.postedOn": "Posted {time} by {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Reported",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Materiale del corso",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Altro... ",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Aggiungi una risposta",
|
||||
"discussions.comments.comment.addComment": "Aggiungi un commento",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Contenuto segnalato per la revisione da parte del personale",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.comments.comment.responseCount": "{num, plural, =0 {Nessuna risposta} one {Mostra # risposte} other {Mostra # risposte} }",
|
||||
@@ -36,9 +36,9 @@
|
||||
"discussions.editor.comments.editReasonCode": "Motivo della modifica",
|
||||
"discussions.editor.posts.editReasonCode.error": "Seleziona il motivo per la modifica",
|
||||
"discussions.comment.comments.editedBy": "A cura di",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Motivo ",
|
||||
"discussions.post.closedBy": "Post chiuso da",
|
||||
"discussion.comment.repliesHeading": "{count} risposte per la risposta aggiunta",
|
||||
"discussion.comment.time": "{time} fa",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
@@ -141,7 +141,7 @@
|
||||
"discussions.post.editor.anonymousPost": "Pubblica in modo anonimo",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Pubblica in modo anonimo ai colleghi",
|
||||
"discussions.editor.posts.editReasonCode": "Motivo della modifica",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.editor.posts.showPreview.button": "Anteprima dello spettacolo",
|
||||
"discussions.topic.noName.label": "Unnamed category",
|
||||
"discussions.subtopic.noName.label": "Unnamed subcategory",
|
||||
"discussions.posts.filter.showALl": "Mostra tutto",
|
||||
@@ -163,7 +163,6 @@
|
||||
"discussions.posts.sort.voteCount": "La maggior parte dei Mi piace",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonimo",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "Ultima risposta {time}",
|
||||
"discussions.post.postedOn": "Inserito {time} da {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Segnalato ",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Course Material",
|
||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Dodaj odpowiedź",
|
||||
"discussions.comments.comment.addComment": "Add a comment",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Content reported for staff to review",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n=0 {No responses}\none {Showing # response}\nother {Showing # responses}\n}",
|
||||
@@ -36,9 +36,9 @@
|
||||
"discussions.editor.comments.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.editReasonCode.error": "Wybierz powód edycji",
|
||||
"discussions.comment.comments.editedBy": "Edytowany przez",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Reason",
|
||||
"discussions.post.closedBy": "Post zamknięty przez",
|
||||
"discussion.comment.repliesHeading": "{count} replies for the response added",
|
||||
"discussion.comment.time": "{time} ago",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
@@ -141,7 +141,7 @@
|
||||
"discussions.post.editor.anonymousPost": "Post anonymously",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Post anonymously to peers",
|
||||
"discussions.editor.posts.editReasonCode": "Powód edycji",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.editor.posts.showPreview.button": "Show Preview",
|
||||
"discussions.topic.noName.label": "Unnamed category",
|
||||
"discussions.subtopic.noName.label": "Unnamed subcategory",
|
||||
"discussions.posts.filter.showALl": "Show all",
|
||||
@@ -163,7 +163,6 @@
|
||||
"discussions.posts.sort.voteCount": "Most likes",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonymous",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "Ostatnia odpowiedź {time}",
|
||||
"discussions.post.postedOn": "Posted {time} by {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Reported",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Ders Materyali",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Daha Fazlası...",
|
||||
"discussions.comments.comment.addComment": "Yorum ekle",
|
||||
"discussions.comments.comment.addResponse": "Bir cevap ekle",
|
||||
"discussions.comments.comment.addComment": "Bir yorum ekle",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Personelin incelemesi için bildirilen içerik",
|
||||
"discussions.actions.back.alt": "Listeye dön",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n =0 {No responses}\n one {Showing # response}\n other {Showing # responses}\n }",
|
||||
@@ -36,9 +36,9 @@
|
||||
"discussions.editor.comments.editReasonCode": "Düzenleme nedeni",
|
||||
"discussions.editor.posts.editReasonCode.error": "Düzenleme nedenini seçin",
|
||||
"discussions.comment.comments.editedBy": "Düzenleyen",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Gerekçe",
|
||||
"discussions.post.closedBy": "Gönderiyi kapatan ",
|
||||
"discussion.comment.repliesHeading": "Eklenen yanıt için {count} yanıt",
|
||||
"discussion.comment.time": "{time} önce",
|
||||
"discussion.thread.notFound": "Tartışma zinciri bulunamadı",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
@@ -52,7 +52,7 @@
|
||||
"discussions.subtopics.unnamed.topic.label": "Unnamed Topic",
|
||||
"discussions.topics.title": "No topic exists",
|
||||
"discussions.topics.createTopic": "Please contact you admin to create a topic",
|
||||
"discussions.topics.nothing": "Burada henüz bir şey yok",
|
||||
"discussions.topics.nothing": "Nothing here yet",
|
||||
"discussions.topics.archived.label": "Arşivlenmiş",
|
||||
"discussions.learner.reported": "{reported} rapor edildi",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} daha önce rapor edildi",
|
||||
@@ -141,7 +141,7 @@
|
||||
"discussions.post.editor.anonymousPost": "Anonim olarak gönder",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Akranlarına anonim olarak gönder",
|
||||
"discussions.editor.posts.editReasonCode": "Düzenleme nedeni",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.editor.posts.showPreview.button": "Önizlemeyi Göster",
|
||||
"discussions.topic.noName.label": "İsimsiz kategori",
|
||||
"discussions.subtopic.noName.label": "İsimsiz alt kategori",
|
||||
"discussions.posts.filter.showALl": "Tümünü göster",
|
||||
@@ -163,7 +163,6 @@
|
||||
"discussions.posts.sort.voteCount": "En çok beğenilenler",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonim",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "Son yanıt {time}",
|
||||
"discussions.post.postedOn": "{author} {authorLabel} tarafından {time} önce gönderildi",
|
||||
"discussions.post.contentReported": "Rapor edildi",
|
||||
@@ -178,7 +177,7 @@
|
||||
"discussions.post.liked": "liked",
|
||||
"discussions.post.likes": "likes",
|
||||
"discussions.post.viewActivity": "Etkinliği görüntüle",
|
||||
"discussions.post.activity": "Etkinlik",
|
||||
"discussions.post.activity": "Activity",
|
||||
"discussions.post.closed": "Yanıtlar ve yorumlar için gönderi kapatıldı",
|
||||
"discussions.post.relatedTo": "Bunun ile ilgili",
|
||||
"discussions.editor.delete.post.title": "Gönderiyi sil",
|
||||
@@ -203,9 +202,9 @@
|
||||
"discussions.topics.sort.courseStructure": "Ders Yapısı",
|
||||
"discussions.topics.unnamed.label": "İsimsiz kategori",
|
||||
"discussions.subtopics.unnamed.label": "İsimsiz alt kategori",
|
||||
"tour.action.advance": "Sonraki",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Tamam",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Course Material",
|
||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Add a response",
|
||||
"discussions.comments.comment.addComment": "Add a comment",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Content reported for staff to review",
|
||||
"discussions.actions.back.alt": "Back to list",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n =0 {No responses}\n one {Showing # response}\n other {Showing # responses}\n }",
|
||||
@@ -36,9 +36,9 @@
|
||||
"discussions.editor.comments.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.editReasonCode.error": "Select reason for editing",
|
||||
"discussions.comment.comments.editedBy": "Edited by",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Reason",
|
||||
"discussions.post.closedBy": "Post closed by",
|
||||
"discussion.comment.repliesHeading": "{count} replies for the response added",
|
||||
"discussion.comment.time": "{time} ago",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
@@ -141,7 +141,7 @@
|
||||
"discussions.post.editor.anonymousPost": "Post anonymously",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Post anonymously to peers",
|
||||
"discussions.editor.posts.editReasonCode": "Reason for editing",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.editor.posts.showPreview.button": "Show Preview",
|
||||
"discussions.topic.noName.label": "Unnamed category",
|
||||
"discussions.subtopic.noName.label": "Unnamed subcategory",
|
||||
"discussions.posts.filter.showALl": "Show all",
|
||||
@@ -163,7 +163,6 @@
|
||||
"discussions.posts.sort.voteCount": "Most likes",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonymous",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.lastResponse": "Last response {time}",
|
||||
"discussions.post.postedOn": "Posted {time} by {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Reported",
|
||||
|
||||
@@ -41,10 +41,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
outline: #EAE6E5 solid 2px;
|
||||
}
|
||||
|
||||
.font-size-16 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.font-size-14 {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -94,16 +90,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
width: 16px !important;
|
||||
}
|
||||
|
||||
.post-summary-comment-count-dimensions {
|
||||
height: 15.39px;
|
||||
width: 15.5px
|
||||
}
|
||||
|
||||
.post-summary-like-dimensions {
|
||||
height: 16px;
|
||||
width: 17px
|
||||
}
|
||||
|
||||
.post-summary-timestamp {
|
||||
font-size: 12px !important;
|
||||
line-height: 20px !important;
|
||||
@@ -144,11 +130,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mx-3px {
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.mt-14px {
|
||||
margin-top: 14px;
|
||||
}
|
||||
@@ -205,14 +186,9 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.py-2px {
|
||||
padding-top: 2px !important;
|
||||
padding-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.question-icon-size {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
width: 1.4581rem;
|
||||
height: 1.4581rem;
|
||||
}
|
||||
|
||||
.question-icon-position {
|
||||
@@ -441,19 +417,10 @@ header {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.post-card-comment {
|
||||
&:not(:hover),
|
||||
&:not(:focus) {
|
||||
.hover-card {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.hover-card {
|
||||
display: flex;
|
||||
}
|
||||
.post-card-comment:hover,
|
||||
.post-card-comment:focus {
|
||||
.hover-card {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,25 +428,3 @@ header {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.MJX-TEX {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.font-style {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.in-context-navbar {
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
.line-height-24 {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.comments-sort {
|
||||
margin-bottom: -44px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { configureStore } from '@reduxjs/toolkit';
|
||||
import { courseTabsReducer } from './components/NavigationBar/data';
|
||||
import { blocksReducer } from './data/slices';
|
||||
import { cohortsReducer } from './discussions/cohorts/data';
|
||||
import { commentsReducer } from './discussions/comments/data';
|
||||
import { configReducer } from './discussions/data/slices';
|
||||
import { inContextTopicsReducer } from './discussions/in-context-topics/data';
|
||||
import { learnersReducer } from './discussions/learners/data';
|
||||
import { commentsReducer } from './discussions/post-comments/data';
|
||||
import { threadsReducer } from './discussions/posts/data';
|
||||
import { topicsReducer } from './discussions/topics/data';
|
||||
import { toursReducer } from './discussions/tours/data';
|
||||
|
||||
Reference in New Issue
Block a user