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.
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
@@ -1,20 +0,0 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
12
.github/workflows/self-assign-issue.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
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'
|
||||
@@ -13,178 +13,41 @@
|
||||
window.MathJax = {
|
||||
tex: {
|
||||
inlineMath: [
|
||||
["$", "$"],
|
||||
["\\\\(", "\\\\)"],
|
||||
["\\(", "\\)"],
|
||||
["[mathjaxinline]", "[/mathjaxinline]"],
|
||||
["\\begin{math}", "\\end{math}"],
|
||||
['$', '$'],
|
||||
['\\\\(', '\\\\)'],
|
||||
['\\(', '\\)'],
|
||||
['[mathjaxinline]', '[/mathjaxinline]'],
|
||||
['\\begin{math}', '\\end{math}'],
|
||||
],
|
||||
displayMath: [
|
||||
["[mathjax]", "[/mathjax]"],
|
||||
["$$", "$$"],
|
||||
["\\\\[", "\\\\]"],
|
||||
["\\[", "\\]"],
|
||||
["\\begin{displaymath}", "\\end{displaymath}"],
|
||||
["\\begin{equation}", "\\end{equation}"],
|
||||
['[mathjax]', '[/mathjax]'],
|
||||
['$$', '$$'],
|
||||
['\\\\[', '\\\\]'],
|
||||
['\\[', '\\]'],
|
||||
['\\begin{displaymath}', '\\end{displaymath}'],
|
||||
['\\begin{equation}', '\\end{equation}'],
|
||||
],
|
||||
processEscapes: true,
|
||||
processEnvironments: true,
|
||||
autoload: {
|
||||
color: [],
|
||||
colorv2: ["color"],
|
||||
colorv2: ['color']
|
||||
},
|
||||
packages: { "[+]": ["noerrors"] },
|
||||
packages: {'[+]': ['noerrors']}
|
||||
},
|
||||
options: {
|
||||
ignoreHtmlClass: "tex2jax_ignore",
|
||||
processHtmlClass: "tex2jax_process",
|
||||
ignoreHtmlClass: 'tex2jax_ignore',
|
||||
processHtmlClass: 'tex2jax_process'
|
||||
},
|
||||
loader: {
|
||||
load: ["input/asciimath", "[tex]/noerrors"],
|
||||
},
|
||||
load: ['input/asciimath', '[tex]/noerrors']
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script
|
||||
defer
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
|
||||
id="MathJax-script"
|
||||
></script>
|
||||
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="small"></div>
|
||||
|
||||
<!-- begin usabilla live embed code -->
|
||||
<script defer type="text/javascript">
|
||||
window.lightningjs ||
|
||||
(function (n) {
|
||||
var e = "lightningjs";
|
||||
function t(e, t) {
|
||||
var r, i, a, o, d, c;
|
||||
return (
|
||||
t && (t += (/\?/.test(t) ? "&" : "?") + "lv=1"),
|
||||
n[e] ||
|
||||
((r = window),
|
||||
(i = document),
|
||||
(a = e),
|
||||
(o = i.location.protocol),
|
||||
(d = "load"),
|
||||
(c = 0),
|
||||
(function () {
|
||||
n[a] = function () {
|
||||
var t = arguments,
|
||||
i = this,
|
||||
o = ++c,
|
||||
d = (i && i != r && i.id) || 0;
|
||||
function s() {
|
||||
return (s.id = o), n[a].apply(s, arguments);
|
||||
}
|
||||
return (
|
||||
(e.s = e.s || []).push([o, d, t]),
|
||||
(s.then = function (n, t, r) {
|
||||
var i = (e.fh[o] = e.fh[o] || []),
|
||||
a = (e.eh[o] = e.eh[o] || []),
|
||||
d = (e.ph[o] = e.ph[o] || []);
|
||||
return (
|
||||
n && i.push(n), t && a.push(t), r && d.push(r), s
|
||||
);
|
||||
}),
|
||||
s
|
||||
);
|
||||
};
|
||||
var e = (n[a]._ = {});
|
||||
function s() {
|
||||
e.P(d), (e.w = 1), n[a]("_load");
|
||||
}
|
||||
(e.fh = {}),
|
||||
(e.eh = {}),
|
||||
(e.ph = {}),
|
||||
(e.l = t
|
||||
? t.replace(/^\/\//, ("https:" == o ? o : "http:") + "//")
|
||||
: t),
|
||||
(e.p = { 0: +new Date() }),
|
||||
(e.P = function (n) {
|
||||
e.p[n] = new Date() - e.p[0];
|
||||
}),
|
||||
e.w && s(),
|
||||
r.addEventListener
|
||||
? r.addEventListener(d, s, !1)
|
||||
: r.attachEvent("onload", s);
|
||||
var l = function () {
|
||||
function n() {
|
||||
return [
|
||||
"<!DOCTYPE ",
|
||||
o,
|
||||
"><",
|
||||
o,
|
||||
"><head></head><",
|
||||
t,
|
||||
"><",
|
||||
r,
|
||||
' src="',
|
||||
e.l,
|
||||
'"></',
|
||||
r,
|
||||
"></",
|
||||
t,
|
||||
"></",
|
||||
o,
|
||||
">",
|
||||
].join("");
|
||||
}
|
||||
var t = "body",
|
||||
r = "script",
|
||||
o = "html",
|
||||
d = i[t];
|
||||
if (!d) return setTimeout(l, 100);
|
||||
e.P(1);
|
||||
var c,
|
||||
s = i.createElement("div"),
|
||||
h = s.appendChild(i.createElement("div")),
|
||||
u = i.createElement("iframe");
|
||||
(s.style.display = "none"),
|
||||
(d.insertBefore(s, d.firstChild).id = "lightningjs-" + a),
|
||||
(u.frameBorder = "0"),
|
||||
(u.id = "lightningjs-frame-" + a),
|
||||
/MSIE[ ]+6/.test(navigator.userAgent) &&
|
||||
(u.src = "javascript:false"),
|
||||
(u.allowTransparency = "true"),
|
||||
h.appendChild(u);
|
||||
try {
|
||||
u.contentWindow.document.open();
|
||||
} catch (n) {
|
||||
(e.domain = i.domain),
|
||||
(c =
|
||||
"javascript:var d=document.open();d.domain='" +
|
||||
i.domain +
|
||||
"';"),
|
||||
(u.src = c + "void(0);");
|
||||
}
|
||||
try {
|
||||
var p = u.contentWindow.document;
|
||||
p.write(n()), p.close();
|
||||
} catch (e) {
|
||||
u.src =
|
||||
c +
|
||||
'd.write("' +
|
||||
n().replace(/"/g, String.fromCharCode(92) + '"') +
|
||||
'");d.close();';
|
||||
}
|
||||
e.P(2);
|
||||
};
|
||||
e.l && l();
|
||||
})()),
|
||||
(n[e].lv = "1"),
|
||||
n[e]
|
||||
);
|
||||
}
|
||||
var r = (window.lightningjs = t(e));
|
||||
(r.require = t), (r.modules = n);
|
||||
})({});
|
||||
window.usabilla_live = lightningjs.require(
|
||||
"usabilla_live",
|
||||
"//w.usabilla.com/9e6036348fa1.js"
|
||||
);
|
||||
</script>
|
||||
<!-- end usabilla live embed code -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,34 +5,30 @@ 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'],
|
||||
};
|
||||
|
||||
function HTMLLoader({
|
||||
htmlNode, componentId, cssClassName, testId, delay,
|
||||
htmlNode, componentId, cssClassName, testId,
|
||||
}) {
|
||||
const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions });
|
||||
const previewRef = useRef();
|
||||
|
||||
const debouncedPostContent = useDebounce(htmlNode, delay);
|
||||
|
||||
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} />
|
||||
@@ -45,7 +41,6 @@ HTMLLoader.propTypes = {
|
||||
componentId: PropTypes.string,
|
||||
cssClassName: PropTypes.string,
|
||||
testId: PropTypes.string,
|
||||
delay: PropTypes.number,
|
||||
};
|
||||
|
||||
HTMLLoader.defaultProps = {
|
||||
@@ -53,7 +48,6 @@ HTMLLoader.defaultProps = {
|
||||
componentId: null,
|
||||
cssClassName: '',
|
||||
testId: '',
|
||||
delay: 0,
|
||||
};
|
||||
|
||||
export default HTMLLoader;
|
||||
|
||||
@@ -29,22 +29,17 @@ function PostPreviewPane({
|
||||
className="float-right p-3"
|
||||
iconClassNames="icon-size"
|
||||
/>
|
||||
<HTMLLoader
|
||||
htmlNode={htmlNode}
|
||||
cssClassName="text-primary"
|
||||
componentId="post-preview"
|
||||
testId="post-preview"
|
||||
delay={500}
|
||||
/>
|
||||
<HTMLLoader htmlNode={htmlNode} cssClassName="text-primary" componentId="post-preview" testId="post-preview" />
|
||||
</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,
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
/* eslint react/prop-types: 0 */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
selectUserHasModerationPrivileges,
|
||||
selectUserIsGroupTa,
|
||||
} from '../discussions/data/selectors';
|
||||
import messages from '../discussions/in-context-topics/messages';
|
||||
|
||||
function TopicStats({
|
||||
threadCounts,
|
||||
activeFlags,
|
||||
inactiveFlags,
|
||||
intl,
|
||||
}) {
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const canSeeReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa);
|
||||
return (
|
||||
<div className="d-flex align-items-center mt-2.5" style={{ marginBottom: '2px' }}>
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{intl.formatMessage(messages.discussions, {
|
||||
count: threadCounts?.discussion || 0,
|
||||
})}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center mr-3.5">
|
||||
<Icon src={PostOutline} className="icon-size mr-2" />
|
||||
{threadCounts?.discussion || 0}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{intl.formatMessage(messages.questions, {
|
||||
count: threadCounts?.question || 0,
|
||||
})}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center mr-3.5">
|
||||
<Icon src={HelpOutline} className="icon-size mr-2" />
|
||||
{threadCounts?.question || 0}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
{Boolean(canSeeReportedStats) && (
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{Boolean(activeFlags) && (
|
||||
<span>
|
||||
{intl.formatMessage(messages.reported, { reported: activeFlags })}
|
||||
</span>
|
||||
)}
|
||||
{Boolean(inactiveFlags) && (
|
||||
<span>
|
||||
{intl.formatMessage(messages.previouslyReported, { previouslyReported: inactiveFlags })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon src={Report} className="icon-size mr-2 text-danger" />
|
||||
{activeFlags}{Boolean(inactiveFlags) && `/${inactiveFlags}`}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TopicStats.propTypes = {
|
||||
threadCounts: PropTypes.shape({
|
||||
discussions: PropTypes.number,
|
||||
questions: PropTypes.number,
|
||||
}),
|
||||
activeFlags: PropTypes.number,
|
||||
inactiveFlags: PropTypes.number,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
TopicStats.defaultProps = {
|
||||
threadCounts: {
|
||||
discussions: 0,
|
||||
questions: 0,
|
||||
},
|
||||
activeFlags: null,
|
||||
inactiveFlags: null,
|
||||
};
|
||||
|
||||
export default injectIntl(TopicStats);
|
||||
@@ -1,4 +1,3 @@
|
||||
export { default as PostActionsBar } from '../discussions/posts/post-actions-bar/PostActionsBar';
|
||||
export { default as Search } from './Search';
|
||||
export { default as TinyMCEEditor } from './TinyMCEEditor';
|
||||
export { default as TopicStats } from './TopicStats';
|
||||
|
||||
@@ -63,7 +63,6 @@ export const ContentActions = {
|
||||
* @enum {string}
|
||||
*/
|
||||
export const RequestStatus = {
|
||||
IDLE: 'idle',
|
||||
IN_PROGRESS: 'in-progress',
|
||||
SUCCESSFUL: 'successful',
|
||||
FAILED: 'failed',
|
||||
|
||||
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);
|
||||
@@ -30,7 +30,6 @@ 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;
|
||||
@@ -47,7 +46,6 @@ function mockAxiosReturnPagedComments() {
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
endorsed,
|
||||
reverse_order: reverseOrder,
|
||||
},
|
||||
})
|
||||
.reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
@@ -84,7 +82,7 @@ function mockAxiosReturnPagedCommentsResponses() {
|
||||
}
|
||||
|
||||
function renderComponent(postId) {
|
||||
const wrapper = render(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
@@ -104,10 +102,9 @@ function renderComponent(postId) {
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
describe('ThreadView', () => {
|
||||
describe('CommentsView', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -165,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();
|
||||
@@ -182,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(() => {
|
||||
@@ -205,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 () => {
|
||||
@@ -257,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,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 () => {
|
||||
@@ -313,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();
|
||||
@@ -340,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();
|
||||
@@ -360,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();
|
||||
@@ -379,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 () => {
|
||||
@@ -394,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 () => {
|
||||
@@ -409,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 () => {
|
||||
@@ -427,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 });
|
||||
@@ -458,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 });
|
||||
@@ -470,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 () => {
|
||||
@@ -502,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();
|
||||
});
|
||||
@@ -515,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 () => {
|
||||
@@ -525,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();
|
||||
});
|
||||
|
||||
@@ -540,7 +552,7 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
}
|
||||
|
||||
await screen.findByTestId('comment-comment-2');
|
||||
await screen.findByTestId('comment-2');
|
||||
await expect(findLoadMoreCommentsButton())
|
||||
.rejects
|
||||
.toThrow();
|
||||
@@ -552,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();
|
||||
});
|
||||
@@ -569,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();
|
||||
|
||||
@@ -585,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
|
||||
@@ -597,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 () => {
|
||||
@@ -621,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 () => {
|
||||
@@ -632,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 () => {
|
||||
@@ -648,52 +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();
|
||||
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" tabIndex="0">
|
||||
<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,11 +120,11 @@ 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>
|
||||
{!enableInContextSidebar && <DiscussionsProductTour />}
|
||||
<DiscussionsProductTour />
|
||||
</main>
|
||||
{!enableInContextSidebar && <Footer />}
|
||||
</DiscussionContext.Provider>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
@@ -8,7 +8,6 @@ import { Spinner } from '@edx/paragon';
|
||||
|
||||
import { RequestStatus, Routes } from '../../data/constants';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { selectDiscussionProvider } from '../data/selectors';
|
||||
import { selectTopicThreads } from '../posts/data/selectors';
|
||||
import PostsList from '../posts/PostsList';
|
||||
import { discussionsPath, handleKeyDown } from '../utils';
|
||||
@@ -16,18 +15,14 @@ import {
|
||||
selectArchivedTopic, selectLoadingStatus, selectNonCoursewareTopics,
|
||||
selectSubsection, selectSubsectionUnits, selectUnits,
|
||||
} from './data/selectors';
|
||||
import { fetchCourseTopicsV3 } from './data/thunks';
|
||||
import { BackButton, NoResults } from './components';
|
||||
import messages from './messages';
|
||||
import { Topic } from './topic';
|
||||
|
||||
function TopicPostsView({ intl }) {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const { courseId, topicId, category } = useContext(DiscussionContext);
|
||||
const provider = useSelector(selectDiscussionProvider);
|
||||
const topicsStatus = useSelector(selectLoadingStatus);
|
||||
const topicsInProgress = topicsStatus === RequestStatus.IN_PROGRESS;
|
||||
const topicsLoadingStatus = useSelector(selectLoadingStatus);
|
||||
const posts = useSelector(selectTopicThreads([topicId]));
|
||||
const selectedSubsectionUnits = useSelector(selectSubsectionUnits(category));
|
||||
const selectedSubsection = useSelector(selectSubsection(category));
|
||||
@@ -35,12 +30,6 @@ function TopicPostsView({ intl }) {
|
||||
const selectedNonCoursewareTopic = useSelector(selectNonCoursewareTopics)?.find(topic => topic.id === topicId);
|
||||
const selectedArchivedTopic = useSelector(selectArchivedTopic(topicId));
|
||||
|
||||
useEffect(() => {
|
||||
if (provider && topicsStatus === RequestStatus.IDLE) {
|
||||
dispatch(fetchCourseTopicsV3(courseId));
|
||||
}
|
||||
}, [provider]);
|
||||
|
||||
const backButtonPath = () => {
|
||||
const path = selectedUnit ? Routes.TOPICS.CATEGORY : Routes.TOPICS.ALL;
|
||||
const params = selectedUnit ? { courseId, category: selectedUnit?.parentId } : { courseId };
|
||||
@@ -51,14 +40,12 @@ function TopicPostsView({ intl }) {
|
||||
<div className="discussion-posts d-flex flex-column h-100">
|
||||
{topicId ? (
|
||||
<BackButton
|
||||
loading={topicsInProgress}
|
||||
path={backButtonPath()}
|
||||
title={selectedUnit?.name || selectedNonCoursewareTopic?.name || selectedArchivedTopic?.name
|
||||
|| intl.formatMessage(messages.unnamedTopic)}
|
||||
/>
|
||||
) : (
|
||||
<BackButton
|
||||
loading={topicsInProgress}
|
||||
path={discussionsPath(Routes.TOPICS.ALL, { courseId })(location)}
|
||||
title={selectedSubsection?.displayName || intl.formatMessage(messages.unnamedSubsection)}
|
||||
/>
|
||||
@@ -69,7 +56,6 @@ function TopicPostsView({ intl }) {
|
||||
<PostsList
|
||||
posts={posts}
|
||||
topics={[topicId]}
|
||||
parentIsLoading={topicsInProgress}
|
||||
/>
|
||||
) : (
|
||||
selectedSubsectionUnits?.map((unit) => (
|
||||
@@ -79,10 +65,10 @@ function TopicPostsView({ intl }) {
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{(category && selectedSubsectionUnits.length === 0 && topicsStatus === RequestStatus.SUCCESSFUL) && (
|
||||
{(category && selectedSubsectionUnits.length === 0 && topicsLoadingStatus === RequestStatus.SUCCESSFUL) && (
|
||||
<NoResults />
|
||||
)}
|
||||
{(category && topicsInProgress) && (
|
||||
{(category && topicsLoadingStatus === RequestStatus.IN_PROGRESS) && (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
import {
|
||||
fireEvent, render, screen, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { generatePath, MemoryRouter, Route } from 'react-router';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { PostActionsBar } from '../../components';
|
||||
import { Routes } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import { getCourseTopicsApiUrl } from './data/api';
|
||||
import { selectCoursewareTopics } from './data/selectors';
|
||||
import { fetchCourseTopicsV3 } from './data/thunks';
|
||||
import TopicPostsView from './TopicPostsView';
|
||||
import TopicsView from './TopicsView';
|
||||
|
||||
import './data/__factories__';
|
||||
import '../posts/data/__factories__/threads.factory';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const topicsApiUrl = getCourseTopicsApiUrl();
|
||||
let store;
|
||||
let axiosMock;
|
||||
let lastLocation;
|
||||
let container;
|
||||
|
||||
async function renderComponent({ topicId, category } = { }) {
|
||||
let path = `/${courseId}/topics`;
|
||||
if (topicId) {
|
||||
path = generatePath(Routes.POSTS.PATH, { courseId, topicId });
|
||||
} else if (category) {
|
||||
path = generatePath(Routes.TOPICS.CATEGORY, { courseId, category });
|
||||
}
|
||||
const wrapper = await render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{
|
||||
courseId,
|
||||
topicId,
|
||||
category,
|
||||
page: 'topics',
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<Route exact path={[Routes.POSTS.PATH, Routes.TOPICS.CATEGORY]}>
|
||||
<TopicPostsView />
|
||||
</Route>
|
||||
<Route exact path={[Routes.TOPICS.ALL]}>
|
||||
<PostActionsBar />
|
||||
<TopicsView />
|
||||
</Route>
|
||||
<Route
|
||||
render={({ location }) => {
|
||||
lastLocation = location;
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
}
|
||||
|
||||
describe('InContext Topic Posts View', () => {
|
||||
let coursewareTopics;
|
||||
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore({
|
||||
config: {
|
||||
enableInContext: true,
|
||||
provider: 'openedx',
|
||||
hasModerationPrivileges: true,
|
||||
blackouts: [],
|
||||
},
|
||||
});
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
lastLocation = undefined;
|
||||
});
|
||||
|
||||
async function setupTopicsMockResponse() {
|
||||
axiosMock.onGet(`${topicsApiUrl}${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();
|
||||
coursewareTopics = selectCoursewareTopics(state);
|
||||
}
|
||||
|
||||
async function setupPostsMockResponse(topicId, numOfResponses = 3) {
|
||||
axiosMock.onGet(threadsApiUrl)
|
||||
.reply(() => {
|
||||
const threadAttrs = { previewBody: 'thread preview body' };
|
||||
return [200, Factory.build('threadsResult', {}, {
|
||||
topicId,
|
||||
threadAttrs,
|
||||
count: numOfResponses,
|
||||
})];
|
||||
});
|
||||
await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
test.each([
|
||||
{ parentId: 'noncourseware-topic-1', parentTitle: 'general-topic-1', topicType: 'NonCourseware' },
|
||||
{ parentId: 'courseware-topic-1-v3-1', parentTitle: 'Introduction Introduction 1-1-1', topicType: 'Courseware' },
|
||||
])('\'$topicType\' topic should have a required number of post lengths.', async ({ parentId, parentTitle }) => {
|
||||
await setupTopicsMockResponse();
|
||||
await setupPostsMockResponse(parentId, 3);
|
||||
|
||||
await act(async () => {
|
||||
renderComponent({ topicId: parentId });
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
const posts = await container.querySelectorAll('.discussion-post');
|
||||
const backButton = screen.getByLabelText('Back to topics list');
|
||||
const parentHeader = await screen.findByText(parentTitle);
|
||||
|
||||
expect(lastLocation.pathname.endsWith(`/topics/${parentId}`)).toBeTruthy();
|
||||
expect(posts).toHaveLength(3);
|
||||
expect(backButton).toBeInTheDocument();
|
||||
expect(parentHeader).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('A back button should redirect from list of posts to list of units.', async () => {
|
||||
await setupTopicsMockResponse();
|
||||
const subSection = coursewareTopics[0].children[0];
|
||||
const unit = subSection.children[0];
|
||||
|
||||
await act(async () => {
|
||||
setupPostsMockResponse(unit.id, 2);
|
||||
renderComponent({ topicId: unit.id });
|
||||
});
|
||||
|
||||
const backButton = await screen.getByLabelText('Back to topics list');
|
||||
|
||||
await act(async () => fireEvent.click(backButton));
|
||||
await waitFor(async () => {
|
||||
renderComponent({ category: subSection.id });
|
||||
|
||||
const subSectionList = await container.querySelector('.list-group');
|
||||
const units = subSectionList.querySelectorAll('.discussion-topic');
|
||||
const unitHeader = within(subSectionList).queryByText(unit.name);
|
||||
|
||||
expect(lastLocation.pathname.endsWith(`/category/${subSection.id}`)).toBeTruthy();
|
||||
expect(unitHeader).toBeInTheDocument();
|
||||
expect(units).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('A back button should redirect from units to the parent/selected subsection.', async () => {
|
||||
await setupTopicsMockResponse();
|
||||
const subSection = coursewareTopics[0].children[0];
|
||||
|
||||
renderComponent({ category: subSection.id });
|
||||
|
||||
const backButton = await screen.getByLabelText('Back to topics list');
|
||||
|
||||
await act(async () => fireEvent.click(backButton));
|
||||
await waitFor(async () => {
|
||||
renderComponent();
|
||||
|
||||
const sectionList = await container.querySelector('.list-group');
|
||||
const subSections = sectionList.querySelectorAll('.discussion-topic-group');
|
||||
const subSectionHeader = within(sectionList).queryByText(subSection.displayName);
|
||||
|
||||
expect(lastLocation.pathname.endsWith('/topics')).toBeTruthy();
|
||||
expect(subSectionHeader).toBeInTheDocument();
|
||||
expect(subSections).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ searchText: 'hello world', output: 'Showing 0 results for', resultCount: 0 },
|
||||
{ searchText: 'introduction', output: 'Showing 8 results for', resultCount: 8 },
|
||||
])('It should have a search bar with a clear button and \'$output\' results found text.',
|
||||
async ({ searchText, output, resultCount }) => {
|
||||
await setupTopicsMockResponse();
|
||||
await renderComponent();
|
||||
|
||||
const searchField = await within(container).getByPlaceholderText('Search topics');
|
||||
const searchButton = await within(container).getByTestId('search-icon');
|
||||
fireEvent.change(searchField, { target: { value: searchText } });
|
||||
|
||||
await waitFor(async () => expect(searchField).toHaveValue(searchText));
|
||||
await act(async () => fireEvent.click(searchButton));
|
||||
await waitFor(async () => {
|
||||
const clearButton = await within(container).queryByText('Clear results');
|
||||
const searchMessage = within(container).queryByText(`${output} "${searchText}"`);
|
||||
const units = container.querySelectorAll('.discussion-topic');
|
||||
|
||||
expect(searchMessage).toBeInTheDocument();
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
expect(units).toHaveLength(resultCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('When click on the clear button it should move to main topics pages.', async () => {
|
||||
await setupTopicsMockResponse();
|
||||
await renderComponent();
|
||||
|
||||
const searchText = 'hello world';
|
||||
const searchField = await within(container).getByPlaceholderText('Search topics');
|
||||
const searchButton = await within(container).getByTestId('search-icon');
|
||||
|
||||
fireEvent.change(searchField, { target: { value: searchText } });
|
||||
|
||||
await waitFor(async () => expect(searchField).toHaveValue(searchText));
|
||||
await act(async () => fireEvent.click(searchButton));
|
||||
await waitFor(async () => {
|
||||
const clearButton = await within(container).queryByText('Clear results');
|
||||
|
||||
await act(async () => fireEvent.click(clearButton));
|
||||
await waitFor(async () => {
|
||||
const coursewareTopicList = await container.querySelectorAll('.discussion-topic-group');
|
||||
|
||||
expect(coursewareTopicList).toHaveLength(3);
|
||||
expect(within(container).queryByText('Clear results')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,233 +0,0 @@
|
||||
import {
|
||||
fireEvent, render, screen, waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { getCourseTopicsApiUrl } from './data/api';
|
||||
import { selectCoursewareTopics, selectNonCoursewareTopics } from './data/selectors';
|
||||
import { fetchCourseTopicsV3 } from './data/thunks';
|
||||
import TopicPostsView from './TopicPostsView';
|
||||
import TopicsView from './TopicsView';
|
||||
|
||||
import './data/__factories__';
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const category = 'section-topic-1';
|
||||
|
||||
const topicsApiUrl = `${getCourseTopicsApiUrl()}`;
|
||||
let store;
|
||||
let axiosMock;
|
||||
let lastLocation;
|
||||
let container;
|
||||
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId, category }}>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/topics/`]}>
|
||||
<Route path="/:courseId/topics/">
|
||||
<TopicsView />
|
||||
</Route>
|
||||
<Route path="/:courseId/category/:category">
|
||||
<TopicPostsView />
|
||||
</Route>
|
||||
<Route
|
||||
render={({ location }) => {
|
||||
lastLocation = location;
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
}
|
||||
|
||||
describe('InContext Topics View', () => {
|
||||
let nonCoursewareTopics;
|
||||
let coursewareTopics;
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore({
|
||||
config: { enableInContext: true, provider: 'openedx', hasModerationPrivileges: true },
|
||||
});
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
lastLocation = undefined;
|
||||
});
|
||||
|
||||
async function setupMockResponse() {
|
||||
axiosMock.onGet(`${topicsApiUrl}${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();
|
||||
nonCoursewareTopics = selectNonCoursewareTopics(state);
|
||||
coursewareTopics = selectCoursewareTopics(state);
|
||||
}
|
||||
|
||||
it('A non-courseware topic should be clickable and should have a title', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
|
||||
const nonCourseware = nonCoursewareTopics[0];
|
||||
const nonCoursewareTopic = await screen.findByText(nonCourseware.name);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(nonCoursewareTopic);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(nonCourseware.name)).toBeInTheDocument();
|
||||
expect(lastLocation.pathname.endsWith(`/topics/${nonCourseware.id}`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('A non-courseware topic should be on the top of the list', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
const topic = await container.querySelector('.discussion-topic');
|
||||
|
||||
expect(within(topic).queryByText('general-topic-1')).toBeInTheDocument();
|
||||
expect(topic.nextSibling).toBe(container.querySelector('.divider'));
|
||||
});
|
||||
|
||||
it('A non-Courseware topic should have 3 stats and should be hoverable', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
|
||||
const topic = await container.querySelector('.discussion-topic');
|
||||
const statsList = await topic.querySelectorAll('.icon-size');
|
||||
|
||||
expect(statsList.length).toBe(3);
|
||||
fireEvent.mouseOver(statsList[0]);
|
||||
expect(screen.queryByText('1 Discussion')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Section groups should be listed in the middle of the topics list.', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
const topicsList = await screen.getByRole('list');
|
||||
const sectionGroups = await screen.getAllByTestId('section-group');
|
||||
|
||||
expect(topicsList.children[1]).toStrictEqual(topicsList.querySelector('.divider'));
|
||||
expect(sectionGroups.length).toBe(2);
|
||||
expect(topicsList.children[5]).toStrictEqual(topicsList.querySelector('.divider'));
|
||||
});
|
||||
|
||||
it('A section group should have only a title and required subsections.', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
const sectionGroups = await screen.getAllByTestId('section-group');
|
||||
|
||||
coursewareTopics.forEach(async (topic, index) => {
|
||||
const stats = await sectionGroups[index].querySelectorAll('.icon-size:not([data-testid="subsection-group"].icon-size)');
|
||||
const subsectionGroups = await within(sectionGroups[index]).getAllByTestId('subsection-group');
|
||||
|
||||
expect(within(sectionGroups[index]).queryByText(topic.displayName)).toBeInTheDocument();
|
||||
expect(stats).toHaveLength(0);
|
||||
expect(subsectionGroups).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('The subsection should have a title name, be clickable, and have the stats', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
const subsectionObject = coursewareTopics[0].children[0];
|
||||
const subSection = await container.querySelector(`[data-subsection-id=${subsectionObject.id}]`);
|
||||
const subSectionTitle = await within(subSection).queryByText(subsectionObject.displayName);
|
||||
const statsList = await subSection.querySelectorAll('.icon-size');
|
||||
|
||||
expect(subSectionTitle).toBeInTheDocument();
|
||||
expect(statsList).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Subsection names should be clickable and redirected to the units lists', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
|
||||
const subsectionObject = coursewareTopics[0].children[0];
|
||||
const subSection = await container.querySelector(`[data-subsection-id=${subsectionObject.id}]`);
|
||||
|
||||
await act(async () => fireEvent.click(subSection));
|
||||
await waitFor(async () => {
|
||||
const backButton = await screen.getByLabelText('Back to topics list');
|
||||
const topicsList = await screen.getByRole('list');
|
||||
const subSectionHeading = await screen.findByText(subsectionObject.displayName);
|
||||
const units = await topicsList.querySelectorAll('.discussion-topic');
|
||||
|
||||
expect(backButton).toBeInTheDocument();
|
||||
expect(subSectionHeading).toBeInTheDocument();
|
||||
expect(units).toHaveLength(4);
|
||||
expect(lastLocation.pathname.endsWith(`/category/${subsectionObject.id}`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('The number of units should be matched with the actual unit length.', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
const subSection = await container.querySelector(`[data-subsection-id=${coursewareTopics[0].children[0].id}]`);
|
||||
|
||||
await act(async () => fireEvent.click(subSection));
|
||||
await waitFor(async () => {
|
||||
const units = await container.querySelectorAll('.discussion-topic');
|
||||
|
||||
expect(units).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('A unit should have a title and stats and should be clickable', async () => {
|
||||
await setupMockResponse();
|
||||
renderComponent();
|
||||
const subSectionObject = coursewareTopics[0].children[0];
|
||||
const unitObject = subSectionObject.children[0];
|
||||
|
||||
const subSection = await container.querySelector(`[data-subsection-id=${subSectionObject.id}]`);
|
||||
|
||||
await act(async () => fireEvent.click(subSection));
|
||||
await waitFor(async () => {
|
||||
const unitElement = await screen.findByText(unitObject.name);
|
||||
const unitContainer = await container.querySelector(`[data-topic-id=${unitObject.id}]`);
|
||||
const statsList = await unitContainer.querySelectorAll('.icon-size');
|
||||
|
||||
expect(unitElement).toBeInTheDocument();
|
||||
expect(statsList).toHaveLength(3);
|
||||
|
||||
await act(async () => fireEvent.click(unitContainer));
|
||||
await waitFor(async () => {
|
||||
expect(lastLocation.pathname.endsWith(`/topics/${unitObject.id}`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,14 +4,12 @@ import PropTypes from 'prop-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton, Spinner } from '@edx/paragon';
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function BackButton({
|
||||
intl, path, title, loading,
|
||||
}) {
|
||||
function BackButton({ intl, path, title }) {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
@@ -26,7 +24,7 @@ function BackButton({
|
||||
alt={intl.formatMessage(messages.backAlt)}
|
||||
/>
|
||||
<div className="d-flex flex-fill justify-content-center align-items-center mr-4.5">
|
||||
{loading ? <Spinner animation="border" variant="primary" size="sm" /> : title}
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-bottom border-light-400" />
|
||||
@@ -38,11 +36,6 @@ BackButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
path: PropTypes.shape({}).isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
};
|
||||
|
||||
BackButton.defaultProps = {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
export default injectIntl(BackButton);
|
||||
|
||||
@@ -1,89 +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('activeFlags', null, true)
|
||||
.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('activeFlags', null, true)
|
||||
.attr('thread_counts', ['discussionCount', 'questionCount'], (discCount, questCount) => {
|
||||
Factory.reset('thread-counts');
|
||||
return Factory.build('thread-counts', null, { discussionCount: discCount, questionCount: questCount });
|
||||
})
|
||||
.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}-v3`)
|
||||
.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', ['id', 'display-name'], (id, name) => {
|
||||
Factory.reset('sub-section');
|
||||
return Factory.buildList('sub-section', 2, null, {
|
||||
sectionPrefix: `${name}-`,
|
||||
topicPrefix: 'section',
|
||||
id,
|
||||
discussionCount: 1,
|
||||
questionCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
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}-v3`);
|
||||
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(`courseware-topic-${index + 1}-v3-${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}-v3`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { RequestStatus } from '../../../data/constants';
|
||||
const topicsSlice = createSlice({
|
||||
name: 'inContextTopics',
|
||||
initialState: {
|
||||
status: RequestStatus.IDLE,
|
||||
status: RequestStatus.IN_PROGRESS,
|
||||
topics: [],
|
||||
coursewareTopics: [],
|
||||
nonCoursewareTopics: [],
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -50,7 +50,6 @@ function TopicSearchBar({ intl }) {
|
||||
<Icon
|
||||
src={SearchIcon}
|
||||
onClick={() => onSubmit(searchValue)}
|
||||
data-testid="search-icon"
|
||||
/>
|
||||
</span>
|
||||
</SearchField.Advanced>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import TopicStats from '../../../components/TopicStats';
|
||||
import { Routes } from '../../../data/constants';
|
||||
import { discussionsPath } from '../../utils';
|
||||
import messages from '../messages';
|
||||
@@ -50,13 +49,12 @@ function SectionBaseGroup({
|
||||
aria-current={isSelected(section.id) ? 'page' : undefined}
|
||||
tabIndex={(isSelected(subsection.id) || index === 0) ? 0 : -1}
|
||||
>
|
||||
<div className="d-flex flex-row pt-2.5 pb-2 px-4">
|
||||
<div className="d-flex flex-row py-3.5 px-4">
|
||||
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill">
|
||||
<div className="topic-name text-truncate">
|
||||
{subsection?.displayName || intl.formatMessage(messages.unnamedSubsection)}
|
||||
</div>
|
||||
<TopicStats threadCounts={subsection?.threadCounts} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons';
|
||||
|
||||
import TopicStats from '../../../components/TopicStats';
|
||||
import { Routes } from '../../../data/constants';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import { discussionsPath } from '../../utils';
|
||||
@@ -54,11 +53,65 @@ function Topic({
|
||||
{topic?.name || topic?.displayName || intl.formatMessage(messages.unnamedTopicSubCategories)}
|
||||
</div>
|
||||
</div>
|
||||
<TopicStats
|
||||
threadCounts={topic?.threadCounts}
|
||||
activeFlags={topic?.activeFlags}
|
||||
inactiveFlags={topic?.inactiveFlags}
|
||||
/>
|
||||
<div className="d-flex align-items-center mt-2.5" style={{ marginBottom: '2px' }}>
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{intl.formatMessage(messages.discussions, {
|
||||
count: topic.threadCounts?.discussion || 0,
|
||||
})}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center mr-3.5">
|
||||
<Icon src={PostOutline} className="icon-size mr-2" />
|
||||
{topic.threadCounts?.discussion || 0}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{intl.formatMessage(messages.questions, {
|
||||
count: topic.threadCounts?.question || 0,
|
||||
})}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center mr-3.5">
|
||||
<Icon src={HelpOutline} className="icon-size mr-2" />
|
||||
{topic.threadCounts?.question || 0}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
{Boolean(canSeeReportedStats) && (
|
||||
<OverlayTrigger
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<div className="d-flex flex-column align-items-start">
|
||||
{Boolean(activeFlags) && (
|
||||
<span>
|
||||
{intl.formatMessage(messages.reported, { reported: activeFlags })}
|
||||
</span>
|
||||
)}
|
||||
{Boolean(inactiveFlags) && (
|
||||
<span>
|
||||
{intl.formatMessage(messages.previouslyReported, { previouslyReported: inactiveFlags })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon src={Report} className="icon-size mr-2 text-danger" />
|
||||
{activeFlags}{Boolean(inactiveFlags) && `/${inactiveFlags}`}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -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,149 +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);
|
||||
const enableCommentsSort = false;
|
||||
|
||||
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 && enableCommentsSort && <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,78 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
|
||||
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';
|
||||
|
||||
function trackLoadMoreEvent(postId, params) {
|
||||
sendTrackEvent(
|
||||
'edx.forum.responses.loadMore',
|
||||
{
|
||||
postId,
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const params = {
|
||||
endorsed,
|
||||
page: currentPage + 1,
|
||||
reverseOrder,
|
||||
};
|
||||
await dispatch(fetchThreadComments(postId, params));
|
||||
trackLoadMoreEvent(postId, params);
|
||||
};
|
||||
|
||||
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';
|
||||
@@ -23,7 +23,7 @@ import NoResults from './NoResults';
|
||||
import { PostLink } from './post';
|
||||
|
||||
function PostsList({
|
||||
posts, topics, intl, isTopicTab, parentIsLoading,
|
||||
posts, topics, intl, isTopicTab,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
@@ -50,7 +50,8 @@ function PostsList({
|
||||
topicIds,
|
||||
isFilterChanged,
|
||||
};
|
||||
if (showOwnPosts && filters.search === '') {
|
||||
|
||||
if (showOwnPosts) {
|
||||
dispatch(fetchUserPosts(courseId, params));
|
||||
} else {
|
||||
dispatch(fetchThreads(courseId, params));
|
||||
@@ -85,10 +86,10 @@ function PostsList({
|
||||
|
||||
return (
|
||||
<>
|
||||
{!parentIsLoading && postInstances(pinnedPosts)}
|
||||
{!parentIsLoading && postInstances(unpinnedPosts)}
|
||||
{postInstances(pinnedPosts)}
|
||||
{postInstances(unpinnedPosts)}
|
||||
{posts?.length === 0 && loadingStatus === RequestStatus.SUCCESSFUL && <NoResults />}
|
||||
{loadingStatus === RequestStatus.IN_PROGRESS || parentIsLoading ? (
|
||||
{loadingStatus === RequestStatus.IN_PROGRESS ? (
|
||||
<div className="d-flex justify-content-center p-4 mx-auto my-auto">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
@@ -110,7 +111,6 @@ PostsList.propTypes = {
|
||||
})),
|
||||
topics: PropTypes.arrayOf(PropTypes.string),
|
||||
isTopicTab: PropTypes.bool,
|
||||
parentIsLoading: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -118,7 +118,6 @@ PostsList.defaultProps = {
|
||||
posts: [],
|
||||
topics: undefined,
|
||||
isTopicTab: false,
|
||||
parentIsLoading: undefined,
|
||||
};
|
||||
|
||||
export default injectIntl(PostsList);
|
||||
|
||||
@@ -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';
|
||||
@@ -37,7 +40,7 @@ function CategoryPostsList({ category }) {
|
||||
const groupedCategory = useSelector(selectCurrentCategoryGrouping)(category);
|
||||
// If grouping at subsection is enabled, only apply it when browsing discussions in context in the learning MFE.
|
||||
const topicIds = useSelector(selectTopicsUnderCategory)(enableInContextSidebar ? groupedCategory : category);
|
||||
const posts = useSelector(enableInContextSidebar ? selectAllThreads : selectTopicThreads(topicIds));
|
||||
const posts = useSelector(selectTopicThreads(topicIds));
|
||||
return <PostsList posts={posts} topics={topicIds} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ describe('PostsView', () => {
|
||||
.toHaveLength(topicThreadCount);
|
||||
// When grouping is enabled, topic 1 will be shown, but not otherwise.
|
||||
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-1/i))
|
||||
.toHaveLength(grouping ? topicThreadCount : 2);
|
||||
.toHaveLength(grouping ? topicThreadCount : 0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -106,7 +108,7 @@ function PostEditor({
|
||||
const nonCoursewareIds = useSelector(enableInContext ? inContextCoursewareIds : selectNonCoursewareIds);
|
||||
const coursewareTopics = useSelector(enableInContext ? inContextCourseware : selectCoursewareTopics);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const post = useSelector(editExisting ? selectThread(postId) : () => ({}));
|
||||
const post = useSelector(selectThread(postId));
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const settings = useSelector(selectDivisionSettings);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,16 +141,6 @@ describe('PostEditor', () => {
|
||||
}
|
||||
},
|
||||
);
|
||||
test('selectThread is not called while creating a new post', async () => {
|
||||
const mockSelectThread = jest.fn();
|
||||
jest.mock('../data/selectors', () => ({
|
||||
selectThread: mockSelectThread,
|
||||
}));
|
||||
await renderComponent();
|
||||
expect(mockSelectThread)
|
||||
.not
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cohorting', () => {
|
||||
|
||||
@@ -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,
|
||||
@@ -90,9 +97,12 @@ function Post({
|
||||
return (
|
||||
<div
|
||||
className="d-flex flex-column w-100 mw-100 post-card-comment"
|
||||
aria-level={5}
|
||||
data-testid={`post-${post.id}`}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex="0"
|
||||
onMouseEnter={() => setShowHoverCard(true)}
|
||||
onMouseLeave={() => setShowHoverCard(false)}
|
||||
onFocus={() => setShowHoverCard(true)}
|
||||
onBlur={(e) => handleBlurEvent(e)}
|
||||
>
|
||||
<Confirmation
|
||||
isOpen={isDeleting}
|
||||
@@ -113,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' }}
|
||||
>
|
||||
@@ -138,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,6 +1,46 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "مواد المساق",
|
||||
"learn.course.tabs.navigation.overflow.menu": "المزيد...",
|
||||
"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 {تم إظهار # ردود}}",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num، plural, =0 {لا ردود معتمدة} one {تم إظهار ردّ واحد معتمد} two {تم إظهار ردّين معتمدين} few {تم إظهار # ردود معتمدة} many {تم إظهار # ردًا معتمدًا} other {تم إظهار # ردود معتمدة}}",
|
||||
"discussions.comments.comment.loadMoreComments": "تحميل المزيد من التعليقات",
|
||||
"discussions.comments.comment.loadMoreResponses": "تحميل المزيد من الردود",
|
||||
"discussions.comments.comment.visibility": "هذه المشاركة تظهر {group، select، null {للجميع} other {لـ {group}}.",
|
||||
"discussions.comments.comment.postedTime": "تم نشر {postType، select، discussion {المناقشة} question {المنشور} other {{postType}} {relativeTime} من طرف",
|
||||
"discussions.comments.comment.commentTime": "تم النشر {relativeTime}",
|
||||
"discussions.comments.comment.answer": "الإجابة",
|
||||
"discussions.comments.comment.answeredlabel": "تم تعليمها كمُجابة من طرف",
|
||||
"discussions.comments.comment.endorsed": "معتمد",
|
||||
"discussions.comments.comment.endorsedlabel": "اعتمده",
|
||||
"discussions.actions.label": "قائمة الإجراءات",
|
||||
"discussions.actions.edit": "تعديل",
|
||||
"discussions.actions.pin": "تثبيت",
|
||||
"discussions.actions.delete": "حذف",
|
||||
"discussions.editor.submit": "إرسال",
|
||||
"discussions.editor.submitting": "الإرسال جارٍ",
|
||||
"discussions.editor.cancel": "إلغاء",
|
||||
"discussions.editor.error.empty": "لا يمكن أن يكون محتوى المنشور فارغًا.",
|
||||
"discussions.editor.delete.response.title": "حذف الرد",
|
||||
"discussions.editor.delete.response.description": "هل أنت متأكد من رغبتك في حذف هذا الردّ نهائيًا؟",
|
||||
"discussions.editor.delete.comment.title": "حذف التعليق",
|
||||
"discussions.editor.delete.comment.description": "هل أنت متأكد من رغبتك في حذف هذا التعليق نهائيا؟",
|
||||
"discussions.delete.confirmation.button.delete": "حذف",
|
||||
"discussions.editor.response.response.title": "أتريد الإبلاغ عن محتوى غير لائق؟",
|
||||
"discussions.editor.response.description": "سيراجع فريق الإشراف على المناقشة هذا المحتوى و يتخذ الإجراء المناسب.",
|
||||
"discussions.editor.report.comment.title": "أتريد الإبلاغ عن محتوى غير لائق؟",
|
||||
"discussions.editor.report.comment.description": "سيراجع فريق الإشراف على المناقشة هذا المحتوى ويتخذ الإجراء المناسب.",
|
||||
"discussions.editor.comments.editReasonCode": "سبب التعديل",
|
||||
"discussions.editor.posts.editReasonCode.error": "حدد سبب التعديل",
|
||||
"discussions.comment.comments.editedBy": "عدّله",
|
||||
"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",
|
||||
"discussions.topics.discussions": "{count، plural, =0 {لا مناقشات} one {مناقشة واحدة} two {مناقشتان} few {# مناقشات} many {# مناقشة} other {# مناقشات}",
|
||||
"discussions.topics.questions": "{count، plural, =0 {لا مناقشات} one {سؤال واحد} two {سؤالان} few {# اسئلة} many {# سؤالًا} other {# أسئلة}",
|
||||
@@ -28,10 +68,7 @@
|
||||
"discussion.learner.posts": "المنشورات",
|
||||
"discussions.actions.button.alt": "قائمة الإجراءات",
|
||||
"discussions.actions.copylink": "نسخ الرابط",
|
||||
"discussions.actions.edit": "تعديل",
|
||||
"discussions.actions.pin": "تثبيت",
|
||||
"discussions.actions.unpin": "إلغاء التثبيت",
|
||||
"discussions.actions.delete": "حذف",
|
||||
"discussions.confirmation.button.confirm": "تأكيد",
|
||||
"discussions.actions.close": "إقفال ",
|
||||
"discussions.actions.reopen": "إعادة الفتح",
|
||||
@@ -72,44 +109,6 @@
|
||||
"discussions.navigation.navigationBar.allTopics": "المواضيع",
|
||||
"discussions.navigation.navigationBar.myPosts": "منشوراتي",
|
||||
"discussions.navigation.navigationBar.learners": "المتعلمون",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "إضافة رد",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "تم إبلاغ الطاقم عن هذا المحتوى لمراجعته.",
|
||||
"discussions.actions.back.alt": "العودة إلى القائمة",
|
||||
"discussions.comments.comment.responseCount": "{num، plural, =0 {دون رد} one {تم إظهار ردّ واحد} two {تم إظهار ردّين} few {تم إظهار # ردود} many {تم إظهار # ردًا} other {تم إظهار # ردود}}",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num، plural, =0 {لا ردود معتمدة} one {تم إظهار ردّ واحد معتمد} two {تم إظهار ردّين معتمدين} few {تم إظهار # ردود معتمدة} many {تم إظهار # ردًا معتمدًا} other {تم إظهار # ردود معتمدة}}",
|
||||
"discussions.comments.comment.loadMoreComments": "تحميل المزيد من التعليقات",
|
||||
"discussions.comments.comment.loadMoreResponses": "تحميل المزيد من الردود",
|
||||
"discussions.comments.comment.visibility": "هذه المشاركة تظهر {group، select، null {للجميع} other {لـ {group}}.",
|
||||
"discussions.comments.comment.postedTime": "تم نشر {postType، select، discussion {المناقشة} question {المنشور} other {{postType}} {relativeTime} من طرف",
|
||||
"discussions.comments.comment.commentTime": "تم النشر {relativeTime}",
|
||||
"discussions.comments.comment.answer": "الإجابة",
|
||||
"discussions.comments.comment.answeredlabel": "تم تعليمها كمُجابة من طرف",
|
||||
"discussions.comments.comment.endorsed": "معتمد",
|
||||
"discussions.comments.comment.endorsedlabel": "اعتمده",
|
||||
"discussions.actions.label": "قائمة الإجراءات",
|
||||
"discussions.editor.submit": "إرسال",
|
||||
"discussions.editor.submitting": "الإرسال جارٍ",
|
||||
"discussions.editor.cancel": "إلغاء",
|
||||
"discussions.editor.error.empty": "لا يمكن أن يكون محتوى المنشور فارغًا.",
|
||||
"discussions.editor.delete.response.title": "حذف الرد",
|
||||
"discussions.editor.delete.response.description": "هل أنت متأكد من رغبتك في حذف هذا الردّ نهائيًا؟",
|
||||
"discussions.editor.delete.comment.title": "حذف التعليق",
|
||||
"discussions.editor.delete.comment.description": "هل أنت متأكد من رغبتك في حذف هذا التعليق نهائيا؟",
|
||||
"discussions.delete.confirmation.button.delete": "حذف",
|
||||
"discussions.editor.response.response.title": "أتريد الإبلاغ عن محتوى غير لائق؟",
|
||||
"discussions.editor.response.description": "سيراجع فريق الإشراف على المناقشة هذا المحتوى و يتخذ الإجراء المناسب.",
|
||||
"discussions.editor.report.comment.title": "أتريد الإبلاغ عن محتوى غير لائق؟",
|
||||
"discussions.editor.report.comment.description": "سيراجع فريق الإشراف على المناقشة هذا المحتوى ويتخذ الإجراء المناسب.",
|
||||
"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.time": "منذ {time}",
|
||||
"discussion.thread.notFound": "المناقشة غير موجودة",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"discussions.app.title": "المناقشات",
|
||||
"discussions.posts.actionBar.searchAllPosts": "البحث في كافّة المنشورات",
|
||||
"discussions.posts.actionBar.search": "{page، select، topics {مواضيع البحث} posts {بحث في كل المشاركات} learners {بحث عن المتعلمين} myPosts} {بحث في كل المشاركات",
|
||||
@@ -142,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": "عرض الكل",
|
||||
@@ -164,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,212 +1,210 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Kursmaterial",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Mehr...",
|
||||
"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.edit": "Bearbeiten",
|
||||
"discussions.actions.pin": "Veröffentlichen",
|
||||
"discussions.actions.unpin": "Ablösen",
|
||||
"discussions.actions.delete": "Löschen",
|
||||
"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.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.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.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"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,18 +1,58 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Material del Curso",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Más...",
|
||||
"discussions.topics.backAlt": "Volver a la lista de temas",
|
||||
"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": "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",
|
||||
"discussions.comments.comment.loadMoreResponses": "Cargar más respuestas",
|
||||
"discussions.comments.comment.visibility": "Esta publicación es visible para {group, select, null {Everyone} other {{group}} }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select,\n discussion {Discussion}\n question {Question}\n other {{postType}}\n } publicado {relativeTime} por",
|
||||
"discussions.comments.comment.commentTime": "Publicado {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Respuesta",
|
||||
"discussions.comments.comment.answeredlabel": "Marcado como respondido por",
|
||||
"discussions.comments.comment.endorsed": "respaldado",
|
||||
"discussions.comments.comment.endorsedlabel": "Avalado por",
|
||||
"discussions.actions.label": "Menú de acciones",
|
||||
"discussions.actions.edit": "Editar",
|
||||
"discussions.actions.pin": "Marcar",
|
||||
"discussions.actions.delete": "Borrar",
|
||||
"discussions.editor.submit": "Enviar",
|
||||
"discussions.editor.submitting": "Enviando",
|
||||
"discussions.editor.cancel": "Cancelar",
|
||||
"discussions.editor.error.empty": "El contenido de la publicación no puede estar vacío.",
|
||||
"discussions.editor.delete.response.title": "Eliminar respuesta",
|
||||
"discussions.editor.delete.response.description": "¿Está seguro de que desea eliminar esta respuesta de forma permanente?",
|
||||
"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": "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.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": "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",
|
||||
@@ -22,26 +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.edit": "Editar",
|
||||
"discussions.actions.pin": "Marcar",
|
||||
"discussions.actions.unpin": "Desmarcar",
|
||||
"discussions.actions.delete": "Borrar",
|
||||
"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í.",
|
||||
@@ -62,54 +99,16 @@
|
||||
"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",
|
||||
"discussions.navigation.navigationBar.allTopics": "Temas",
|
||||
"discussions.navigation.navigationBar.myPosts": "Mis publicaciones",
|
||||
"discussions.navigation.navigationBar.learners": "Estudiantes",
|
||||
"discussions.comments.comment.addComment": "Añadir comentario",
|
||||
"discussions.comments.comment.addResponse": "Agregar una respuesta",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Contenido informado para que el personal lo revise",
|
||||
"discussions.actions.back.alt": "Volver a la lista",
|
||||
"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",
|
||||
"discussions.comments.comment.loadMoreResponses": "Cargar más respuestas",
|
||||
"discussions.comments.comment.visibility": "Esta publicación es visible para {group, select, null {Everyone} other {{group}} }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select,\n discussion {Discussion}\n question {Question}\n other {{postType}}\n } publicado {relativeTime} por",
|
||||
"discussions.comments.comment.commentTime": "Publicado {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Respuesta",
|
||||
"discussions.comments.comment.answeredlabel": "Marcado como respondido por",
|
||||
"discussions.comments.comment.endorsed": "respaldado",
|
||||
"discussions.comments.comment.endorsedlabel": "Avalado por",
|
||||
"discussions.actions.label": "Menú de acciones",
|
||||
"discussions.editor.submit": "Enviar",
|
||||
"discussions.editor.submitting": "Enviando",
|
||||
"discussions.editor.cancel": "Cancelar",
|
||||
"discussions.editor.error.empty": "El contenido de la publicación no puede estar vacío.",
|
||||
"discussions.editor.delete.response.title": "Eliminar respuesta",
|
||||
"discussions.editor.delete.response.description": "¿Está seguro de que desea eliminar esta respuesta de forma permanente?",
|
||||
"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.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.time": "hace {time}",
|
||||
"discussion.thread.notFound": "Hilo no encontrado",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"discussions.app.title": "Debates",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Buscar en todas las publicaciones",
|
||||
"discussions.posts.actionBar.search": "{page, select, topics {Search topics} posts {Search all posts} learners {Search learners} myPosts {Search all posts} other {{page}} }",
|
||||
@@ -143,8 +142,8 @@
|
||||
"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": "Mostrar vista previa",
|
||||
"discussions.topic.noName.label": "Categoría sin nombre",
|
||||
"discussions.subtopic.noName.label": "Subcategoría sin nombre",
|
||||
"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",
|
||||
@@ -154,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",
|
||||
@@ -162,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": "Añadir respuesta",
|
||||
"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",
|
||||
@@ -196,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,6 +1,46 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Course Material",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Plus...",
|
||||
"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 }",
|
||||
"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": "Charger plus de commentaires",
|
||||
"discussions.comments.comment.loadMoreResponses": "Charger plus de réponses",
|
||||
"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": "Réponse",
|
||||
"discussions.comments.comment.answeredlabel": "Marqué comme répondu par",
|
||||
"discussions.comments.comment.endorsed": "Approuvé",
|
||||
"discussions.comments.comment.endorsedlabel": "Approuvé par",
|
||||
"discussions.actions.label": "Menu Actions",
|
||||
"discussions.actions.edit": "Modifier",
|
||||
"discussions.actions.pin": "Épingler",
|
||||
"discussions.actions.delete": "Delete",
|
||||
"discussions.editor.submit": "Submit",
|
||||
"discussions.editor.submitting": "Submitting",
|
||||
"discussions.editor.cancel": "Annuler",
|
||||
"discussions.editor.error.empty": "Le contenu de la publication ne peut pas être vide.",
|
||||
"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": "Édité par",
|
||||
"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",
|
||||
"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 }",
|
||||
@@ -28,10 +68,7 @@
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.actions.button.alt": "Actions menu",
|
||||
"discussions.actions.copylink": "Copy link",
|
||||
"discussions.actions.edit": "Modifier",
|
||||
"discussions.actions.pin": "Épingler",
|
||||
"discussions.actions.unpin": "Unpin",
|
||||
"discussions.actions.delete": "Delete",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Close",
|
||||
"discussions.actions.reopen": "Reopen",
|
||||
@@ -72,44 +109,6 @@
|
||||
"discussions.navigation.navigationBar.allTopics": "Topics",
|
||||
"discussions.navigation.navigationBar.myPosts": "My posts",
|
||||
"discussions.navigation.navigationBar.learners": "Learners",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Ajouter une réponse",
|
||||
"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 }",
|
||||
"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": "Charger plus de commentaires",
|
||||
"discussions.comments.comment.loadMoreResponses": "Charger plus de réponses",
|
||||
"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": "Réponse",
|
||||
"discussions.comments.comment.answeredlabel": "Marqué comme répondu par",
|
||||
"discussions.comments.comment.endorsed": "Approuvé",
|
||||
"discussions.comments.comment.endorsedlabel": "Approuvé par",
|
||||
"discussions.actions.label": "Menu Actions",
|
||||
"discussions.editor.submit": "Submit",
|
||||
"discussions.editor.submitting": "Submitting",
|
||||
"discussions.editor.cancel": "Annuler",
|
||||
"discussions.editor.error.empty": "Le contenu de la publication ne peut pas être vide.",
|
||||
"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": "Édité par",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Motif",
|
||||
"discussions.post.closedBy": "Message fermé par",
|
||||
"discussion.comment.time": "il y a {time}",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"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 }",
|
||||
@@ -142,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",
|
||||
@@ -164,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,6 +1,46 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Matériel de cours",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Plus...",
|
||||
"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 }",
|
||||
"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": "Charger plus de commentaires",
|
||||
"discussions.comments.comment.loadMoreResponses": "Charger plus de réponses",
|
||||
"discussions.comments.comment.visibility": "Ce message est visible par {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": "Publié {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Réponse",
|
||||
"discussions.comments.comment.answeredlabel": "Marqué comme répondu par",
|
||||
"discussions.comments.comment.endorsed": "Approuvé",
|
||||
"discussions.comments.comment.endorsedlabel": "Approuvé par",
|
||||
"discussions.actions.label": "Menu Actions",
|
||||
"discussions.actions.edit": "Éditer",
|
||||
"discussions.actions.pin": "Épingler",
|
||||
"discussions.actions.delete": "Supprimer",
|
||||
"discussions.editor.submit": "Soumettre",
|
||||
"discussions.editor.submitting": "Soumission",
|
||||
"discussions.editor.cancel": "Annuler",
|
||||
"discussions.editor.error.empty": "Le contenu de la publication ne peut pas être vide.",
|
||||
"discussions.editor.delete.response.title": "Supprimer la réponse",
|
||||
"discussions.editor.delete.response.description": "Êtes-vous sûr de vouloir supprimer définitivement cette réponse?",
|
||||
"discussions.editor.delete.comment.title": "Supprimer le commentaire",
|
||||
"discussions.editor.delete.comment.description": "Êtes-vous sûr de vouloir supprimer définitivement ce commentaire?",
|
||||
"discussions.delete.confirmation.button.delete": "Supprimer",
|
||||
"discussions.editor.response.response.title": "Signaler un contenu inapproprié?",
|
||||
"discussions.editor.response.description": "L'équipe de modération de la discussion examinera ce contenu et prendra les mesures appropriées.",
|
||||
"discussions.editor.report.comment.title": "Signaler un contenu inapproprié?",
|
||||
"discussions.editor.report.comment.description": "L'équipe de modération de la discussion examinera ce contenu et prendra les mesures appropriées.",
|
||||
"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.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",
|
||||
"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 }",
|
||||
@@ -28,10 +68,7 @@
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.actions.button.alt": "Menu Actions",
|
||||
"discussions.actions.copylink": "Copier le lien",
|
||||
"discussions.actions.edit": "Éditer",
|
||||
"discussions.actions.pin": "Épingler",
|
||||
"discussions.actions.unpin": "Détacher",
|
||||
"discussions.actions.delete": "Supprimer",
|
||||
"discussions.confirmation.button.confirm": "Confirmer",
|
||||
"discussions.actions.close": "Fermer",
|
||||
"discussions.actions.reopen": "Rouvrir",
|
||||
@@ -72,44 +109,6 @@
|
||||
"discussions.navigation.navigationBar.allTopics": "Sujets",
|
||||
"discussions.navigation.navigationBar.myPosts": "Mes messages",
|
||||
"discussions.navigation.navigationBar.learners": "Apprenants",
|
||||
"discussions.comments.comment.addComment": "Ajouter un commentaire",
|
||||
"discussions.comments.comment.addResponse": "Ajouter une réponse",
|
||||
"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 }",
|
||||
"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": "Charger plus de commentaires",
|
||||
"discussions.comments.comment.loadMoreResponses": "Charger plus de réponses",
|
||||
"discussions.comments.comment.visibility": "Ce message est visible par {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": "Publié {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Réponse",
|
||||
"discussions.comments.comment.answeredlabel": "Marqué comme répondu par",
|
||||
"discussions.comments.comment.endorsed": "Approuvé",
|
||||
"discussions.comments.comment.endorsedlabel": "Approuvé par",
|
||||
"discussions.actions.label": "Menu Actions",
|
||||
"discussions.editor.submit": "Soumettre",
|
||||
"discussions.editor.submitting": "Soumission",
|
||||
"discussions.editor.cancel": "Annuler",
|
||||
"discussions.editor.error.empty": "Le contenu de la publication ne peut pas être vide.",
|
||||
"discussions.editor.delete.response.title": "Supprimer la réponse",
|
||||
"discussions.editor.delete.response.description": "Êtes-vous sûr de vouloir supprimer définitivement cette réponse?",
|
||||
"discussions.editor.delete.comment.title": "Supprimer le commentaire",
|
||||
"discussions.editor.delete.comment.description": "Êtes-vous sûr de vouloir supprimer définitivement ce commentaire?",
|
||||
"discussions.delete.confirmation.button.delete": "Supprimer",
|
||||
"discussions.editor.response.response.title": "Signaler un contenu inapproprié?",
|
||||
"discussions.editor.response.description": "L'équipe de modération de la discussion examinera ce contenu et prendra les mesures appropriées.",
|
||||
"discussions.editor.report.comment.title": "Signaler un contenu inapproprié?",
|
||||
"discussions.editor.report.comment.description": "L'équipe de modération de la discussion examinera ce contenu et prendra les mesures appropriées.",
|
||||
"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.time": "il y a {time}",
|
||||
"discussion.thread.notFound": "Sujet introuvable",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"discussions.app.title": "Discussions",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Recherche dans les messages",
|
||||
"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 }",
|
||||
@@ -164,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,6 +1,46 @@
|
||||
{
|
||||
"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 }",
|
||||
@@ -28,10 +68,7 @@
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.actions.button.alt": "Actions menu",
|
||||
"discussions.actions.copylink": "Copy link",
|
||||
"discussions.actions.edit": "Edit",
|
||||
"discussions.actions.pin": "Pin",
|
||||
"discussions.actions.unpin": "Unpin",
|
||||
"discussions.actions.delete": "Delete",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Close",
|
||||
"discussions.actions.reopen": "Reopen",
|
||||
@@ -72,44 +109,6 @@
|
||||
"discussions.navigation.navigationBar.allTopics": "Topics",
|
||||
"discussions.navigation.navigationBar.myPosts": "My posts",
|
||||
"discussions.navigation.navigationBar.learners": "Learners",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Add a response",
|
||||
"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.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.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Reason",
|
||||
"discussions.post.closedBy": "Post closed by",
|
||||
"discussion.comment.time": "{time} ago",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"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 }",
|
||||
@@ -142,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",
|
||||
@@ -164,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,6 +1,46 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Materiale del corso",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Altro... ",
|
||||
"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} }",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural, =0 {Nessuna risposta approvata} one {Mostra # risposta approvata} other {Mostra # risposte approvate} }",
|
||||
"discussions.comments.comment.loadMoreComments": "Carica più commenti",
|
||||
"discussions.comments.comment.loadMoreResponses": "Carica più risposte",
|
||||
"discussions.comments.comment.visibility": "Questo post è visibile a {group, select, null {Everyone} other {{group}} }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select, discussion {Discussione} question {Domanda} other {{postType}} } pubblicato da {a0917e90}14c5z0",
|
||||
"discussions.comments.comment.commentTime": "Inserito {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Risposta",
|
||||
"discussions.comments.comment.answeredlabel": "Contrassegnato come risposta da",
|
||||
"discussions.comments.comment.endorsed": "Approvato",
|
||||
"discussions.comments.comment.endorsedlabel": "Approvato dal",
|
||||
"discussions.actions.label": "Menù Azioni",
|
||||
"discussions.actions.edit": "Modifica",
|
||||
"discussions.actions.pin": "Blocca",
|
||||
"discussions.actions.delete": "Cancella",
|
||||
"discussions.editor.submit": "Invia",
|
||||
"discussions.editor.submitting": "In fase di invio",
|
||||
"discussions.editor.cancel": "Annulla",
|
||||
"discussions.editor.error.empty": "Il contenuto del post non può essere vuoto.",
|
||||
"discussions.editor.delete.response.title": "Elimina risposta",
|
||||
"discussions.editor.delete.response.description": "Sei sicuro di voler eliminare definitivamente questa risposta?",
|
||||
"discussions.editor.delete.comment.title": "Elimina commento",
|
||||
"discussions.editor.delete.comment.description": "Sei sicuro di voler eliminare definitivamente questo commento?",
|
||||
"discussions.delete.confirmation.button.delete": "Cancella",
|
||||
"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": "Motivo della modifica",
|
||||
"discussions.editor.posts.editReasonCode.error": "Seleziona il motivo per la modifica",
|
||||
"discussions.comment.comments.editedBy": "A cura di",
|
||||
"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",
|
||||
"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 }",
|
||||
@@ -28,10 +68,7 @@
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.actions.button.alt": "Menù Azioni",
|
||||
"discussions.actions.copylink": "Copia link",
|
||||
"discussions.actions.edit": "Modifica",
|
||||
"discussions.actions.pin": "Blocca",
|
||||
"discussions.actions.unpin": "Sblocca ",
|
||||
"discussions.actions.delete": "Cancella",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Chiudi",
|
||||
"discussions.actions.reopen": "Riaprire",
|
||||
@@ -72,44 +109,6 @@
|
||||
"discussions.navigation.navigationBar.allTopics": "Argomenti",
|
||||
"discussions.navigation.navigationBar.myPosts": "I miei post",
|
||||
"discussions.navigation.navigationBar.learners": "Utenti",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Aggiungi una risposta",
|
||||
"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} }",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural, =0 {Nessuna risposta approvata} one {Mostra # risposta approvata} other {Mostra # risposte approvate} }",
|
||||
"discussions.comments.comment.loadMoreComments": "Carica più commenti",
|
||||
"discussions.comments.comment.loadMoreResponses": "Carica più risposte",
|
||||
"discussions.comments.comment.visibility": "Questo post è visibile a {group, select, null {Everyone} other {{group}} }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select, discussion {Discussione} question {Domanda} other {{postType}} } pubblicato da {a0917e90}14c5z0",
|
||||
"discussions.comments.comment.commentTime": "Inserito {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Risposta",
|
||||
"discussions.comments.comment.answeredlabel": "Contrassegnato come risposta da",
|
||||
"discussions.comments.comment.endorsed": "Approvato",
|
||||
"discussions.comments.comment.endorsedlabel": "Approvato dal",
|
||||
"discussions.actions.label": "Menù Azioni",
|
||||
"discussions.editor.submit": "Invia",
|
||||
"discussions.editor.submitting": "In fase di invio",
|
||||
"discussions.editor.cancel": "Annulla",
|
||||
"discussions.editor.error.empty": "Il contenuto del post non può essere vuoto.",
|
||||
"discussions.editor.delete.response.title": "Elimina risposta",
|
||||
"discussions.editor.delete.response.description": "Sei sicuro di voler eliminare definitivamente questa risposta?",
|
||||
"discussions.editor.delete.comment.title": "Elimina commento",
|
||||
"discussions.editor.delete.comment.description": "Sei sicuro di voler eliminare definitivamente questo commento?",
|
||||
"discussions.delete.confirmation.button.delete": "Cancella",
|
||||
"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": "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.time": "{time} fa",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"discussions.app.title": "Discussioni",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Cerca tutti i messaggi",
|
||||
"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 }",
|
||||
@@ -142,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",
|
||||
@@ -164,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,6 +1,46 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Course Material",
|
||||
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||
"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}",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural,\n=0 {No endorsed responses}\none {Showing # endorsed response}\nother {Showing # endorsed responses}\n}",
|
||||
"discussions.comments.comment.loadMoreComments": "Załaduj więcej komentarzy",
|
||||
"discussions.comments.comment.loadMoreResponses": "Załaduj więcej odpowiedzi",
|
||||
"discussions.comments.comment.visibility": "Ten post jest widoczny dla {group, select,\n null {Everyone}\nother {{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": "Wysłano {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Answer",
|
||||
"discussions.comments.comment.answeredlabel": "Oznaczono jako odpowiedziane przez",
|
||||
"discussions.comments.comment.endorsed": "Zatwierdzony",
|
||||
"discussions.comments.comment.endorsedlabel": "Zatwierdzony przez",
|
||||
"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": "Usuń odpowiedź",
|
||||
"discussions.editor.delete.response.description": "Czy na pewno chcesz trwale usunąć tę odpowiedź?",
|
||||
"discussions.editor.delete.comment.title": "Usuń komentarz",
|
||||
"discussions.editor.delete.comment.description": "Czy na pewno chcesz trwale usunąć ten komentarz?",
|
||||
"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": "Wybierz powód edycji",
|
||||
"discussions.comment.comments.editedBy": "Edytowany przez",
|
||||
"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",
|
||||
"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 }",
|
||||
@@ -28,10 +68,7 @@
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.actions.button.alt": "Menu czynności",
|
||||
"discussions.actions.copylink": "Copy link",
|
||||
"discussions.actions.edit": "Edit",
|
||||
"discussions.actions.pin": "Pin",
|
||||
"discussions.actions.unpin": "Unpin",
|
||||
"discussions.actions.delete": "Delete",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Close",
|
||||
"discussions.actions.reopen": "Otwórz ponownie",
|
||||
@@ -72,44 +109,6 @@
|
||||
"discussions.navigation.navigationBar.allTopics": "Topics",
|
||||
"discussions.navigation.navigationBar.myPosts": "Moje posty",
|
||||
"discussions.navigation.navigationBar.learners": "Learners",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Dodaj odpowiedź",
|
||||
"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}",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural,\n=0 {No endorsed responses}\none {Showing # endorsed response}\nother {Showing # endorsed responses}\n}",
|
||||
"discussions.comments.comment.loadMoreComments": "Załaduj więcej komentarzy",
|
||||
"discussions.comments.comment.loadMoreResponses": "Załaduj więcej odpowiedzi",
|
||||
"discussions.comments.comment.visibility": "Ten post jest widoczny dla {group, select,\n null {Everyone}\nother {{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": "Wysłano {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Answer",
|
||||
"discussions.comments.comment.answeredlabel": "Oznaczono jako odpowiedziane przez",
|
||||
"discussions.comments.comment.endorsed": "Zatwierdzony",
|
||||
"discussions.comments.comment.endorsedlabel": "Zatwierdzony przez",
|
||||
"discussions.actions.label": "Actions menu",
|
||||
"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": "Usuń odpowiedź",
|
||||
"discussions.editor.delete.response.description": "Czy na pewno chcesz trwale usunąć tę odpowiedź?",
|
||||
"discussions.editor.delete.comment.title": "Usuń komentarz",
|
||||
"discussions.editor.delete.comment.description": "Czy na pewno chcesz trwale usunąć ten komentarz?",
|
||||
"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": "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.time": "{time} ago",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"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 }",
|
||||
@@ -142,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",
|
||||
@@ -164,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,18 +1,58 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Ders Materyali",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Daha Fazlası...",
|
||||
"discussions.topics.backAlt": "Konular listesine dön",
|
||||
"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 }",
|
||||
"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": "Daha fazla yorum yükle",
|
||||
"discussions.comments.comment.loadMoreResponses": "Daha fazla yanıt yükle",
|
||||
"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": "{relativeTime} önce gönderildi",
|
||||
"discussions.comments.comment.answer": "Cevap",
|
||||
"discussions.comments.comment.answeredlabel": "Yanıtlandı olarak işaretleyen ",
|
||||
"discussions.comments.comment.endorsed": "Doğrulandı",
|
||||
"discussions.comments.comment.endorsedlabel": "Doğrulayan",
|
||||
"discussions.actions.label": "Eylemler menüsü",
|
||||
"discussions.actions.edit": "Düzenle",
|
||||
"discussions.actions.pin": "İşaretle",
|
||||
"discussions.actions.delete": "Sil",
|
||||
"discussions.editor.submit": "Gönder",
|
||||
"discussions.editor.submitting": "Gönderiliyor",
|
||||
"discussions.editor.cancel": "İptal",
|
||||
"discussions.editor.error.empty": "Gönderi içeriği boş olamaz.",
|
||||
"discussions.editor.delete.response.title": "Yanıtı sil",
|
||||
"discussions.editor.delete.response.description": "Bu yanıtı kalıcı olarak silmek istediğinizden emin misiniz?",
|
||||
"discussions.editor.delete.comment.title": "Yorumu sil",
|
||||
"discussions.editor.delete.comment.description": "Bu yorumu kalıcı olarak silmek istediğinizden emin misiniz?",
|
||||
"discussions.delete.confirmation.button.delete": "Sil",
|
||||
"discussions.editor.response.response.title": "Uygunsuz içerik mi raporlayacaksınız?",
|
||||
"discussions.editor.response.description": "Tartışma yöneticileri bu içeriği inceleyecek ve uygun işlemi yapacaktır.",
|
||||
"discussions.editor.report.comment.title": "Uygunsuz içerik mi raporlayacaksınız?",
|
||||
"discussions.editor.report.comment.description": "Tartışma yöneticileri bu içeriği inceleyecek ve uygun işlemi yapacaktır.",
|
||||
"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.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",
|
||||
"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} rapor edildi",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} ileti rapor edildi",
|
||||
"discussions.topics.find.label": "Konuları ara",
|
||||
"discussions.topics.unnamed.section.label": "İsimsiz Bölüm",
|
||||
"discussions.topics.unnamed.subsection.label": "İsimsiz Altbölüm",
|
||||
"discussions.subtopics.unnamed.topic.label": "İsimsiz Konu",
|
||||
"discussions.topics.title": "Hiçbir konu yok",
|
||||
"discussions.topics.createTopic": "Bir konu başlatmak için yöneticinizle iletişime geçin",
|
||||
"discussions.topics.nothing": "Burada henüz bir şey yok",
|
||||
"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": "Arşivlenmiş",
|
||||
"discussions.learner.reported": "{reported} rapor edildi",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} daha önce rapor edildi",
|
||||
@@ -28,17 +68,14 @@
|
||||
"discussion.learner.posts": "Gönderiler",
|
||||
"discussions.actions.button.alt": "Eylemler menüsü",
|
||||
"discussions.actions.copylink": "Bağlantıyı kopyala",
|
||||
"discussions.actions.edit": "Düzenle",
|
||||
"discussions.actions.pin": "İşaretle",
|
||||
"discussions.actions.unpin": "İşareti kaldır",
|
||||
"discussions.actions.delete": "Sil",
|
||||
"discussions.confirmation.button.confirm": "Onayla",
|
||||
"discussions.actions.close": "Kapat",
|
||||
"discussions.actions.reopen": "Yeniden aç",
|
||||
"discussions.actions.report": "Raporla",
|
||||
"discussions.actions.unreport": "Raporlamaktan vazgeç",
|
||||
"discussions.actions.unreport": "Bildirme",
|
||||
"discussions.actions.endorse": "Destekle",
|
||||
"discussions.actions.unendorse": "Desteklemekten vazgeç",
|
||||
"discussions.actions.unendorse": "Destekleme",
|
||||
"discussions.actions.markAnswered": "Cevaplandı olarak işaretle",
|
||||
"discussions.actions.unMarkAnswered": "Cevaplandı olarak işaretini kaldır",
|
||||
"discussions.modal.confirmation.button.cancel": "İptal",
|
||||
@@ -62,7 +99,7 @@
|
||||
"discussion.banner.welcomeMessage": "🎉 Yeni ve geliştirilmiş tartışma deneyimine hoş geldiniz!",
|
||||
"discussion.banner.learnMore": "Daha fazlasını öğren",
|
||||
"discussion.banner.shareFeedback": "Geri bildirim paylaş",
|
||||
"discussion.blackoutBanner.information": "Tartışmalarda ileti yayınlama, ders ekibi tarafından geçici olarak devre dışı bırakıldı",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is temporarily disabled by the course team",
|
||||
"discussions.editor.image.warning.message": "Genişliği veya yüksekliği 999 pikselden büyük olan resimler, çevrimiçi ders tartışmalarında yer alan gönderi, yanıt veya yorumlarda görüntülenemez.",
|
||||
"discussions.editor.image.warning.title": "Uyarı!",
|
||||
"discussions.editor.image.warning.dismiss": "Tamam",
|
||||
@@ -72,44 +109,6 @@
|
||||
"discussions.navigation.navigationBar.allTopics": "Konular",
|
||||
"discussions.navigation.navigationBar.myPosts": "İletilerim",
|
||||
"discussions.navigation.navigationBar.learners": "Öğrenciler",
|
||||
"discussions.comments.comment.addComment": "Yorum ekle",
|
||||
"discussions.comments.comment.addResponse": "Bir cevap 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 }",
|
||||
"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": "Daha fazla yorum yükle",
|
||||
"discussions.comments.comment.loadMoreResponses": "Daha fazla yanıt yükle",
|
||||
"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": "{relativeTime} önce gönderildi",
|
||||
"discussions.comments.comment.answer": "Cevap",
|
||||
"discussions.comments.comment.answeredlabel": "Yanıtlandı olarak işaretleyen ",
|
||||
"discussions.comments.comment.endorsed": "Doğrulandı",
|
||||
"discussions.comments.comment.endorsedlabel": "Doğrulayan",
|
||||
"discussions.actions.label": "Eylemler menüsü",
|
||||
"discussions.editor.submit": "Gönder",
|
||||
"discussions.editor.submitting": "Gönderiliyor",
|
||||
"discussions.editor.cancel": "İptal",
|
||||
"discussions.editor.error.empty": "Gönderi içeriği boş olamaz.",
|
||||
"discussions.editor.delete.response.title": "Yanıtı sil",
|
||||
"discussions.editor.delete.response.description": "Bu yanıtı kalıcı olarak silmek istediğinizden emin misiniz?",
|
||||
"discussions.editor.delete.comment.title": "Yorumu sil",
|
||||
"discussions.editor.delete.comment.description": "Bu yorumu kalıcı olarak silmek istediğinizden emin misiniz?",
|
||||
"discussions.delete.confirmation.button.delete": "Sil",
|
||||
"discussions.editor.response.response.title": "Uygunsuz içerik mi raporlayacaksınız?",
|
||||
"discussions.editor.response.description": "Tartışma yöneticileri bu içeriği inceleyecek ve uygun işlemi yapacaktır.",
|
||||
"discussions.editor.report.comment.title": "Uygunsuz içerik mi raporlayacaksınız?",
|
||||
"discussions.editor.report.comment.description": "Tartışma yöneticileri bu içeriği inceleyecek ve uygun işlemi yapacaktır.",
|
||||
"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.time": "{time} önce",
|
||||
"discussion.thread.notFound": "Tartışma zinciri bulunamadı",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"discussions.app.title": "Forumlar",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Tüm gönderilerde ara",
|
||||
"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 }",
|
||||
@@ -142,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": "Önizlemeyi göster",
|
||||
"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",
|
||||
@@ -164,22 +163,21 @@
|
||||
"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": "Yanıt ekle",
|
||||
"discussions.post.lastResponse": "Son yanıt {time}",
|
||||
"discussions.post.postedOn": "{author} {authorLabel} tarafından {time} önce gönderildi",
|
||||
"discussions.post.contentReported": "Rapor edildi",
|
||||
"discussions.post.following": "Takip ediliyor",
|
||||
"discussions.post.follow": "Takip et",
|
||||
"discussions.post.followed": "İzlendi",
|
||||
"discussions.post.notFollowed": "İzlenmedi",
|
||||
"discussions.post.followed": "Followed",
|
||||
"discussions.post.notFollowed": "Not Followed",
|
||||
"discussions.post.answered": "Yanıtlandı",
|
||||
"discussions.post.unFollow": "Takibi bırak",
|
||||
"discussions.post.like": "Beğen",
|
||||
"discussions.post.removeLike": "Beğenmeme",
|
||||
"discussions.post.liked": "beğendi",
|
||||
"discussions.post.likes": "beğeni",
|
||||
"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",
|
||||
@@ -196,17 +194,17 @@
|
||||
"discussions.post.editedBy": "Düzenleyen",
|
||||
"discussions.post.editReason": "Gerekçe",
|
||||
"discussions.post.postWithoutPreview": "Önizleme yok",
|
||||
"discussions.post.follow.description": "bu iletiyi izliyorsunuz",
|
||||
"discussions.post.unfollow.description": "bu iletiyi izlemiyorsunuz",
|
||||
"discussions.post.follow.description": "you are following this post",
|
||||
"discussions.post.unfollow.description": "you are not following this post",
|
||||
"discussions.topics.sort.message": "{sortBy} ölçütüne göre sıralandı",
|
||||
"discussions.topics.sort.lastActivity": "Son etkinlik",
|
||||
"discussions.topics.sort.commentCount": "En çok etkinlik",
|
||||
"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.dismiss": "İptal",
|
||||
"tour.action.end": "Tamam",
|
||||
"tour.body.notRespondedFilter": "Artık yanıt vermeyen iletileri bulmak için tartışmaları filtreleyebilirsiniz.",
|
||||
"tour.title.notRespondedFilter": "Yeni filtreleme seçeneği!"
|
||||
"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,6 +1,46 @@
|
||||
{
|
||||
"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 }",
|
||||
@@ -28,10 +68,7 @@
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussions.actions.button.alt": "Actions menu",
|
||||
"discussions.actions.copylink": "Copy link",
|
||||
"discussions.actions.edit": "Edit",
|
||||
"discussions.actions.pin": "Pin",
|
||||
"discussions.actions.unpin": "Unpin",
|
||||
"discussions.actions.delete": "Delete",
|
||||
"discussions.confirmation.button.confirm": "Confirm",
|
||||
"discussions.actions.close": "Close",
|
||||
"discussions.actions.reopen": "Reopen",
|
||||
@@ -72,44 +109,6 @@
|
||||
"discussions.navigation.navigationBar.allTopics": "Topics",
|
||||
"discussions.navigation.navigationBar.myPosts": "My posts",
|
||||
"discussions.navigation.navigationBar.learners": "Learners",
|
||||
"discussions.comments.comment.addComment": "Add comment",
|
||||
"discussions.comments.comment.addResponse": "Add a response",
|
||||
"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.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.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Reason",
|
||||
"discussions.post.closedBy": "Post closed by",
|
||||
"discussion.comment.time": "{time} ago",
|
||||
"discussion.thread.notFound": "Thread not found",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Oldest first}\n true {Newest first}\n other {{sort}}\n }",
|
||||
"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 }",
|
||||
@@ -142,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",
|
||||
@@ -164,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 {
|
||||
@@ -433,7 +409,7 @@ header {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.on-focus:focus-within {
|
||||
.on-focus:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
@@ -441,21 +417,10 @@ header {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.post-card-comment {
|
||||
outline: none;
|
||||
|
||||
&:not(:hover),
|
||||
&:not(:focus) {
|
||||
.hover-card {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.hover-card {
|
||||
display: flex;
|
||||
}
|
||||
.post-card-comment:hover,
|
||||
.post-card-comment:focus {
|
||||
.hover-card {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,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