Compare commits

...

23 Commits

Author SHA1 Message Date
SaadYousaf
f3226c729b feat: send enableInContextSidebar param to backend to identify source of content for events 2023-03-08 16:42:11 +05:00
sundasnoreen12
627390c4e3 test: added testcases of redux, selector and api (#459)
* test: added testcases of redux, selector and api

* refactor: fixe recommanded issue and improve code cov

* refactor: added cases for filter statuses

* refactor: updated test description

* refactor: add common method of mock data for learner and post

* refactor: code and moved test utils in learners folder

---------

Co-authored-by: sundasnoreen12 <sundasnoreen12@ggmail.com>
Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
2023-03-08 14:25:54 +05:00
Muhammad Adeel Tajamul
1db94718c8 fix: more actions dropdown was not visible (#461) 2023-03-07 06:22:31 +05:00
ayesha waris
24d02350a8 fix: fix topic info for course-wide discussion topics (#458)
* fix: fix topic info for course-wide discussion topics

* refactor: removed const and used url directly

* test: adds test cases for topic info

* test: updated test cases
2023-03-06 21:10:49 +05:00
Jenkins
f66cdda1b6 chore(i18n): update translations 2023-03-05 15:26:48 -05:00
ayesha waris
07b56e6070 style: add border on focused post (#460) 2023-03-03 16:14:52 +05:00
ayesha waris
be1a2ccaab fix: fixed post coment actions menu accessibilty for keyboard (#456) 2023-03-01 21:46:23 +05:00
Sarina Canelake
ed0c73e051 Merge pull request #454 from openedx/repo_checks/ensure_workflows
Update standard workflow files.
2023-02-28 09:40:08 -05:00
Feanil Patel
1041b3e45f build: Updating a missing workflow file add-depr-ticket-to-depr-board.yml.
The .github/workflows/add-depr-ticket-to-depr-board.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 09:34:03 -05:00
Feanil Patel
493a0610ca build: Creating a missing workflow file add-remove-label-on-comment.yml.
The .github/workflows/add-remove-label-on-comment.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 09:34:03 -05:00
Feanil Patel
679e21c270 build: Creating a missing workflow file self-assign-issue.yml.
The .github/workflows/self-assign-issue.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 09:34:03 -05:00
sundasnoreen12
62eb9f5e02 test: Added test cases for noncourseware and courseware topic posts (#452)
* test: Added test cases for noncourseware and courseware topic posts

* refactor: optimized code for post view list

* refactor: updated selector tag

---------

Co-authored-by: sundasnoreen12 <sundasnoreen12@ggmail.com>
2023-02-24 12:39:46 +05:00
Muhammad Adeel Tajamul
dedbc25358 fix: incontext crashing (#453)
Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
2023-02-23 19:05:39 +05:00
Mehak Nasir
0f2ad8b7b4 fix: conditionally skipped some API calls and deffered script loading to improve performance 2023-02-23 14:26:02 +05:00
Ahtisham Shahid
61581ff474 fix: resolved data retention issue in add a post form (#451)
* fix: resolved data retention issue in adding a post form

* test: added unit test for post editor
2023-02-22 22:58:23 +05:00
Ahtisham Shahid
3afce17a32 feat: added event tracking on load more response (#442)
* feat: added event tracking on load more response
2023-02-21 16:30:30 +05:00
Muhammad Adeel Tajamul
7e36e9f14c fix: post loading slow (#447)
Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
2023-02-21 15:09:44 +05:00
sundasnoreen12
c662310b08 test: added test cases for v3 Topics and units list page (#440)
* test: added  test cases for v3 Topics and units list page

* refactor: v3 topics unit test cases

* refactor: v3 topics unit test cases

* refactor: v3 topics unit test cases

* refactor: removed my added commented line and also optimized the code to get stats for section

---------

Co-authored-by: sundasnoreen12 <sundasnoreen12@ggmail.com>
2023-02-21 12:45:08 +05:00
Muhammad Adeel Tajamul
682a118a9b fix: reduced padding for topic names (#443)
Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
2023-02-21 11:36:05 +05:00
Awais Ansari
d34d0ebbbc feat: hide the comments sort feature (#441)
* feat: hide the comments sort feature

* test: temporarily comment the commnets/responses test cases

* refactor: remove comments sort commented test cases
2023-02-20 16:07:58 +05:00
Awais Ansari
6afb7c7763 temp: testing getFeedback widget response time (#444) 2023-02-20 14:47:07 +05:00
Jenkins
7dca99dfe3 chore(i18n): update translations 2023-02-19 15:26:47 -05:00
Muhammad Adeel Tajamul
137795f254 feat: added stats at subsection level in topics tab (#437)
Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
2023-02-17 08:29:06 +05:00
54 changed files with 1831 additions and 690 deletions

View File

@@ -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 }}

View File

@@ -0,0 +1,20 @@
# 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 Normal file
View File

@@ -0,0 +1,12 @@
# 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

View File

@@ -13,41 +13,178 @@
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 async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script>
<script
defer
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>

View File

@@ -13,12 +13,12 @@ const defaultSanitizeOptions = {
};
function HTMLLoader({
htmlNode, componentId, cssClassName, testId,
htmlNode, componentId, cssClassName, testId, delay,
}) {
const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions });
const previewRef = useRef();
const debouncedPostContent = useDebounce(htmlNode, 500);
const debouncedPostContent = useDebounce(htmlNode, delay);
useEffect(() => {
let promise = Promise.resolve(); // Used to hold chain of typesetting calls
@@ -45,6 +45,7 @@ HTMLLoader.propTypes = {
componentId: PropTypes.string,
cssClassName: PropTypes.string,
testId: PropTypes.string,
delay: PropTypes.number,
};
HTMLLoader.defaultProps = {
@@ -52,6 +53,7 @@ HTMLLoader.defaultProps = {
componentId: null,
cssClassName: '',
testId: '',
delay: 0,
};
export default HTMLLoader;

View File

@@ -29,7 +29,13 @@ function PostPreviewPane({
className="float-right p-3"
iconClassNames="icon-size"
/>
<HTMLLoader htmlNode={htmlNode} cssClassName="text-primary" componentId="post-preview" testId="post-preview" />
<HTMLLoader
htmlNode={htmlNode}
cssClassName="text-primary"
componentId="post-preview"
testId="post-preview"
delay={500}
/>
</div>
)}
<div className="d-flex justify-content-end">

View File

@@ -0,0 +1,108 @@
/* 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);

View File

@@ -1,3 +1,4 @@
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';

View File

@@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
@@ -16,7 +16,6 @@ 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';
function ActionsDropdown({
intl,
@@ -26,41 +25,54 @@ function ActionsDropdown({
iconSize,
dropDownIconSize,
}) {
const buttonRef = useRef();
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);
const actions = useActions(commentOrPost);
const { enableInContextSidebar } = useContext(DiscussionContext);
const handleActions = (action) => {
const handleActions = useCallback((action) => {
const actionFunction = actionHandlers[action];
if (actionFunction) {
actionFunction();
} else {
logError(`Unknown or unimplemented action ${action}`);
}
};
}, [actionHandlers]);
const blackoutDateRange = useSelector(selectBlackoutDate);
// Find and remove edit action if in blackout date range.
if (inBlackoutDateRange(blackoutDateRange)) {
actions.splice(actions.findIndex(action => action.id === 'edit'), 1);
}
const onClickButton = useCallback(() => {
setTarget(buttonRef.current);
open();
}, [open]);
const onCloseModal = useCallback(() => {
close();
setTarget(null);
}, [close]);
return (
<>
<IconButton
onClick={open}
onClick={onClickButton}
alt={intl.formatMessage(messages.actionsAlt)}
src={MoreHoriz}
iconAs={Icon}
disabled={disabled}
size={iconSize}
ref={setTarget}
ref={buttonRef}
iconClassNames={dropDownIconSize ? 'dropdown-icon-dimentions' : ''}
/>
<div className="actions-dropdown">
<ModalPopup
onClose={close}
onClose={onCloseModal}
positionRef={target}
isOpen={isOpen}
placement={enableInContextSidebar ? 'left' : 'auto-start'}
placement="bottom-end"
>
<div
className="bg-white p-1 shadow d-flex flex-column"

View File

@@ -28,6 +28,7 @@ const discussionPostId = 'thread-1';
const questionPostId = 'thread-2';
const courseId = 'course-v1:edX+TestX+Test_Course';
const reverseOrder = false;
const enableInContextSidebar = false;
let store;
let axiosMock;
let container;
@@ -45,6 +46,7 @@ function mockAxiosReturnPagedComments() {
requested_fields: 'profile_image',
endorsed,
reverse_order: reverseOrder,
enable_in_context_sidebar: enableInContextSidebar,
},
})
.reply(200, Factory.build('commentsResult', { can_delete: true }, {

View File

@@ -124,7 +124,7 @@ export default function DiscussionsHome() {
</Switch>
)}
</div>
<DiscussionsProductTour />
{!enableInContextSidebar && <DiscussionsProductTour />}
</main>
{!enableInContextSidebar && <Footer />}
</DiscussionContext.Provider>

View File

@@ -0,0 +1,255 @@
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();
});
});
});
});

View File

@@ -0,0 +1,233 @@
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();
});
});
});
});

View File

@@ -8,7 +8,7 @@ Factory.define('topic')
.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 });
@@ -27,6 +27,11 @@ Factory.define('sub-section')
.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, {
@@ -42,7 +47,7 @@ Factory.define('sub-section')
Factory.define('section')
.sequence('block_id', (idx) => `${idx}`)
.option('topicPrefix', null, '')
.sequence('id', ['topicPrefix'], (idx, topicPrefix) => `${topicPrefix}-topic-${idx}`)
.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')
@@ -53,9 +58,15 @@ Factory.define('section')
.sequence('student_view_url', ['id', 'courseId'],
(idx, id, courseId) => `${getApiBaseUrl}/xblock/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`)
.attr('type', null, 'chapter')
.attr('children', ['display-name'], (name) => {
.attr('children', ['id', 'display-name'], (id, name) => {
Factory.reset('sub-section');
return Factory.buildList('sub-section', 2, null, { sectionPrefix: `${name}-`, topicPrefix: 'section' });
return Factory.buildList('sub-section', 2, null, {
sectionPrefix: `${name}-`,
topicPrefix: 'section',
id,
discussionCount: 1,
questionCount: 1,
});
});
Factory.define('thread-counts')

View File

@@ -101,7 +101,7 @@ describe('Redux in context topics tests', () => {
// contain chapter at first level
coursewareTopics.forEach((chapter, index) => {
expect(chapter.courseware).toEqual(true);
expect(chapter.id).toEqual(`courseware-topic-${index + 1}`);
expect(chapter.id).toEqual(`courseware-topic-${index + 1}-v3`);
expect(chapter.type).toEqual('chapter');
expect(chapter).toHaveProperty('blockId');
expect(chapter).toHaveProperty('lmsWebUrl');
@@ -120,7 +120,7 @@ describe('Redux in context topics tests', () => {
// contain sub section at third level
section.children.forEach((subSection, subSecIndex) => {
expect(subSection.enabledInContext).toEqual(true);
expect(subSection.id).toEqual(`${section.id}-${subSecIndex + 1}`);
expect(subSection.id).toEqual(`courseware-topic-${index + 1}-v3-${subSecIndex + 1}`);
expect(subSection).toHaveProperty('usageKey');
expect(subSection).not.toHaveProperty('blockId');
expect(subSection?.threadCounts?.discussion).toEqual(1);

View File

@@ -88,7 +88,7 @@ describe('In Context Topics Selector test cases', () => {
expect(coursewareTopics).not.toBeUndefined();
coursewareTopics.forEach((topic, index) => {
expect(topic?.id).toEqual(`courseware-topic-${index + 1}`);
expect(topic?.id).toEqual(`courseware-topic-${index + 1}-v3`);
});
});
});

View File

@@ -50,6 +50,7 @@ function TopicSearchBar({ intl }) {
<Icon
src={SearchIcon}
onClick={() => onSubmit(searchValue)}
data-testid="search-icon"
/>
</span>
</SearchField.Advanced>

View File

@@ -7,6 +7,7 @@ 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';
@@ -49,12 +50,13 @@ function SectionBaseGroup({
aria-current={isSelected(section.id) ? 'page' : undefined}
tabIndex={(isSelected(subsection.id) || index === 0) ? 0 : -1}
>
<div className="d-flex flex-row py-3.5 px-4">
<div className="d-flex flex-row pt-2.5 pb-2 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>

View File

@@ -11,6 +11,7 @@ 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';
@@ -53,65 +54,11 @@ function Topic({
{topic?.name || topic?.displayName || intl.formatMessage(messages.unnamedTopicSubCategories)}
</div>
</div>
<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>
<TopicStats
threadCounts={topic?.threadCounts}
activeFlags={topic?.activeFlags}
inactiveFlags={topic?.inactiveFlags}
/>
</div>
</div>
</Link>

View File

@@ -64,9 +64,9 @@ describe('LearnersView', () => {
axiosMock.onGet(`${coursesApiUrl}${courseId}/activity_stats/`)
.reply(() => [200, learnersData]);
const learnersProfile = Factory.build('learnersProfile', {}, {
username: ['leaner-1', 'leaner-2', 'leaner-3'],
username: ['learner-1', 'learner-2', 'learner-3'],
});
axiosMock.onGet(`${userProfileApiUrl}?username=leaner-1,leaner-2,leaner-3`)
axiosMock.onGet(`${userProfileApiUrl}?username=learner-1,learner-2,learner-3`)
.reply(() => [200, learnersProfile.profiles]);
await executeThunk(fetchLearners(courseId), store.dispatch, store.getState);
});

View File

@@ -2,7 +2,7 @@ import { Factory } from 'rosie';
Factory.define('learner')
.sequence('id')
.attr('username', ['id'], (id) => `leaner-${id}`)
.attr('username', ['id'], (id) => `learner-${id}`)
.option('activeFlags', null, null)
.attr('active_flags', ['activeFlags'], (activeFlags) => activeFlags)
.attrs({
@@ -23,14 +23,8 @@ Factory.define('learnersResult')
['courseId', 'count', 'page', 'pageSize'],
(courseId, count, page, pageSize) => {
const numPages = Math.ceil(count / pageSize);
const next = page < numPages
? `http://test.site/api/discussion/v1/courses/course-v1:edX+DemoX+Demo_Course/activity_stats?page=${page + 1
}`
: null;
const prev = page > 1
? `http://test.site/api/discussion/v1/courses/course-v1:edX+DemoX+Demo_Course/activity_stats?page=${page - 1
}`
: null;
const next = page < numPages ? page + 1 : null;
const prev = page > 1 ? page - 1 : null;
return {
next,
prev,
@@ -65,7 +59,7 @@ Factory.define('learnersResult')
);
Factory.define('learnersProfile')
.option('usernames', null, ['leaner-1', 'leaner-2', 'leaner-3'])
.option('usernames', null, ['learner-1', 'learner-2', 'learner-3'])
.attr('profiles', ['usernames'], (usernames) => {
const profiles = usernames.map((user) => ({
account_privacy: 'private',

View File

@@ -10,6 +10,8 @@ ensureConfig([
export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
export const getUserProfileApiUrl = () => `${getConfig().LMS_BASE_URL}/api/user/v1/accounts`;
export const learnerPostsApiUrl = (courseId) => `${getCoursesApiUrl()}${courseId}/learner/`;
export const learnersApiUrl = (courseId) => `${getCoursesApiUrl()}${courseId}/activity_stats/`;
/**
* Fetches all the learners in the given course.
@@ -18,8 +20,7 @@ export const getUserProfileApiUrl = () => `${getConfig().LMS_BASE_URL}/api/user/
* @returns {Promise<{}>}
*/
export async function getLearners(courseId, params) {
const url = `${getCoursesApiUrl()}${courseId}/activity_stats/`;
const { data } = await getAuthenticatedHttpClient().get(url, { params });
const { data } = await getAuthenticatedHttpClient().get(learnersApiUrl(courseId), { params });
return data;
}
@@ -65,8 +66,6 @@ export async function getUserPosts(courseId, {
countFlagged,
cohort,
} = {}) {
const learnerPostsApiUrl = `${getCoursesApiUrl()}${courseId}/learner/`;
const params = snakeCaseObject({
page,
pageSize,
@@ -81,6 +80,6 @@ export async function getUserPosts(courseId, {
});
const { data } = await getAuthenticatedHttpClient()
.get(learnerPostsApiUrl, { params });
.get(learnerPostsApiUrl(courseId), { params });
return data;
}

View File

@@ -0,0 +1,79 @@
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 { setupLearnerMockResponse, setupPostsMockResponse } from '../test-utils';
import './__factories__';
const courseId2 = 'course-v1:edX+TestX+Test_Course2';
let axiosMock;
describe('Learner api test cases', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
Factory.resetAll();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
afterEach(() => {
axiosMock.reset();
});
it('Successfully get and store API response for the learner\'s list and learners posts in redux',
async () => {
const learners = await setupLearnerMockResponse();
const threads = await setupPostsMockResponse();
expect(learners.status).toEqual('successful');
expect(Object.values(learners.learnerProfiles)).toHaveLength(3);
expect(threads.status).toEqual('successful');
expect(Object.values(threads.threadsById)).toHaveLength(2);
});
it.each([
{ status: 'statusUnread', search: 'Title', cohort: 'post' },
{ status: 'statusUnanswered', search: 'Title', cohort: 'post' },
{ status: 'statusReported', search: 'Title', cohort: 'post' },
{ status: 'statusUnresponded', search: 'Title', cohort: 'post' },
])('Successfully fetch user posts based on %s filters',
async ({ status, search, cohort }) => {
const threads = await setupPostsMockResponse({ filters: { status, search, cohort } });
expect(threads.status).toEqual('successful');
expect(Object.values(threads.threadsById)).toHaveLength(2);
});
it('Failed to fetch learners', async () => {
const learners = await setupLearnerMockResponse({ learnerCourseId: courseId2 });
expect(learners.status).toEqual('failed');
});
it('Denied to fetch learners', async () => {
const learners = await setupLearnerMockResponse({ statusCode: 403 });
expect(learners.status).toEqual('denied');
});
it('Failed to fetch learnerPosts', async () => {
const threads = await setupPostsMockResponse({ learnerCourseId: courseId2 });
expect(threads.status).toEqual('failed');
});
it('Denied to fetch learnerPosts', async () => {
const threads = await setupPostsMockResponse({ statusCode: 403 });
expect(threads.status).toEqual('denied');
});
});

View File

@@ -0,0 +1,120 @@
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 { setupLearnerMockResponse } from '../test-utils';
import { setPostFilter, setSortedBy, setUsernameSearch } from './slices';
import { fetchLearners } from './thunks';
import './__factories__';
let axiosMock;
let store;
describe('Learner redux test cases', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
Factory.resetAll();
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
afterEach(() => {
axiosMock.reset();
});
test('Successfully load initial states in redux', async () => {
executeThunk(
fetchLearners('course-v1:edX+DemoX+Demo_Course', { usernameSearch: 'learner-1' }),
store.dispatch, store.getState,
);
const { learners } = store.getState();
expect(learners.status).toEqual('in-progress');
expect(learners.learnerProfiles).toEqual({});
expect(learners.pages).toHaveLength(0);
expect(learners.nextPage).toBeNull();
expect(learners.totalPages).toBeNull();
expect(learners.totalLearners).toBeNull();
expect(learners.sortedBy).toEqual('activity');
expect(learners.usernameSearch).toBeNull();
expect(learners.postFilter.postType).toEqual('all');
expect(learners.postFilter.status).toEqual('statusAll');
expect(learners.postFilter.orderBy).toEqual('lastActivityAt');
expect(learners.postFilter.cohort).toEqual('');
});
test('Successfully store a learner posts stats data as pages object in redux',
async () => {
const learners = await setupLearnerMockResponse();
const page = learners.pages[0];
const statsObject = page[0];
expect(page).toHaveLength(3);
expect(statsObject.responses).toEqual(3);
expect(statsObject.threads).toEqual(1);
expect(statsObject.replies).toEqual(0);
});
test('Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux',
async () => {
const learners = await setupLearnerMockResponse();
expect(learners.nextPage).toEqual(2);
expect(learners.totalPages).toEqual(2);
expect(learners.totalLearners).toEqual(6);
expect(learners.sortedBy).toEqual('activity');
});
test('Successfully updated the learner\'s sort data in redux', async () => {
const learners = await setupLearnerMockResponse();
expect(learners.sortedBy).toEqual('activity');
expect(learners.pages[0]).toHaveLength(3);
await store.dispatch(setSortedBy('recency'));
const updatedLearners = store.getState().learners;
expect(updatedLearners.sortedBy).toEqual('recency');
expect(updatedLearners.pages).toHaveLength(0);
});
test('Successfully updated the post-filter data in redux', async () => {
const learners = await setupLearnerMockResponse();
const filter = {
...learners.postFilter,
postType: 'discussion',
};
expect(learners.postFilter.postType).toEqual('all');
await store.dispatch(setPostFilter(filter));
const updatedLearners = store.getState().learners;
expect(updatedLearners.postFilter.postType).toEqual('discussion');
expect(updatedLearners.pages).toHaveLength(0);
});
test('Successfully update the learner\'s search query in redux when searching for a learner',
async () => {
const learners = await setupLearnerMockResponse();
expect(learners.usernameSearch).toBeNull();
await store.dispatch(setUsernameSearch('learner-2'));
const updatedLearners = store.getState().learners;
expect(updatedLearners.usernameSearch).toEqual('learner-2');
});
});

View File

@@ -0,0 +1,81 @@
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 { getUserProfileApiUrl, learnersApiUrl } from './api';
import {
learnersLoadingStatus,
selectLearnerNextPage,
selectLearnerSorting,
selectUsernameSearch,
} from './selectors';
import { fetchLearners } from './thunks';
import './__factories__';
const courseId = 'course-v1:edX+DemoX+Demo_Course';
const userProfileApiUrl = getUserProfileApiUrl();
let axiosMock;
let store;
const username = 'abc123';
const learnerCount = 6;
let state;
describe('Learner selectors test cases', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username,
administrator: true,
roles: [],
},
});
Factory.resetAll();
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(learnersApiUrl(courseId))
.reply(() => [200, Factory.build('learnersResult', {}, {
count: learnerCount,
pageSize: 3,
})]);
axiosMock.onGet(`${userProfileApiUrl}?username=learner-1,learner-2,learner-3`)
.reply(() => [200, Factory.build('learnersProfile', {}, {
username: ['learner-1', 'learner-2', 'learner-3'],
}).profiles]);
await executeThunk(fetchLearners(courseId), store.dispatch, store.getState);
state = store.getState();
});
afterEach(() => {
axiosMock.reset();
});
test('learnersLoadingStatus should return learners list loading status.', async () => {
const status = learnersLoadingStatus()(state);
expect(status).toEqual('successful');
});
test('selectUsernameSearch should return a learner search query.', async () => {
const userNameSearch = selectUsernameSearch()(state);
expect(userNameSearch).toBeNull();
});
test('selectLearnerSorting should return learner sortedBy.', async () => {
const learnerSorting = selectLearnerSorting()(state);
expect(learnerSorting).toEqual('activity');
});
test('selectLearnerNextPage should return learners next page.', async () => {
const learnerNextPage = selectLearnerNextPage()(state);
expect(learnerNextPage).toEqual(2);
});
});

View File

@@ -0,0 +1,52 @@
import MockAdapter from 'axios-mock-adapter';
import { Factory } from 'rosie';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils';
import { getUserProfileApiUrl, learnerPostsApiUrl, learnersApiUrl } from './data/api';
import { fetchLearners, fetchUserPosts } from './data/thunks';
const courseId = 'course-v1:edX+DemoX+Demo_Course';
export async function setupLearnerMockResponse({
learnerCourseId = courseId,
statusCode = 200,
learnerCount = 6,
} = {}) {
const store = initializeStore();
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(learnersApiUrl(learnerCourseId))
.reply(() => [statusCode, Factory.build('learnersResult', {}, {
count: learnerCount,
pageSize: 3,
})]);
axiosMock.onGet(`${getUserProfileApiUrl()}?username=learner-1,learner-2,learner-3`)
.reply(() => [statusCode, Factory.build('learnersProfile', {}, {
username: ['learner-1', 'learner-2', 'learner-3'],
}).profiles]);
await executeThunk(fetchLearners(courseId), store.dispatch, store.getState);
return store.getState().learners;
}
export async function setupPostsMockResponse({
learnerCourseId = courseId,
statusCode = 200,
username = 'abc123',
filters = { status: 'all' },
} = {}) {
const store = initializeStore();
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(learnerPostsApiUrl(learnerCourseId), { username, count_flagged: true })
.reply(() => [statusCode, Factory.build('learnerPosts', {}, {
abuseFlaggedCount: 1,
})]);
await executeThunk(fetchUserPosts(courseId, { filters }), store.dispatch, store.getState);
return store.getState().threads;
}

View File

@@ -40,6 +40,7 @@ function PostCommentsView({ intl }) {
const {
courseId, learnerUsername, category, topicId, page, enableInContextSidebar,
} = useContext(DiscussionContext);
const enableCommentsSort = false;
useEffect(() => {
if (!thread) { submitDispatch(fetchThread(postId, courseId, true)); }
@@ -98,7 +99,7 @@ function PostCommentsView({ intl }) {
)
)}
<div
className="discussion-comments d-flex flex-column card border-0 post-card-margin post-card-padding"
className="discussion-comments d-flex flex-column card border-0 post-card-margin post-card-padding on-focus"
>
<Post post={thread} handleAddResponseButton={() => setAddingResponse(true)} />
{!thread.closed && (
@@ -109,7 +110,7 @@ function PostCommentsView({ intl }) {
/>
)}
</div>
{!!commentsCount && commentsStatus === RequestStatus.SUCCESSFUL && <CommentsSort />}
{!!commentsCount && commentsStatus === RequestStatus.SUCCESSFUL && enableCommentsSort && <CommentsSort />}
{thread.type === ThreadType.DISCUSSION && (
<CommentsView
postId={postId}

View File

@@ -10,6 +10,7 @@ import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { getApiBaseUrl } from '../../data/constants';
import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils';
import { DiscussionContext } from '../common/context';
@@ -17,12 +18,13 @@ import { getCourseConfigApiUrl } from '../data/api';
import { fetchCourseConfig } from '../data/thunks';
import DiscussionContent from '../discussions-home/DiscussionContent';
import { getThreadsApiUrl } from '../posts/data/api';
import { fetchThreads } from '../posts/data/thunks';
import { fetchThread, fetchThreads } from '../posts/data/thunks';
import { fetchCourseTopics } from '../topics/data/thunks';
import { getCommentsApiUrl } from './data/api';
import { removeComment } from './data/thunks';
import '../posts/data/__factories__';
import './data/__factories__';
import '../topics/data/__factories__';
const courseConfigApiUrl = getCourseConfigApiUrl();
const commentsApiUrl = getCommentsApiUrl();
@@ -31,11 +33,12 @@ const discussionPostId = 'thread-1';
const questionPostId = 'thread-2';
const closedPostId = 'thread-2';
const courseId = 'course-v1:edX+TestX+Test_Course';
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
const reverseOrder = false;
const enableInContextSidebar = false;
let store;
let axiosMock;
let testLocation;
let container;
function mockAxiosReturnPagedComments() {
[null, false, true].forEach(endorsed => {
@@ -50,6 +53,7 @@ function mockAxiosReturnPagedComments() {
requested_fields: 'profile_image',
endorsed,
reverse_order: reverseOrder,
enable_in_context_sidebar: enableInContextSidebar,
},
})
.reply(200, Factory.build('commentsResult', { can_delete: true }, {
@@ -85,6 +89,12 @@ function mockAxiosReturnPagedCommentsResponses() {
}
}
async function getThreadAPIResponse(threadId, topicId) {
axiosMock.onGet(`${threadsApiUrl}${discussionPostId}/`)
.reply(200, Factory.build('thread', { id: threadId, topic_id: topicId }));
await executeThunk(fetchThread(discussionPostId), store.dispatch, store.getState);
}
function renderComponent(postId) {
const wrapper = render(
<IntlProvider locale="en">
@@ -106,9 +116,48 @@ function renderComponent(postId) {
</AppProvider>
</IntlProvider>,
);
container = wrapper.container;
return wrapper;
}
describe('PostView', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
Factory.resetAll();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(topicsApiUrl)
.reply(200, {
non_courseware_topics: Factory.buildList('topic', 1, {}, { topicPrefix: 'non-courseware-' }),
courseware_topics: Factory.buildList('category', 1, {}, { name: 'courseware' }),
});
executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState);
});
it('should show Topic Info for non-courseware topics', async () => {
await getThreadAPIResponse('thread-1', 'non-courseware-topic-1');
renderComponent(discussionPostId);
expect(await screen.findByText('Related to')).toBeInTheDocument();
expect(await screen.findByText('non-courseware-topic 1')).toBeInTheDocument();
});
it('should show Topic Info for courseware topics with category', async () => {
await getThreadAPIResponse('thread-2', 'courseware-topic-2');
renderComponent('thread-2');
expect(await screen.findByText('Related to')).toBeInTheDocument();
expect(await screen.findByText('category-1 / courseware-topic 2')).toBeInTheDocument();
});
});
describe('ThreadView', () => {
beforeEach(() => {
initializeMockApp({
@@ -698,68 +747,4 @@ describe('ThreadView', () => {
expect(screen.queryByRole('dialog', { name: /Delete/i, exact: false })).toBeInTheDocument();
});
});
describe('for comments sort', () => {
it('should show sort dropdown if there are endorse or unendorsed comments', async () => {
renderComponent(discussionPostId);
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
const sortWrapper = container.querySelector('.comments-sort');
const sortDropDown = within(sortWrapper).getByRole('button', { name: /Oldest first/i });
expect(comment).toBeInTheDocument();
expect(sortDropDown).toBeInTheDocument();
});
it('should not show sort dropdown if there is no response', async () => {
const commentId = 'comment-1';
renderComponent(discussionPostId);
await waitFor(() => screen.findByTestId('comment-comment-1'));
axiosMock.onDelete(`${commentsApiUrl}${commentId}/`).reply(201);
await executeThunk(removeComment(commentId, discussionPostId), store.dispatch, store.getState);
expect(await waitFor(() => screen.findByText('No responses', { exact: true }))).toBeInTheDocument();
expect(container.querySelector('.comments-sort')).not.toBeInTheDocument();
});
it('should have only two options', async () => {
renderComponent(discussionPostId);
await waitFor(() => screen.findByTestId('comment-comment-1'));
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Oldest first/i })); });
const dropdown = await waitFor(() => screen.findByTestId('comment-sort-dropdown-modal-popup'));
expect(dropdown).toBeInTheDocument();
expect(await within(dropdown).getAllByRole('button')).toHaveLength(2);
});
it('should be selected Oldest first and auto focus', async () => {
renderComponent(discussionPostId);
await waitFor(() => screen.findByTestId('comment-comment-1'));
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Oldest first/i })); });
const dropdown = await waitFor(() => screen.findByTestId('comment-sort-dropdown-modal-popup'));
expect(dropdown).toBeInTheDocument();
expect(within(dropdown).getByRole('button', { name: /Oldest first/i })).toBeInTheDocument();
expect(within(dropdown).getByRole('button', { name: /Oldest first/i })).toHaveFocus();
expect(within(dropdown).getByRole('button', { name: /Newest first/i })).not.toHaveFocus();
});
test('successfully handles sort state update', async () => {
renderComponent(discussionPostId);
expect(store.getState().comments.sortOrder).toBeFalsy();
await waitFor(() => screen.findByTestId('comment-comment-1'));
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Oldest first/i })); });
const dropdown = await waitFor(() => screen.findByTestId('comment-sort-dropdown-modal-popup'));
await act(async () => {
fireEvent.click(within(dropdown).getByRole('button', { name: /Newest first/i }));
});
expect(store.getState().comments.sortOrder).toBeTruthy();
});
});
});

View File

@@ -1,4 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
import React, {
useCallback,
useContext, useEffect, useMemo, useState,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@@ -43,28 +46,28 @@ function Comment({
const hasMorePages = useSelector(selectCommentHasMorePages(comment.id));
const currentPage = useSelector(selectCommentCurrentPage(comment.id));
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
const {
courseId,
} = useContext(DiscussionContext);
const { courseId } = useContext(DiscussionContext);
useEffect(() => {
// If the comment has a parent comment, it won't have any children, so don't fetch them.
if (hasChildren && !currentPage && showFullThread) {
dispatch(fetchCommentResponses(comment.id, { page: 1 }));
}
}, [comment.id]);
const actions = useActions({
...comment,
postType,
});
const endorseIcons = actions.find(({ action }) => action === EndorsementStatus.ENDORSED);
const handleAbusedFlag = () => {
const handleAbusedFlag = useCallback(() => {
if (comment.abuseFlagged) {
dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged }));
} else {
showReportConfirmation();
}
};
}, [comment.abuseFlagged, comment.id, dispatch, showReportConfirmation]);
const handleDeleteConfirmation = () => {
dispatch(removeComment(comment.id));
@@ -76,7 +79,7 @@ function Comment({
hideReportConfirmation();
};
const actionHandlers = {
const actionHandlers = useMemo(() => ({
[ContentActions.EDIT_CONTENT]: () => setEditing(true),
[ContentActions.ENDORSE]: async () => {
await dispatch(editComment(comment.id, { endorsed: !comment.endorsed }, ContentActions.ENDORSE));
@@ -84,7 +87,7 @@ function Comment({
},
[ContentActions.DELETE]: showDeleteConfirmation,
[ContentActions.REPORT]: () => handleAbusedFlag(),
};
}), [showDeleteConfirmation, dispatch, comment.id, comment.endorsed, comment.threadId, courseId, handleAbusedFlag]);
const handleLoadMoreComments = () => (
dispatch(fetchCommentResponses(comment.id, { page: currentPage + 1 }))
@@ -119,7 +122,7 @@ function Comment({
/>
)}
<EndorsedAlertBanner postType={postType} content={comment} />
<div className="d-flex flex-column post-card-comment px-4 pt-3.5 pb-10px" aria-level={5}>
<div className="d-flex flex-column post-card-comment px-4 pt-3.5 pb-10px" tabIndex="0">
<HoverCard
commentOrPost={comment}
actionHandlers={actionHandlers}

View File

@@ -13,6 +13,7 @@ import { TinyMCEEditor } from '../../../../components';
import FormikErrorFeedback from '../../../../components/FormikErrorFeedback';
import PostPreviewPane from '../../../../components/PostPreviewPane';
import { useDispatchWithState } from '../../../../data/hooks';
import { DiscussionContext } from '../../../common/context';
import {
selectModerationSettings,
selectUserHasModerationPrivileges,
@@ -32,6 +33,7 @@ function CommentEditor({
}) {
const editorRef = useRef(null);
const { authenticatedUser } = useContext(AppContext);
const { enableInContextSidebar } = useContext(DiscussionContext);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const userIsStaff = useSelector(selectUserIsStaff);
@@ -71,7 +73,7 @@ function CommentEditor({
};
await dispatch(editComment(comment.id, payload));
} else {
await dispatch(addComment(values.comment, comment.threadId, comment.parentId));
await dispatch(addComment(values.comment, comment.threadId, comment.parentId, enableInContextSidebar));
}
/* istanbul ignore if: TinyMCE is mocked so this cannot be easily tested */
if (editorRef.current) {

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
@@ -31,13 +31,13 @@ function Reply({
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
const handleAbusedFlag = () => {
const handleAbusedFlag = useCallback(() => {
if (reply.abuseFlagged) {
dispatch(editComment(reply.id, { flagged: !reply.abuseFlagged }));
} else {
showReportConfirmation();
}
};
}, [dispatch, reply.abuseFlagged, reply.id, showReportConfirmation]);
const handleDeleteConfirmation = () => {
dispatch(removeComment(reply.id));
@@ -49,7 +49,7 @@ function Reply({
hideReportConfirmation();
};
const actionHandlers = {
const actionHandlers = useMemo(() => ({
[ContentActions.EDIT_CONTENT]: () => setEditing(true),
[ContentActions.ENDORSE]: () => dispatch(editComment(
reply.id,
@@ -58,7 +58,8 @@ function Reply({
)),
[ContentActions.DELETE]: showDeleteConfirmation,
[ContentActions.REPORT]: () => handleAbusedFlag(),
};
}), [dispatch, handleAbusedFlag, reply.endorsed, reply.id, showDeleteConfirmation]);
const authorAvatars = useSelector(selectAuthorAvatars(reply.author));
const colorClass = AvatarOutlineAndLabelColors[reply.authorLabel];
const hasAnyAlert = useAlertBannerVisible(reply);

View File

@@ -16,6 +16,8 @@ export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussi
* @param {EndorsementStatus} endorsed
* @param {number=} page
* @param {number=} pageSize
* @param reverseOrder
* @param enableInContextSidebar
* @returns {Promise<{}>}
*/
export async function getThreadComments(
@@ -24,6 +26,7 @@ export async function getThreadComments(
page,
pageSize,
reverseOrder,
enableInContextSidebar = false,
} = {},
) {
const params = snakeCaseObject({
@@ -33,6 +36,7 @@ export async function getThreadComments(
pageSize,
reverseOrder,
requestedFields: 'profile_image',
enableInContextSidebar,
});
const { data } = await getAuthenticatedHttpClient()
@@ -69,11 +73,14 @@ export async function getCommentResponses(
* @param {string} comment Raw comment data to post.
* @param {string} threadId Thread ID for thread in which to post comment.
* @param {string=} parentId ID for a comments parent.
* @param {boolean} enableInContextSidebar
* @returns {Promise<{}>}
*/
export async function postComment(comment, threadId, parentId = null) {
export async function postComment(comment, threadId, parentId = null, enableInContextSidebar = false) {
const { data } = await getAuthenticatedHttpClient()
.post(getCommentsApiUrl(), snakeCaseObject({ threadId, raw_body: comment, parentId }));
.post(getCommentsApiUrl(), snakeCaseObject({
threadId, raw_body: comment, parentId, enableInContextSidebar,
}));
return data;
}

View File

@@ -1,9 +1,12 @@
import { useEffect } from 'react';
import { useContext, 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 { DiscussionContext } from '../../common/context';
import { selectThread } from '../../posts/data/selectors';
import { markThreadAsRead } from '../../posts/data/thunks';
import {
@@ -11,6 +14,16 @@ import {
} 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));
@@ -30,18 +43,24 @@ export function usePostComments(postId, endorsed = null) {
const reverseOrder = useSelector(selectCommentSortOrder);
const hasMorePages = useSelector(selectThreadHasMorePages(postId, endorsed));
const currentPage = useSelector(selectThreadCurrentPage(postId, endorsed));
const { enableInContextSidebar } = useContext(DiscussionContext);
const handleLoadMoreResponses = async () => dispatch(fetchThreadComments(postId, {
endorsed,
page: currentPage + 1,
reverseOrder,
}));
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,
enableInContextSidebar,
}));
}, [postId, reverseOrder]);

View File

@@ -80,12 +80,15 @@ export function fetchThreadComments(
page = 1,
reverseOrder,
endorsed = EndorsementStatus.DISCUSSION,
enableInContextSidebar,
} = {},
) {
return async (dispatch) => {
try {
dispatch(fetchCommentsRequest());
const data = await getThreadComments(threadId, { page, reverseOrder, endorsed });
const data = await getThreadComments(threadId, {
page, reverseOrder, endorsed, enableInContextSidebar,
});
dispatch(fetchCommentsSuccess({
...normaliseComments(camelCaseObject(data)),
endorsed,
@@ -144,7 +147,7 @@ export function editComment(commentId, comment, action = null) {
};
}
export function addComment(comment, threadId, parentId = null) {
export function addComment(comment, threadId, parentId = null, enableInContextSidebar = false) {
return async (dispatch) => {
try {
dispatch(postCommentRequest({
@@ -152,7 +155,7 @@ export function addComment(comment, threadId, parentId = null) {
threadId,
parentId,
}));
const data = await postComment(comment, threadId, parentId);
const data = await postComment(comment, threadId, parentId, enableInContextSidebar);
dispatch(postCommentSuccess(camelCaseObject(data)));
} catch (error) {
if (getHttpErrorStatus(error) === 403) {

View File

@@ -160,9 +160,9 @@ describe('PostsView', () => {
test('displays a list of posts in a topic', async () => {
setupStore();
await act(async () => {
await renderComponent({ topicId: 'some-topic-1' });
await renderComponent({ topicId: 'test-topic-1' });
});
expect(screen.getAllByText(/this is thread-\d+ in topic some-topic-1/i)).toHaveLength(Math.ceil(threadCount / 3));
expect(screen.getAllByText(/this is thread-\d+ in topic test-topic-1/i)).toHaveLength(Math.ceil(threadCount / 3));
});
test.each([true, false])(
@@ -173,10 +173,10 @@ describe('PostsView', () => {
blocks: {
'test-usage-key': {
type: 'vertical',
topics: ['some-topic-2', 'some-topic-0'],
topics: ['test-topic-2', 'test-topic-0'],
parent: 'test-seq-key',
},
'test-seq-key': { type: 'sequential', topics: ['some-topic-0', 'some-topic-1', 'some-topic-2'] },
'test-seq-key': { type: 'sequential', topics: ['test-topic-0', 'test-topic-1', 'test-topic-2'] },
},
},
config: { groupAtSubsection: grouping, hasModerationPrivileges: true, provider: 'openedx' },
@@ -185,12 +185,12 @@ describe('PostsView', () => {
await renderComponent({ category: 'test-usage-key', enableInContextSidebar: true, p: true });
});
const topicThreadCount = Math.ceil(threadCount / 3);
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-2/i))
expect(screen.queryAllByText(/this is thread-\d+ in topic test-topic-2/i))
.toHaveLength(topicThreadCount);
expect(screen.queryAllByText(/this is thread-\d+ in topic some-topic-0/i))
expect(screen.queryAllByText(/this is thread-\d+ in topic test-topic-0/i))
.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))
expect(screen.queryAllByText(/this is thread-\d+ in topic test-topic-1/i))
.toHaveLength(grouping ? topicThreadCount : 2);
},
);

View File

@@ -7,7 +7,7 @@ Factory.define('thread')
.sequence('rendered_body', (idx) => `Some contents for <b>thread number ${idx}</b>.`)
.sequence('type', (idx) => (idx % 2 === 1 ? 'discussion' : 'question'))
.sequence('pinned', idx => (idx < 3))
.sequence('topic_id', idx => `some-topic-${(idx % 3)}`)
.sequence('topic_id', idx => `test-topic-${(idx % 3)}`)
.sequence('closed', idx => Boolean(idx % 3 === 2)) // Mark every 3rd post closed
.attr('comment_list_url', ['id'], (threadId) => `http://test.site/api/discussion/v1/comments/?thread_id=${threadId}`)
.attrs({

View File

@@ -87,6 +87,7 @@ export async function getThread(threadId, courseId) {
* @param {boolean} following Follow the thread after creating
* @param {boolean} anonymous Should the thread be anonymous to all users
* @param {boolean} anonymousToPeers Should the thread be anonymous to peers
* @param {boolean} enableInContextSidebar
* @returns {Promise<{}>}
*/
export async function postThread(
@@ -101,6 +102,7 @@ export async function postThread(
anonymous,
anonymousToPeers,
} = {},
enableInContextSidebar = false,
) {
const postData = snakeCaseObject({
courseId,
@@ -112,8 +114,8 @@ export async function postThread(
anonymous,
anonymousToPeers,
groupId: cohort,
enableInContextSidebar,
});
const { data } = await getAuthenticatedHttpClient()
.post(getThreadsApiUrl(), postData);
return data;

View File

@@ -102,7 +102,7 @@ describe('Threads/Posts data layer tests', () => {
expect(store.getState().threads.threadsById['thread-1'])
.toHaveProperty('topicId');
expect(store.getState().threads.threadsById['thread-1'].topicId)
.toEqual('some-topic-1');
.toEqual('test-topic-1');
});
test('successfully handles thread creation', async () => {

View File

@@ -204,6 +204,7 @@ export function createNewThread({
anonymous,
anonymousToPeers,
cohort,
enableInContextSidebar,
}) {
return async (dispatch) => {
try {
@@ -223,7 +224,7 @@ export function createNewThread({
following,
anonymous,
anonymousToPeers,
});
}, enableInContextSidebar);
dispatch(postThreadSuccess(camelCaseObject(data)));
} catch (error) {
if (getHttpErrorStatus(error) === 403) {

View File

@@ -106,7 +106,7 @@ function PostEditor({
const nonCoursewareIds = useSelector(enableInContext ? inContextCoursewareIds : selectNonCoursewareIds);
const coursewareTopics = useSelector(enableInContext ? inContextCourseware : selectCoursewareTopics);
const cohorts = useSelector(selectCourseCohorts);
const post = useSelector(selectThread(postId));
const post = useSelector(editExisting ? selectThread(postId) : () => ({}));
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const settings = useSelector(selectDivisionSettings);
@@ -187,6 +187,7 @@ function PostEditor({
anonymous: allowAnonymous ? values.anonymous : undefined,
anonymousToPeers: allowAnonymousToPeers ? values.anonymousToPeers : undefined,
cohort,
enableInContextSidebar,
}));
}
/* istanbul ignore if: TinyMCE is mocked so this cannot be easily tested */

View File

@@ -141,6 +141,16 @@ 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', () => {

View File

@@ -1,10 +1,11 @@
import React, { useContext } from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink, useToggle } from '@edx/paragon';
@@ -41,13 +42,14 @@ function Post({
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
const handleAbusedFlag = () => {
const handleAbusedFlag = useCallback(() => {
if (post.abuseFlagged) {
dispatch(updateExistingThread(post.id, { flagged: !post.abuseFlagged }));
} else {
showReportConfirmation();
}
};
}, [dispatch, post.abuseFlagged, post.id, showReportConfirmation]);
const handleDeleteConfirmation = async () => {
await dispatch(removeThread(post.id));
@@ -63,7 +65,7 @@ function Post({
hideReportConfirmation();
};
const actionHandlers = {
const actionHandlers = useMemo(() => ({
[ContentActions.EDIT_CONTENT]: () => history.push({
...location,
pathname: `${location.pathname}/edit`,
@@ -81,17 +83,34 @@ function Post({
[ContentActions.COPY_LINK]: () => { navigator.clipboard.writeText(`${window.location.origin}/${courseId}/posts/${post.id}`); },
[ContentActions.PIN]: () => dispatch(updateExistingThread(post.id, { pinned: !post.pinned })),
[ContentActions.REPORT]: () => handleAbusedFlag(),
};
}), [
showDeleteConfirmation,
history,
location,
post.closed,
post.id,
post.pinned,
reasonCodesEnabled,
dispatch,
showClosePostModal,
courseId,
handleAbusedFlag,
]);
const getTopicCategoryName = topicData => (
topicData.usageKey ? getTopicSubsection(topicData.usageKey)?.displayName : topicData.categoryId
);
const getTopicInfo = topicData => (
getTopicCategoryName(topicData) ? `${getTopicCategoryName(topicData)} / ${topicData.name}` : `${topicData.name}`
);
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"
>
<Confirmation
isOpen={isDeleting}
@@ -126,7 +145,7 @@ function Post({
<div className="d-flex mt-14px text-break font-style 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',
{ 'w-100': enableInContextSidebar })}
@@ -134,7 +153,7 @@ function Post({
>
<span className="text-gray-500" style={{ lineHeight: '20px' }}>{intl.formatMessage(messages.relatedTo)}{' '}</span>
<Hyperlink
destination={topicContext.unitLink}
destination={topicContext ? topicContext.unitLink : `${getConfig().BASE_URL}/${courseId}/topics/${post.topicId}`}
target="_top"
>
{(topicContext && !topic)
@@ -147,7 +166,7 @@ function Post({
<span className="w-auto">{topicContext.unitName}</span>
</>
)
: `${getTopicCategoryName(topic)} / ${topic.name}`}
: getTopicInfo(topic)}
</Hyperlink>
</div>
)}

View File

@@ -1,46 +1,6 @@
{
"navigation.course.tabs.label": "مواد المساق",
"learn.course.tabs.navigation.overflow.menu": "المزيد...",
"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.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.fullStop": "•",
"discussions.comment.comments.reason": "السبب",
"discussions.post.closedBy": "تم إقفال المنشور من طرف",
"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 {# أسئلة}",
@@ -68,7 +28,10 @@
"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": "إعادة الفتح",
@@ -109,6 +72,44 @@
"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} {بحث في كل المشاركات",

View File

@@ -1,46 +1,6 @@
{
"navigation.course.tabs.label": "Kursmaterial",
"learn.course.tabs.navigation.overflow.menu": "Mehr...",
"discussions.comments.comment.addComment": "Kommentar hinzufügen",
"discussions.comments.comment.addResponse": "Fügen Sie eine Antwort hinzu",
"discussions.comments.comment.abuseFlaggedMessage": "Inhalte, die den Kursmitarbeitern zur Überprüfung gemeldet wurden",
"discussions.actions.back.alt": "Zurück zur Liste",
"discussions.comments.comment.responseCount": "{num, plural, =0 {Keine Antworten} one {# Antwort wird angezeigt} other {# Antworten werden angezeigt} }",
"discussions.comments.comment.endorsedResponseCount": "{num, plural, =0 {Keine empfohlenen Antworten} one {# empfohlene Antworten werden angezeigt} other {# empfohlene Antworten werden angezeigt} }",
"discussions.comments.comment.loadMoreComments": "Weitere Kommentare laden",
"discussions.comments.comment.loadMoreResponses": "Weitere Antworten laden",
"discussions.comments.comment.visibility": "Dieser Beitrag ist sichtbar für {group, select, null {Jeder} other {{group}} }.",
"discussions.comments.comment.postedTime": "{postType, select, discussion {Diskussion} question {Frage} other {{postType}} } gepostet {a0917e9bee14} von.c5z0",
"discussions.comments.comment.commentTime": "Gepostet {relativeTime}",
"discussions.comments.comment.answer": "Antwort",
"discussions.comments.comment.answeredlabel": "Als beantwortet von markiert",
"discussions.comments.comment.endorsed": "Bestätigt",
"discussions.comments.comment.endorsedlabel": "Bestätigt von",
"discussions.actions.label": "Aktionsmenü",
"discussions.actions.edit": "Bearbeiten",
"discussions.actions.pin": "Veröffentlichen",
"discussions.actions.delete": "Löschen",
"discussions.editor.submit": "Einreichen",
"discussions.editor.submitting": "Übermitteln, einreichen",
"discussions.editor.cancel": "Löschen",
"discussions.editor.error.empty": "Der Beitragsinhalt darf nicht leer sein.",
"discussions.editor.delete.response.title": "Antwort löschen",
"discussions.editor.delete.response.description": "Möchten Sie diese Antwort wirklich dauerhaft löschen?",
"discussions.editor.delete.comment.title": "Kommentar löschen",
"discussions.editor.delete.comment.description": "Möchten Sie diesen Kommentar wirklich dauerhaft löschen?",
"discussions.delete.confirmation.button.delete": "Löschen",
"discussions.editor.response.response.title": "Unangemessene Inhalte melden?",
"discussions.editor.response.description": "Das Diskussionsmoderationsteam überprüft diesen Inhalt und ergreift entsprechende Maßnahmen.",
"discussions.editor.report.comment.title": "Unangemessene Inhalte melden?",
"discussions.editor.report.comment.description": "Das Diskussionsmoderationsteam überprüft diesen Inhalt und ergreift entsprechende Maßnahmen.",
"discussions.editor.comments.editReasonCode": "Grund für die Bearbeitung",
"discussions.editor.posts.editReasonCode.error": "Grund für die Bearbeitung auswählen",
"discussions.comment.comments.editedBy": "Bearbeitet von",
"discussions.comment.comments.fullStop": "•",
"discussions.comment.comments.reason": "Grund",
"discussions.post.closedBy": "Post geschlossen von",
"discussion.comment.time": "{time} vor",
"discussion.thread.notFound": "Thema nicht gefunden",
"discussions.topics.backAlt": "Zurück zur Themenliste",
"discussions.topics.discussions": "{count, plural, =0 {Diskussion} one {# Diskussion} other {# Diskussionen} }",
"discussions.topics.questions": "{count, plural, =0 {Frage} one {# Frage} other {# Fragen} }",
@@ -68,7 +28,10 @@
"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",
@@ -109,6 +72,44 @@
"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}",

View File

@@ -1,46 +1,6 @@
{
"navigation.course.tabs.label": "Material del Curso",
"learn.course.tabs.navigation.overflow.menu": "Más...",
"discussions.comments.comment.addComment": "Add comment",
"discussions.comments.comment.addResponse": "Agregar una respuesta",
"discussions.comments.comment.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.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": "¿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.topics.backAlt": "Volver a la lista de temas",
"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 }",
@@ -68,7 +28,10 @@
"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.actions.close": "Cerrar",
"discussions.actions.reopen": "Reabrir",
@@ -109,6 +72,44 @@
"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}} }",
@@ -141,7 +142,7 @@
"discussions.post.editor.anonymousPost": "Publicar de forma anónima",
"discussions.post.editor.anonymousToPeersPost": "Publicar de forma anónima para tus compañeros",
"discussions.editor.posts.editReasonCode": "Motivo de la edición",
"discussions.editor.posts.showPreview.button": "Show preview",
"discussions.editor.posts.showPreview.button": "Mostrar vista previa",
"discussions.topic.noName.label": "Categoría sin nombre",
"discussions.subtopic.noName.label": "Subcategoría sin nombre",
"discussions.posts.filter.showALl": "Mostrar todo",
@@ -163,7 +164,7 @@
"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.post.author.anonymous": "anónimo",
"discussions.post.addResponse": "Add response",
"discussions.post.addResponse": "Añadir respuesta",
"discussions.post.lastResponse": "Última respuesta {time}",
"discussions.post.postedOn": "Publicado {time} por {author} {authorLabel}",
"discussions.post.contentReported": "Informado",

View File

@@ -1,6 +1,77 @@
{
"navigation.course.tabs.label": "Course Material",
"navigation.course.tabs.label": "Matériel de cours",
"learn.course.tabs.navigation.overflow.menu": "Plus...",
"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 }",
"discussions.topics.reported": "{reported} signalé",
"discussions.topics.previouslyReported": "{previouslyReported} signalé précédemment",
"discussions.topics.find.label": "Rechercher des sujets",
"discussions.topics.unnamed.section.label": "Section sans nom",
"discussions.topics.unnamed.subsection.label": "Sous-section sans nom",
"discussions.subtopics.unnamed.topic.label": "Sujet sans nom",
"discussions.topics.title": "Aucun sujet n'existe",
"discussions.topics.createTopic": "Veuillez contacter votre administrateur pour créer un sujet",
"discussions.topics.nothing": "Nothing here yet",
"discussions.topics.archived.label": "Archivé",
"discussions.learner.reported": "{reported} signalé",
"discussions.learner.previouslyReported": "{previouslyReported} signalé précédemment",
"discussions.learner.lastLogin": "Dernier actif {lastActiveTime}",
"discussions.learner.loadMostLearners": "Charger plus",
"discussions.learner.back": "Retour",
"discussions.learner.activityForLearner": "Activité pour {username}",
"discussions.learner.mostActivity": "La plupart des activités",
"discussions.learner.reportedActivity": "Activité signalée",
"discussions.learner.recentActivity": "Activité récente",
"discussions.learner.sortFilterStatus": "All learners sorted by {sort, select,\n flagged {reported activity}\n activity {most activity}\n other {{sort}}\n }",
"discussion.learner.allActivity": "Toutes les activités",
"discussion.learner.posts": "Posts",
"discussions.actions.button.alt": "Menu Actions",
"discussions.actions.copylink": "Copier le lien",
"discussions.actions.edit": "Modifier",
"discussions.actions.pin": "Épingler",
"discussions.actions.unpin": "Ne plus épingler",
"discussions.actions.delete": "Supprimer",
"discussions.confirmation.button.confirm": "Confirmer",
"discussions.actions.close": "Fermer",
"discussions.actions.reopen": "Réouvrir",
"discussions.actions.report": "Report",
"discussions.actions.unreport": "Unreport",
"discussions.actions.endorse": "Approuver",
"discussions.actions.unendorse": "Ne plus approuver",
"discussions.actions.markAnswered": "Marquer comme répondu",
"discussions.actions.unMarkAnswered": "Unmark as answered",
"discussions.modal.confirmation.button.cancel": "Annuler",
"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": "Rien ici encore",
"discussions.empty.noPostSelected": "No post selected",
"discussions.empty.noTopicSelected": "Aucun sujet sélectionné",
"discussions.sidebar.noResultsFound": "Aucun résultat trouvé",
"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": "Vide",
"discussions.authors.label.staff": "Équipe",
"discussions.authors.label.ta": "TA",
"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": "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.comments.comment.addComment": "Add comment",
"discussions.comments.comment.addResponse": "Ajouter une réponse",
"discussions.comments.comment.abuseFlaggedMessage": "Contenu signalé au personnel pour examen",
@@ -17,9 +88,6 @@
"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",
@@ -41,74 +109,7 @@
"discussions.post.closedBy": "Message fermé par",
"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 }",
"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} signalé",
"discussions.learner.previouslyReported": "{previouslyReported} previously reported",
"discussions.learner.lastLogin": "Last active {lastActiveTime}",
"discussions.learner.loadMostLearners": "Charger plus",
"discussions.learner.back": "Retour",
"discussions.learner.activityForLearner": "Activité pour {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": "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": "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.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 }",

View File

@@ -1,46 +1,6 @@
{
"navigation.course.tabs.label": "Matériel de cours",
"learn.course.tabs.navigation.overflow.menu": "Plus...",
"discussions.comments.comment.addComment": "Ajouter un commentaire",
"discussions.comments.comment.addResponse": "Ajouter une réponse",
"discussions.comments.comment.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.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.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 }",
@@ -68,7 +28,10 @@
"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",
@@ -109,6 +72,44 @@
"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 }",

View File

@@ -1,46 +1,6 @@
{
"navigation.course.tabs.label": "Course Material",
"learn.course.tabs.navigation.overflow.menu": "More...",
"discussions.comments.comment.addComment": "Add comment",
"discussions.comments.comment.addResponse": "Add a response",
"discussions.comments.comment.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.fullStop": "•",
"discussions.comment.comments.reason": "Reason",
"discussions.post.closedBy": "Post closed by",
"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 }",
@@ -68,7 +28,10 @@
"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",
@@ -109,6 +72,44 @@
"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 }",

View File

@@ -1,46 +1,6 @@
{
"navigation.course.tabs.label": "Materiale del corso",
"learn.course.tabs.navigation.overflow.menu": "Altro... ",
"discussions.comments.comment.addComment": "Add comment",
"discussions.comments.comment.addResponse": "Aggiungi una risposta",
"discussions.comments.comment.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.fullStop": "•",
"discussions.comment.comments.reason": "Motivo ",
"discussions.post.closedBy": "Post chiuso da",
"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 }",
@@ -68,7 +28,10 @@
"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",
@@ -109,6 +72,44 @@
"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 }",

View File

@@ -1,46 +1,6 @@
{
"navigation.course.tabs.label": "Course Material",
"learn.course.tabs.navigation.overflow.menu": "More...",
"discussions.comments.comment.addComment": "Add comment",
"discussions.comments.comment.addResponse": "Dodaj odpowiedź",
"discussions.comments.comment.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.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.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 }",
@@ -68,7 +28,10 @@
"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",
@@ -109,6 +72,44 @@
"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 }",

View File

@@ -1,57 +1,17 @@
{
"navigation.course.tabs.label": "Ders Materyali",
"learn.course.tabs.navigation.overflow.menu": "Daha Fazlası...",
"discussions.comments.comment.addComment": "Yorum ekle",
"discussions.comments.comment.addResponse": "Bir cevap ekle",
"discussions.comments.comment.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.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.topics.backAlt": "Back to topics list",
"discussions.topics.backAlt": "Konular listesine dön",
"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": "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.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.archived.label": "Arşivlenmiş",
"discussions.learner.reported": "{reported} rapor edildi",
@@ -68,14 +28,17 @@
"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": "Bildirme",
"discussions.actions.unreport": "Raporlamaktan vazgeç",
"discussions.actions.endorse": "Destekle",
"discussions.actions.unendorse": "Destekleme",
"discussions.actions.unendorse": "Desteklemekten vazgeç",
"discussions.actions.markAnswered": "Cevaplandı olarak işaretle",
"discussions.actions.unMarkAnswered": "Cevaplandı olarak işaretini kaldır",
"discussions.modal.confirmation.button.cancel": "İptal",
@@ -99,7 +62,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": "Posting in discussions is temporarily disabled by the course team",
"discussion.blackoutBanner.information": "Tartışmalarda ileti yayınlama, ders ekibi tarafından geçici olarak devre dışı bırakıldı",
"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",
@@ -109,6 +72,44 @@
"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 }",
@@ -141,7 +142,7 @@
"discussions.post.editor.anonymousPost": "Anonim olarak gönder",
"discussions.post.editor.anonymousToPeersPost": "Akranlarına anonim olarak gönder",
"discussions.editor.posts.editReasonCode": "Düzenleme nedeni",
"discussions.editor.posts.showPreview.button": "Show preview",
"discussions.editor.posts.showPreview.button": "Önizlemeyi göster",
"discussions.topic.noName.label": "İsimsiz kategori",
"discussions.subtopic.noName.label": "İsimsiz alt kategori",
"discussions.posts.filter.showALl": "Tümünü göster",
@@ -163,20 +164,20 @@
"discussions.posts.sort.voteCount": "En çok beğenilenler",
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
"discussions.post.author.anonymous": "anonim",
"discussions.post.addResponse": "Add response",
"discussions.post.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": "Followed",
"discussions.post.notFollowed": "Not Followed",
"discussions.post.followed": "İzlendi",
"discussions.post.notFollowed": "İzlenmedi",
"discussions.post.answered": "Yanıtlandı",
"discussions.post.unFollow": "Takibi bırak",
"discussions.post.like": "Beğen",
"discussions.post.removeLike": "Beğenmeme",
"discussions.post.liked": "liked",
"discussions.post.likes": "likes",
"discussions.post.liked": "beğendi",
"discussions.post.likes": "beğeni",
"discussions.post.viewActivity": "Etkinliği görüntüle",
"discussions.post.activity": "Etkinlik",
"discussions.post.closed": "Yanıtlar ve yorumlar için gönderi kapatıldı",
@@ -195,8 +196,8 @@
"discussions.post.editedBy": "Düzenleyen",
"discussions.post.editReason": "Gerekçe",
"discussions.post.postWithoutPreview": "Önizleme yok",
"discussions.post.follow.description": "you are following this post",
"discussions.post.unfollow.description": "you are not following this post",
"discussions.post.follow.description": "bu iletiyi izliyorsunuz",
"discussions.post.unfollow.description": "bu iletiyi izlemiyorsunuz",
"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",
@@ -204,8 +205,8 @@
"discussions.topics.unnamed.label": "İsimsiz kategori",
"discussions.subtopics.unnamed.label": "İsimsiz alt kategori",
"tour.action.advance": "Sonraki",
"tour.action.dismiss": "Dismiss",
"tour.action.dismiss": "İptal",
"tour.action.end": "Tamam",
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
"tour.title.notRespondedFilter": "New filtering option!"
"tour.body.notRespondedFilter": "Artık yanıt vermeyen iletileri bulmak için tartışmaları filtreleyebilirsiniz.",
"tour.title.notRespondedFilter": "Yeni filtreleme seçeneği!"
}

View File

@@ -1,46 +1,6 @@
{
"navigation.course.tabs.label": "Course Material",
"learn.course.tabs.navigation.overflow.menu": "More...",
"discussions.comments.comment.addComment": "Add comment",
"discussions.comments.comment.addResponse": "Add a response",
"discussions.comments.comment.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.fullStop": "•",
"discussions.comment.comments.reason": "Reason",
"discussions.post.closedBy": "Post closed by",
"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 }",
@@ -68,7 +28,10 @@
"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",
@@ -109,6 +72,44 @@
"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 }",

View File

@@ -433,7 +433,7 @@ header {
pointer-events: none;
}
.on-focus:focus-visible {
.on-focus:focus-within {
outline: 2px solid black;
}
@@ -442,6 +442,8 @@ header {
}
.post-card-comment {
outline: none;
&:not(:hover),
&:not(:focus) {
.hover-card {
@@ -450,7 +452,7 @@ header {
}
&:hover,
&:focus {
&:focus-within {
.hover-card {
display: flex;
}

View File

@@ -1,4 +1,5 @@
/* eslint-disable import/prefer-default-export */
export const executeThunk = async (thunk, dispatch, getState) => {
await thunk(dispatch, getState);
await new Promise(setImmediate);