Compare commits
37 Commits
inf-890
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dafb6ad82 | ||
|
|
e9e3db5193 | ||
|
|
cbb35e7cad | ||
|
|
cf8ef159e0 | ||
|
|
eb127cd8e1 | ||
|
|
68841d03ee | ||
|
|
67212254f7 | ||
|
|
af5b10a575 | ||
|
|
fb2be35d00 | ||
|
|
10adf1171b | ||
|
|
2609380bd8 | ||
|
|
61d0f9a7ea | ||
|
|
ea235cf6ca | ||
|
|
b8f11c3046 | ||
|
|
1769692d22 | ||
|
|
3d6b71c247 | ||
|
|
bd42521f6b | ||
|
|
445caca4e4 | ||
|
|
4a2b32494d | ||
|
|
a16bd783a0 | ||
|
|
df1a16ee85 | ||
|
|
656935336e | ||
|
|
2498f74556 | ||
|
|
99ad3aff53 | ||
|
|
e2bb68a1cd | ||
|
|
f694b480b5 | ||
|
|
228a771a39 | ||
|
|
c821033a64 | ||
|
|
7ce4566df3 | ||
|
|
a02771f96f | ||
|
|
8c53a7a19e | ||
|
|
c8500a0c1e | ||
|
|
f7ad94997d | ||
|
|
733a74d9e4 | ||
|
|
b2b33b76f7 | ||
|
|
70f6541585 | ||
|
|
822854953f |
5
.env
5
.env
@@ -20,6 +20,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
SUPPORT_URL=''
|
||||
TA_FEEDBACK_FORM= ''
|
||||
STAFF_FEEDBACK_FORM= ''
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
STAFF_FEEDBACK_URL=''
|
||||
|
||||
@@ -21,6 +21,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SUPPORT_URL='https://support.edx.org'
|
||||
TA_FEEDBACK_FORM='https://learner-form.test'
|
||||
STAFF_FEEDBACK_FORM='https://staff-form.test'
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
STAFF_FEEDBACK_URL=''
|
||||
|
||||
@@ -19,6 +19,5 @@ SEGMENT_KEY=''
|
||||
SITE_NAME='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SUPPORT_URL='https://support.edx.org'
|
||||
TA_FEEDBACK_FORM='https://learner-form.test'
|
||||
STAFF_FEEDBACK_FORM='https://staff-form.test'
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
STAFF_FEEDBACK_URL=''
|
||||
|
||||
14
.eslintrc.js
14
.eslintrc.js
@@ -1,9 +1,10 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint',
|
||||
{
|
||||
"plugins": ["simple-import-sort"],
|
||||
"rules": {
|
||||
module.exports = createConfig(
|
||||
'eslint',
|
||||
{
|
||||
plugins: ['simple-import-sort'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'jsx-a11y/no-noninteractive-element-interactions': 'off',
|
||||
@@ -25,7 +26,6 @@ module.exports = createConfig('eslint',
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -9,18 +9,17 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Validate package-lock.json changes
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
process.env.TA_FEEDBACK_FORM= 'https://learner-form.test';
|
||||
process.env.STAFF_FEEDBACK_FORM= 'https://staff-form.test';
|
||||
18
Makefile
18
Makefile
@@ -1,7 +1,8 @@
|
||||
export TRANSIFEX_RESOURCE = frontend-app-discussions
|
||||
transifex_resource = frontend-app-discussions
|
||||
transifex_langs = "ar,fr,es_419,zh_CN,tr_TR,pl,fr_CA,fr_FR,de_DE,it_IT"
|
||||
transifex_langs = "ar,fr,es_419,zh_CN,tr_TR,pl,fr_CA,fr_FR,de_DE,it_IT,pt_PT,uk,ru,hi,cs,es_AR,es_ES,fa_IR"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
@@ -55,9 +56,24 @@ push_translations:
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-app-discussions/src/i18n/messages:frontend-app-discussions
|
||||
|
||||
$(intl_imports) frontend-component-header frontend-component-footer paragon frontend-app-discussions
|
||||
endif
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
20
README.rst
20
README.rst
@@ -1,10 +1,15 @@
|
||||
|Codecov| |license|
|
||||
|
||||
.. |Codecov| image:: https://codecov.io/gh/openedx/frontend-app-discussions/branch/master/graph/badge.svg?token=3z7XvuzTq3
|
||||
:target: https://codecov.io/gh/openedx/frontend-app-discussions
|
||||
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
|
||||
:target: https://github.com/openedx/frontend-app-discussions/blob/master/LICENSE
|
||||
|
||||
frontend-app-discussions
|
||||
========================
|
||||
|
||||
|Build Status| |Codecov| |license|
|
||||
|
||||
Purpose
|
||||
-------
|
||||
Introduction
|
||||
------------
|
||||
|
||||
This repository is a React-based micro frontend for the Open edX discussion forums.
|
||||
|
||||
@@ -79,10 +84,3 @@ Internationalization
|
||||
--------------------
|
||||
|
||||
Please see `edx/frontend-platform's i18n module <https://edx.github.io/frontend-platform/module-Internationalization.html>`_ for documentation on internationalization. The documentation explains how to use it, and the `How To <https://github.com/openedx/frontend-i18n/blob/master/docs/how_tos/i18n.rst>`_ has more detail.
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-discussions.svg?branch=master
|
||||
:target: https://travis-ci.org/edx/frontend-app-discussions
|
||||
.. |Codecov| image:: https://codecov.io/gh/edx/frontend-app-discussions/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/edx/frontend-app-discussions
|
||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-discussions.svg
|
||||
:target: @edx/frontend-app-discussions
|
||||
@@ -1,9 +1,9 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
|
||||
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.
|
||||
// If you want to add config BEFORE jest loads, use setupFiles instead.
|
||||
setupFiles: ['<rootDir>/.jest/setEnvVars.js'],
|
||||
setupFiles: ['<rootDir>/.env.test'],
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
|
||||
32822
package-lock.json
generated
32822
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -34,10 +34,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "11.2.0",
|
||||
"@edx/frontend-component-header": "3.2.0",
|
||||
"@edx/frontend-platform": "2.6.1",
|
||||
"@edx/paragon": "20.15.0",
|
||||
"@edx/frontend-component-footer": "12.1.0",
|
||||
"@edx/frontend-component-header": "4.3.0",
|
||||
"@edx/frontend-platform": "4.6.0",
|
||||
"@edx/paragon": "20.44.0",
|
||||
"@reduxjs/toolkit": "1.8.0",
|
||||
"@tinymce/tinymce-react": "3.13.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
@@ -48,9 +48,9 @@
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"raw-loader": "4.0.2",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-redux": "7.2.6",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"redux": "4.1.2",
|
||||
@@ -61,10 +61,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.1.0",
|
||||
"@edx/frontend-build": "11.0.1",
|
||||
"@edx/frontend-build": "12.8.27",
|
||||
"@edx/reactifex": "1.0.3",
|
||||
"@testing-library/jest-dom": "5.16.2",
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"axios-mock-adapter": "1.20.0",
|
||||
"babel-plugin-react-intl": "8.2.25",
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
"config:base",
|
||||
"schedule:weekly",
|
||||
":automergeLinters",
|
||||
":automergeMinor",
|
||||
":automergeTesters",
|
||||
":enableVulnerabilityAlerts",
|
||||
":rebaseStalePrs",
|
||||
":semanticCommits",
|
||||
":updateNotScheduled"
|
||||
],
|
||||
"patch": {
|
||||
"automerge": true
|
||||
},
|
||||
"rebaseStalePrs": true
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": [
|
||||
"devDependencies"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"lockFileMaintenance",
|
||||
"minor",
|
||||
"patch",
|
||||
"pin"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
|
||||
@@ -19,19 +19,21 @@ import { selectCourseCohorts } from '../discussions/cohorts/data/selectors';
|
||||
import messages from '../discussions/posts/post-filter-bar/messages';
|
||||
import { ActionItem } from '../discussions/posts/post-filter-bar/PostFilterBar';
|
||||
|
||||
function FilterBar({
|
||||
const FilterBar = ({
|
||||
intl,
|
||||
filters,
|
||||
selectedFilters,
|
||||
onFilterChange,
|
||||
showCohortsFilter,
|
||||
}) {
|
||||
}) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const { status } = useSelector(state => state.cohorts);
|
||||
const selectedCohort = useMemo(() => cohorts.find(cohort => (
|
||||
toString(cohort.id) === selectedFilters.cohort)),
|
||||
[selectedFilters.cohort]);
|
||||
const selectedCohort = useMemo(
|
||||
() => cohorts.find(cohort => (
|
||||
toString(cohort.id) === selectedFilters.cohort)),
|
||||
[selectedFilters.cohort],
|
||||
);
|
||||
|
||||
const allFilters = [
|
||||
{
|
||||
@@ -183,7 +185,7 @@ function FilterBar({
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
FilterBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -24,25 +24,23 @@ const CourseTabsNavigation = ({
|
||||
}, [courseId]);
|
||||
|
||||
return (
|
||||
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation', className)}>
|
||||
<div className="container-xl">
|
||||
{!!tabs.length && (
|
||||
<Tabs
|
||||
className="nav-underline-tabs"
|
||||
aria-label={intl.formatMessage(messages.courseMaterial)}
|
||||
>
|
||||
{tabs.map(({ url, title, slug }) => (
|
||||
<a
|
||||
key={slug}
|
||||
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTab })}
|
||||
href={url}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation px-4', className)}>
|
||||
{!!tabs.length && (
|
||||
<Tabs
|
||||
className="nav-underline-tabs"
|
||||
aria-label={intl.formatMessage(messages.courseMaterial)}
|
||||
>
|
||||
{tabs.map(({ url, title, slug }) => (
|
||||
<a
|
||||
key={slug}
|
||||
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTab })}
|
||||
href={url}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,8 +56,10 @@ describe('Navigation bar api tests', () => {
|
||||
});
|
||||
|
||||
it('Denied to get navigation bar when user has no access on course', async () => {
|
||||
axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(200,
|
||||
(Factory.build('navigationBar', 1, { hasCourseAccess: false })));
|
||||
axiosMock.onGet(`${getCourseMetadataApiUrl(courseId)}`).reply(
|
||||
200,
|
||||
(Factory.build('navigationBar', 1, { hasCourseAccess: false })),
|
||||
);
|
||||
await executeThunk(fetchTab(courseId, 'outline'), store.dispatch, store.getState);
|
||||
|
||||
expect(store.getState().courseTabs.courseStatus).toEqual('denied');
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@import "@edx/brand/paragon/fonts.scss";
|
||||
@import "@edx/brand/paragon/variables.scss";
|
||||
@import "@edx/paragon/scss/core/core.scss";
|
||||
@import "@edx/brand/paragon/overrides.scss";
|
||||
@import "~@edx/brand/paragon/fonts.scss";
|
||||
@import "~@edx/brand/paragon/variables.scss";
|
||||
@import "~@edx/paragon/scss/core/core.scss";
|
||||
@import "~@edx/brand/paragon/overrides.scss";
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
.course-tabs-navigation {
|
||||
border-bottom: solid 1px #eaeaea;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Dropdown } from '@edx/paragon';
|
||||
|
||||
import useIndexOfLastVisibleChild from './useIndexOfLastVisibleChild';
|
||||
|
||||
export default function Tabs({ children, className, ...attrs }) {
|
||||
const Tabs = ({ children, className, ...attrs }) => {
|
||||
const [
|
||||
indexOfLastVisibleChild,
|
||||
containerElementRef,
|
||||
@@ -31,25 +31,28 @@ export default function Tabs({ children, className, ...attrs }) {
|
||||
|
||||
// Insert the overflow menu at the cut off index (even if it will be hidden
|
||||
// it so it can be part of measurements)
|
||||
wrappedChildren.splice(indexOfOverflowStart, 0, (
|
||||
<div
|
||||
className="nav-item flex-shrink-0"
|
||||
style={indexOfOverflowStart >= React.Children.count(children) ? invisibleStyle : null}
|
||||
ref={overflowElementRef}
|
||||
key="overflow"
|
||||
>
|
||||
<Dropdown className="h-100">
|
||||
<Dropdown.Toggle variant="link" className="nav-link h-100" id="learn.course.tabs.navigation.overflow.menu">
|
||||
<FormattedMessage
|
||||
id="learn.course.tabs.navigation.overflow.menu"
|
||||
description="The title of the overflow menu for course tabs"
|
||||
defaultMessage="More..."
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">{overflowChildren}</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
));
|
||||
wrappedChildren.splice(
|
||||
indexOfOverflowStart,
|
||||
0, (
|
||||
<div
|
||||
className="nav-item flex-shrink-0"
|
||||
style={indexOfOverflowStart >= React.Children.count(children) ? invisibleStyle : null}
|
||||
ref={overflowElementRef}
|
||||
key="overflow"
|
||||
>
|
||||
<Dropdown className="h-100">
|
||||
<Dropdown.Toggle variant="link" className="nav-link h-100" id="learn.course.tabs.navigation.overflow.menu">
|
||||
<FormattedMessage
|
||||
id="learn.course.tabs.navigation.overflow.menu"
|
||||
description="The title of the overflow menu for course tabs"
|
||||
defaultMessage="More..."
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">{overflowChildren}</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
return wrappedChildren;
|
||||
}, [children, indexOfLastVisibleChild]);
|
||||
|
||||
@@ -62,7 +65,7 @@ export default function Tabs({ children, className, ...attrs }) {
|
||||
{tabChildren}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Tabs.propTypes = {
|
||||
children: PropTypes.node,
|
||||
@@ -73,3 +76,5 @@ Tabs.defaultProps = {
|
||||
children: null,
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useState,
|
||||
useCallback, useContext, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
|
||||
import camelCase from 'lodash/camelCase';
|
||||
@@ -25,6 +25,7 @@ const Search = () => {
|
||||
const isPostSearch = ['posts', 'my-posts'].includes(page);
|
||||
const isTopicSearch = 'topics'.includes(page);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const previousSearchValueRef = useRef('');
|
||||
let currentValue = '';
|
||||
|
||||
if (isPostSearch) {
|
||||
@@ -39,14 +40,15 @@ const Search = () => {
|
||||
dispatch(setSearchQuery(''));
|
||||
dispatch(setTopicFilter(''));
|
||||
dispatch(setUsernameSearch(''));
|
||||
}, []);
|
||||
previousSearchValueRef.current = '';
|
||||
}, [previousSearchValueRef]);
|
||||
|
||||
const onChange = useCallback((query) => {
|
||||
setSearchValue(query);
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback((query) => {
|
||||
if (query === '') {
|
||||
if (query === '' || query === previousSearchValueRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,7 +59,8 @@ const Search = () => {
|
||||
} else if (page === 'learners') {
|
||||
dispatch(setUsernameSearch(query));
|
||||
}
|
||||
}, [page, searchValue]);
|
||||
previousSearchValueRef.current = query;
|
||||
}, [page, searchValue, previousSearchValueRef]);
|
||||
|
||||
const handleIconClick = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useLocation, useParams } from 'react-router';
|
||||
// TinyMCE so the global var exists
|
||||
// eslint-disable-next-line no-unused-vars,import/no-extraneous-dependencies
|
||||
import tinymce from 'tinymce/tinymce';
|
||||
@@ -42,13 +42,14 @@ import contentCss from '!!raw-loader!tinymce/skins/content/default/content.min.c
|
||||
import contentUiCss from '!!raw-loader!tinymce/skins/ui/oxide/content.min.css';
|
||||
|
||||
/* istanbul ignore next */
|
||||
function TinyMCEEditor(props) {
|
||||
const TinyMCEEditor = (props) => {
|
||||
// note that skin and content_css is disabled to avoid the normal
|
||||
// loading process and is instead loaded as a string via content_style
|
||||
|
||||
const locationObj = useLocation();
|
||||
const { courseId, postId } = useParams();
|
||||
const [showImageWarning, setShowImageWarning] = useState(false);
|
||||
const intl = useIntl();
|
||||
const enableInContextSidebar = Boolean(new URLSearchParams(locationObj.search).get('inContextSidebar') !== null);
|
||||
|
||||
/* istanbul ignore next */
|
||||
const setup = useCallback((editor) => {
|
||||
@@ -99,6 +100,29 @@ function TinyMCEEditor(props) {
|
||||
contentStyle = '';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
useEffect(() => {
|
||||
if (enableInContextSidebar) {
|
||||
const checkToxDialogVisibility = () => {
|
||||
const toxDialog = document.querySelector('.tox-dialog');
|
||||
if (toxDialog) {
|
||||
toxDialog.style.alignSelf = 'start';
|
||||
toxDialog.style.marginTop = '50px';
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(checkToxDialogVisibility);
|
||||
|
||||
// Observe changes to the entire document
|
||||
observer.observe(document, { childList: true, subtree: true });
|
||||
|
||||
// Clean up the observer when the component unmounts
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}
|
||||
}, [enableInContextSidebar]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Editor
|
||||
@@ -152,6 +176,6 @@ function TinyMCEEditor(props) {
|
||||
</AlertModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default React.memo(TinyMCEEditor);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function InsertLink() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function Issue() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function People() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function PushPin() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function Question() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function QuestionAnswer() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function QuestionAnswerOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function StarFilled() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function StarOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function ThumbUpFilled() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function ThumbUpOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export const getApiBaseUrl = () => getConfig().LMS_BASE_URL;
|
||||
export const getFullUrl = (path) => (
|
||||
new URL(`${getConfig().PUBLIC_PATH.replace(/\/$/, '')}/${path}`, window.location.origin).href
|
||||
);
|
||||
|
||||
/**
|
||||
* Enum for thread types.
|
||||
|
||||
@@ -13,23 +13,23 @@ import {
|
||||
import { MoreHoriz } from '@edx/paragon/icons';
|
||||
|
||||
import { ContentActions } from '../../data/constants';
|
||||
import { selectBlackoutDate } from '../data/selectors';
|
||||
import { selectIsPostingEnabled } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { inBlackoutDateRange, useActions } from '../utils';
|
||||
import { useActions } from '../utils';
|
||||
|
||||
function ActionsDropdown({
|
||||
const ActionsDropdown = ({
|
||||
actionHandlers,
|
||||
contentType,
|
||||
disabled,
|
||||
dropDownIconSize,
|
||||
iconSize,
|
||||
id,
|
||||
}) {
|
||||
}) => {
|
||||
const buttonRef = useRef();
|
||||
const intl = useIntl();
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const actions = useActions(contentType, id);
|
||||
|
||||
const handleActions = useCallback((action) => {
|
||||
@@ -41,12 +41,12 @@ function ActionsDropdown({
|
||||
}
|
||||
}, [actionHandlers]);
|
||||
|
||||
// Find and remove edit action if in blackout date range.
|
||||
// Find and remove edit action if in Posting is disabled.
|
||||
useMemo(() => {
|
||||
if (inBlackoutDateRange(blackoutDateRange)) {
|
||||
if (!isPostingEnabled) {
|
||||
actions.splice(actions.findIndex(action => action.id === 'edit'), 1);
|
||||
}
|
||||
}, [actions, blackoutDateRange]);
|
||||
}, [actions, isPostingEnabled]);
|
||||
|
||||
const onClickButton = useCallback(() => {
|
||||
setTarget(buttonRef.current);
|
||||
@@ -109,7 +109,7 @@ function ActionsDropdown({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ActionsDropdown.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
|
||||
@@ -13,6 +13,8 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { ContentActions } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import messages from '../messages';
|
||||
import { getCommentsApiUrl } from '../post-comments/data/api';
|
||||
import { addComment, fetchThreadComments } from '../post-comments/data/thunks';
|
||||
@@ -29,6 +31,7 @@ let store;
|
||||
let axiosMock;
|
||||
const commentsApiUrl = getCommentsApiUrl();
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const discussionThreadId = 'thread-1';
|
||||
const questionThreadId = 'thread-2';
|
||||
const commentContent = 'This is a comment for thread-1';
|
||||
@@ -170,7 +173,7 @@ const findOpenActionsDropdownButton = async () => (
|
||||
);
|
||||
|
||||
describe('ActionsDropdown', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
@@ -182,6 +185,11 @@ describe('ActionsDropdown', () => {
|
||||
store = initializeStore();
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
|
||||
axiosMock.onGet(`${getCourseConfigApiUrl()}${courseId}/`)
|
||||
.reply(200, { isPostingEnabled: true });
|
||||
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
});
|
||||
|
||||
it.each(Object.values(buildTestContent()))('can open drop down if enabled', async (commentOrPost) => {
|
||||
|
||||
@@ -51,7 +51,7 @@ const AuthorLabel = ({
|
||||
|
||||
const authorName = useMemo(() => (
|
||||
<span
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500 author-name', {
|
||||
'text-gray-700': isRetiredUser,
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser,
|
||||
})}
|
||||
@@ -100,7 +100,7 @@ const AuthorLabel = ({
|
||||
{postCreatedAt && (
|
||||
<span
|
||||
title={postCreatedAt}
|
||||
className={classNames('font-family-inter align-content-center', {
|
||||
className={classNames('align-content-center', {
|
||||
'text-white': alert,
|
||||
'text-gray-500': !alert,
|
||||
})}
|
||||
|
||||
@@ -66,19 +66,20 @@ describe('Author label', () => {
|
||||
['retired__user', null, false, ''],
|
||||
['staff_user', 'Staff', true, 'text-staff-color'],
|
||||
['learner_user', null, false, ''],
|
||||
])('for %s', (
|
||||
author, authorLabel, linkToProfile, labelColor,
|
||||
) => {
|
||||
it('it has author name text',
|
||||
])('for %s', (author, authorLabel, linkToProfile, labelColor) => {
|
||||
it(
|
||||
'it has author name text',
|
||||
async () => {
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor);
|
||||
const authorElement = container.querySelector('[role=heading]');
|
||||
const authorName = author.startsWith('retired__user') ? '[Deactivated]' : author;
|
||||
|
||||
expect(authorElement).toHaveTextContent(authorName);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(`it is "${!linkToProfile && 'not'}" clickable when linkToProfile is ${!!linkToProfile}`,
|
||||
it(
|
||||
`it is "${!linkToProfile && 'not'}" clickable when linkToProfile is ${!!linkToProfile}`,
|
||||
async () => {
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor);
|
||||
|
||||
@@ -87,9 +88,11 @@ describe('Author label', () => {
|
||||
} else {
|
||||
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it(`it has "${!linkToProfile && 'not'}" label text and label color when linkToProfile is ${!!linkToProfile}`,
|
||||
it(
|
||||
`it has "${!linkToProfile && 'not'}" label text and label color when linkToProfile is ${!!linkToProfile}`,
|
||||
async () => {
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor);
|
||||
const authorElement = container.querySelector('[role=heading]');
|
||||
@@ -104,6 +107,7 @@ describe('Author label', () => {
|
||||
expect(authorElement.parentNode.lastChild).not.toHaveTextContent(label, { exact: true });
|
||||
expect(authorElement.parentNode).not.toHaveClass(labelColor, { exact: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ActionRow, Button, ModalDialog } from '@edx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function Confirmation({
|
||||
const Confirmation = ({
|
||||
isOpen,
|
||||
title,
|
||||
description,
|
||||
@@ -15,7 +15,7 @@ function Confirmation({
|
||||
closeButtonVaraint,
|
||||
confirmButtonVariant,
|
||||
confirmButtonText,
|
||||
}) {
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
@@ -40,7 +40,7 @@ function Confirmation({
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Confirmation.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -13,12 +13,12 @@ import { PostCommentsContext } from '../post-comments/postCommentsContext';
|
||||
import AuthorLabel from './AuthorLabel';
|
||||
import timeLocale from './time-locale';
|
||||
|
||||
function EndorsedAlertBanner({
|
||||
const EndorsedAlertBanner = ({
|
||||
endorsed,
|
||||
endorsedAt,
|
||||
endorsedBy,
|
||||
endorsedByLabel,
|
||||
}) {
|
||||
}) => {
|
||||
timeago.register('time-locale', timeLocale);
|
||||
|
||||
const intl = useIntl();
|
||||
@@ -43,7 +43,7 @@ function EndorsedAlertBanner({
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
<strong className="ml-2 font-family-inter">
|
||||
<strong className="ml-2">
|
||||
{intl.formatMessage(isQuestion ? messages.answer : messages.endorsed)}
|
||||
</strong>
|
||||
</div>
|
||||
@@ -62,7 +62,7 @@ function EndorsedAlertBanner({
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
EndorsedAlertBanner.propTypes = {
|
||||
endorsed: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -22,9 +22,7 @@ function buildTestContent(type, buildParams) {
|
||||
return camelCaseObject(Factory.build(type, { ...buildParamsSnakeCase }, null));
|
||||
}
|
||||
|
||||
function renderComponent(
|
||||
content, postType,
|
||||
) {
|
||||
const renderComponent = (content, postType) => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
@@ -47,7 +45,7 @@ function renderComponent(
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
describe.each([
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import {
|
||||
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
|
||||
} from '../../components/icons';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../data/hooks';
|
||||
import { useUserPostingEnabled } from '../data/hooks';
|
||||
import { PostCommentsContext } from '../post-comments/postCommentsContext';
|
||||
import ActionsDropdown from './ActionsDropdown';
|
||||
import { DiscussionContext } from './context';
|
||||
@@ -31,7 +31,7 @@ const HoverCard = ({
|
||||
const intl = useIntl();
|
||||
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const { isClosed } = useContext(PostCommentsContext);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -39,12 +39,14 @@ const HoverCard = ({
|
||||
data-testid={`hover-card-${id}`}
|
||||
id={`hover-card-${id}`}
|
||||
>
|
||||
{userCanAddThreadInBlackoutDate && (
|
||||
{isUserPrivilagedInPostingRestriction && (
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className={classNames('px-2.5 py-2 border-0 font-style text-gray-700 font-size-12',
|
||||
{ 'w-100': enableInContextSidebar })}
|
||||
className={classNames(
|
||||
'px-2.5 py-2 border-0 font-style text-gray-700 font-size-12',
|
||||
{ 'w-100': enableInContextSidebar },
|
||||
)}
|
||||
onClick={() => handleResponseCommentButton()}
|
||||
disabled={isClosed}
|
||||
style={{ lineHeight: '20px' }}
|
||||
@@ -125,6 +127,7 @@ HoverCard.propTypes = {
|
||||
addResponseCommentButtonMessage: PropTypes.string.isRequired,
|
||||
onLike: PropTypes.func.isRequired,
|
||||
voted: PropTypes.bool.isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
endorseIcons: PropTypes.objectOf(PropTypes.any),
|
||||
onFollow: PropTypes.func,
|
||||
following: PropTypes.bool,
|
||||
|
||||
@@ -6,15 +6,17 @@ import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
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 { getCourseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getCommentsApiUrl } from '../post-comments/data/api';
|
||||
import { fetchCommentResponses, fetchThreadComments } from '../post-comments/data/thunks';
|
||||
import { fetchCommentResponses } from '../post-comments/data/thunks';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import { DiscussionContext } from './context';
|
||||
@@ -25,45 +27,11 @@ import '../post-comments/data/__factories__';
|
||||
const commentsApiUrl = getCommentsApiUrl();
|
||||
const threadsApiUrl = getThreadsApiUrl();
|
||||
const discussionPostId = 'thread-1';
|
||||
const questionPostId = 'thread-2';
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const reverseOrder = true;
|
||||
const enableInContextSidebar = false;
|
||||
let store;
|
||||
let axiosMock;
|
||||
let container;
|
||||
|
||||
async function mockAxiosReturnPagedComments() {
|
||||
const endorsedArray = [null, false, true];
|
||||
const pageArray = [1, 2];
|
||||
|
||||
endorsedArray.forEach(async (endorsed) => {
|
||||
const postId = endorsed === null ? discussionPostId : questionPostId;
|
||||
pageArray.forEach(async (page) => {
|
||||
const params = {
|
||||
thread_id: postId,
|
||||
page,
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
endorsed,
|
||||
reverse_order: reverseOrder,
|
||||
enable_in_context_sidebar: enableInContextSidebar,
|
||||
signal: {},
|
||||
};
|
||||
axiosMock.onGet(commentsApiUrl, { ...params }).reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
threadId: postId,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
endorsed,
|
||||
childCount: page === 1 ? 2 : 0,
|
||||
}));
|
||||
|
||||
await executeThunk(fetchThreadComments(postId, { ...params }), store.dispatch, store.getState);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function mockAxiosReturnPagedCommentsResponses() {
|
||||
const parentId = 'comment-1';
|
||||
const commentsResponsesApiUrl = `${commentsApiUrl}${parentId}/`;
|
||||
@@ -75,13 +43,15 @@ async function mockAxiosReturnPagedCommentsResponses() {
|
||||
};
|
||||
|
||||
[1, 2].forEach(async (page) => {
|
||||
axiosMock.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } }).reply(200,
|
||||
axiosMock.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } }).reply(
|
||||
200,
|
||||
Factory.build('commentsResult', null, {
|
||||
parentId,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
await executeThunk(fetchCommentResponses(parentId), store.dispatch, store.getState);
|
||||
});
|
||||
@@ -116,6 +86,7 @@ describe('HoverCard', () => {
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
isPostingEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -123,26 +94,17 @@ describe('HoverCard', () => {
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(threadsApiUrl).reply(200, Factory.build('threadsResult'));
|
||||
axiosMock.onPatch(new RegExp(`${commentsApiUrl}*`)).reply(({ url, data }) => {
|
||||
const commentId = url.match(/comments\/(?<id>[a-z1-9-]+)\//).groups.id;
|
||||
const { rawBody } = camelCaseObject(JSON.parse(data));
|
||||
return [200, Factory.build('comment', {
|
||||
id: commentId,
|
||||
rendered_body: rawBody,
|
||||
raw_body: rawBody,
|
||||
})];
|
||||
});
|
||||
axiosMock.onPost(commentsApiUrl).reply(({ data }) => {
|
||||
const { rawBody, threadId } = camelCaseObject(JSON.parse(data));
|
||||
return [200, Factory.build('comment', {
|
||||
rendered_body: rawBody,
|
||||
raw_body: rawBody,
|
||||
thread_id: threadId,
|
||||
})];
|
||||
});
|
||||
axiosMock.onGet(`${getCourseConfigApiUrl()}${courseId}/`).reply(200, { isPostingEnabled: true });
|
||||
axiosMock.onGet(commentsApiUrl).reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
threadId: discussionPostId,
|
||||
endorsed: false,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
childCount: 2,
|
||||
}));
|
||||
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||
await mockAxiosReturnPagedComments();
|
||||
await mockAxiosReturnPagedCommentsResponses();
|
||||
});
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@ import tourCheckpoints from '../tours/constants';
|
||||
import { selectTours } from '../tours/data/selectors';
|
||||
import { updateTourShowStatus } from '../tours/data/thunks';
|
||||
import messages from '../tours/messages';
|
||||
import { discussionsPath, inBlackoutDateRange } from '../utils';
|
||||
import { discussionsPath } from '../utils';
|
||||
import {
|
||||
selectAreThreadsFiltered,
|
||||
selectBlackoutDate,
|
||||
selectEnableInContext,
|
||||
selectIsCourseAdmin,
|
||||
selectIsCourseStaff,
|
||||
selectIsPostingEnabled,
|
||||
selectLearnersTabEnabled,
|
||||
selectModerationSettings,
|
||||
selectPostThreadCount,
|
||||
@@ -40,12 +40,14 @@ import { fetchCourseConfig } from './thunks';
|
||||
|
||||
export function useTotalTopicThreadCount() {
|
||||
const topics = useSelector(selectTopics);
|
||||
const count = useMemo(() => (
|
||||
Object.keys(topics)?.reduce((total, topicId) => {
|
||||
const topic = topics[topicId];
|
||||
return total + topic.threadCounts.discussion + topic.threadCounts.question;
|
||||
}, 0)),
|
||||
[]);
|
||||
const count = useMemo(
|
||||
() => (
|
||||
Object.keys(topics)?.reduce((total, topicId) => {
|
||||
const topic = topics[topicId];
|
||||
return total + topic.threadCounts.discussion + topic.threadCounts.question;
|
||||
}, 0)),
|
||||
[],
|
||||
);
|
||||
|
||||
return count;
|
||||
}
|
||||
@@ -190,17 +192,16 @@ export const useCurrentDiscussionTopic = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useUserCanAddThreadInBlackoutDate = () => {
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
export const useUserPostingEnabled = () => {
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const isUserAdmin = useSelector(selectUserIsStaff);
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const isUserGroupTA = useSelector(selectUserIsGroupTa);
|
||||
const isCourseAdmin = useSelector(selectIsCourseAdmin);
|
||||
const isCourseStaff = useSelector(selectIsCourseStaff);
|
||||
const isPrivileged = isUserAdmin || userHasModerationPrivileges || isUserGroupTA || isCourseAdmin || isCourseStaff;
|
||||
const isInBlackoutDateRange = useMemo(() => inBlackoutDateRange(blackoutDateRange), [blackoutDateRange]);
|
||||
|
||||
return (!(isInBlackoutDateRange) || (isPrivileged));
|
||||
return (isPostingEnabled || isPrivileged);
|
||||
};
|
||||
|
||||
function camelToConstant(string) {
|
||||
@@ -222,7 +223,7 @@ export const useTourConfiguration = () => {
|
||||
), []);
|
||||
|
||||
const toursConfig = useMemo(() => (
|
||||
tours?.map((tour) => (
|
||||
tours?.map((tour) => Object.keys(tourCheckpoints(intl)).includes(tour.tourName) && (
|
||||
{
|
||||
tourId: tour.tourName,
|
||||
advanceButtonText: intl.formatMessage(messages.advanceButtonText),
|
||||
|
||||
@@ -11,7 +11,7 @@ import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { getCourseConfigApiUrl } from './api';
|
||||
import { useCurrentDiscussionTopic, useUserCanAddThreadInBlackoutDate } from './hooks';
|
||||
import { useCurrentDiscussionTopic, useUserPostingEnabled } from './hooks';
|
||||
import { fetchCourseConfig } from './thunks';
|
||||
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
@@ -19,8 +19,8 @@ const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
const generateApiResponse = (blackouts = [], isCourseAdmin = false) => ({
|
||||
blackouts,
|
||||
const generateApiResponse = (isPostingEnabled, isCourseAdmin = false) => ({
|
||||
isPostingEnabled,
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isCourseAdmin,
|
||||
@@ -30,14 +30,14 @@ const generateApiResponse = (blackouts = [], isCourseAdmin = false) => ({
|
||||
|
||||
describe('Hooks', () => {
|
||||
describe('useCurrentDiscussionTopic', () => {
|
||||
function ComponentWithHook() {
|
||||
const ComponentWithHook = () => {
|
||||
const topic = useCurrentDiscussionTopic();
|
||||
return (
|
||||
<div>
|
||||
{String(topic)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function renderComponent({ topicId, category }) {
|
||||
return render(
|
||||
@@ -102,15 +102,15 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUserCanAddThreadInBlackoutDate', () => {
|
||||
function ComponentWithHook() {
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
describe('useUserPostingEnabled', () => {
|
||||
const ComponentWithHook = () => {
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
return (
|
||||
<div>
|
||||
{String(userCanAddThreadInBlackoutDate)}
|
||||
{String(isUserPrivilagedInPostingRestriction)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function renderComponent() {
|
||||
return render(
|
||||
@@ -121,7 +121,7 @@ describe('Hooks', () => {
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
describe('User can add Thread in blackoutdates ', () => {
|
||||
describe('User can add Thread in Posting Restrictions ', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -136,37 +136,34 @@ describe('Hooks', () => {
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
test('when blackoutdates are not active and Role is Learner return true', async () => {
|
||||
test('when posting is not disabled and Role is Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse([], false));
|
||||
.reply(200, generateApiResponse(true, false));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when blackoutdates are not active and Role is not Learner return true', async () => {
|
||||
test('when posting is not disabled and Role is not Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse([], true));
|
||||
.reply(200, generateApiResponse(true, true));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when blackoutdates are active and Role is Learner return false', async () => {
|
||||
test('when posting is disabled and Role is Learner return false', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse([{
|
||||
start: '2022-11-25T00:00:00Z',
|
||||
end: '2050-11-25T23:59:00Z',
|
||||
}], false));
|
||||
.reply(200, generateApiResponse(false, false));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('false')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when blackoutdates are active and Role is not Learner return true', async () => {
|
||||
test('when posting is not disabled and Role is not Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse([
|
||||
{ start: '2022-11-25T00:00:00Z', end: '2050-11-25T23:59:00Z' }], true));
|
||||
.reply(200, generateApiResponse(false, true));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -20,8 +20,6 @@ export const selectUserRoles = state => state.config.userRoles;
|
||||
|
||||
export const selectDivisionSettings = state => state.config.settings;
|
||||
|
||||
export const selectBlackoutDate = state => state.config.blackouts;
|
||||
|
||||
export const selectGroupAtSubsection = state => state.config.groupAtSubsection;
|
||||
|
||||
export const selectIsCourseAdmin = state => state.config.isCourseAdmin;
|
||||
@@ -30,6 +28,8 @@ export const selectIsCourseStaff = state => state.config.isCourseStaff;
|
||||
|
||||
export const selectEnableInContext = state => state.config.enableInContext;
|
||||
|
||||
export const selectIsPostingEnabled = state => state.config.isPostingEnabled;
|
||||
|
||||
export const selectModerationSettings = state => ({
|
||||
postCloseReasons: state.config.postCloseReasons,
|
||||
editReasons: state.config.editReasons,
|
||||
|
||||
@@ -7,7 +7,6 @@ const configSlice = createSlice({
|
||||
name: 'config',
|
||||
initialState: {
|
||||
status: RequestStatus.IN_PROGRESS,
|
||||
blackouts: [],
|
||||
allowAnonymous: false,
|
||||
allowAnonymousToPeers: false,
|
||||
userRoles: [],
|
||||
@@ -18,6 +17,7 @@ const configSlice = createSlice({
|
||||
isCourseStaff: false,
|
||||
isUserAdmin: false,
|
||||
learnersTabEnabled: false,
|
||||
isPostingEnabled: false,
|
||||
settings: {
|
||||
divisionScheme: 'none',
|
||||
alwaysDivideInlineDiscussions: false,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React, { lazy, Suspense, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
} from 'react-router';
|
||||
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { Spinner } from '../../components';
|
||||
import { selectCourseTabs } from '../../components/NavigationBar/data/selectors';
|
||||
@@ -29,10 +29,9 @@ const CourseTabsNavigation = lazy(() => import('../../components/NavigationBar/C
|
||||
const LegacyBreadcrumbMenu = lazy(() => import('../navigation/breadcrumb-menu/LegacyBreadcrumbMenu'));
|
||||
const NavigationBar = lazy(() => import('../navigation/navigation-bar/NavigationBar'));
|
||||
const DiscussionsProductTour = lazy(() => import('../tours/DiscussionsProductTour'));
|
||||
const BlackoutInformationBanner = lazy(() => import('./BlackoutInformationBanner'));
|
||||
const DiscussionsRestrictionBanner = lazy(() => import('./DiscussionsRestrictionBanner'));
|
||||
const DiscussionContent = lazy(() => import('./DiscussionContent'));
|
||||
const DiscussionSidebar = lazy(() => import('./DiscussionSidebar'));
|
||||
const InformationBanner = lazy(() => import('./InformationBanner'));
|
||||
|
||||
const DiscussionsHome = () => {
|
||||
const location = useLocation();
|
||||
@@ -47,7 +46,6 @@ const DiscussionsHome = () => {
|
||||
const isOnDesktop = useIsOnDesktop();
|
||||
let displaySidebar = useSidebarVisible();
|
||||
const enableInContextSidebar = Boolean(new URLSearchParams(location.search).get('inContextSidebar') !== null);
|
||||
const isFeedbackBannerVisible = getConfig().DISPLAY_FEEDBACK_BANNER === 'true';
|
||||
const {
|
||||
courseId, postId, topicId, category, learnerUsername,
|
||||
} = params;
|
||||
@@ -94,8 +92,7 @@ const DiscussionsHome = () => {
|
||||
)}
|
||||
<PostActionsBar />
|
||||
</div>
|
||||
{isFeedbackBannerVisible && <InformationBanner />}
|
||||
<BlackoutInformationBanner />
|
||||
<DiscussionsRestrictionBanner />
|
||||
</div>
|
||||
{provider === DiscussionProvider.LEGACY && (
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('DiscussionsHome', () => {
|
||||
|
||||
test('header, course navigation bar and footer are only visible in Discussions MFE', async () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByRole('banner')).toBeInTheDocument();
|
||||
waitFor(() => expect(screen.queryByRole('banner')).toBeInTheDocument());
|
||||
expect(document.getElementById('courseTabsNavigation')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('contentinfo')).toBeInTheDocument();
|
||||
});
|
||||
@@ -172,7 +172,8 @@ describe('DiscussionsHome', () => {
|
||||
it.each([
|
||||
{ searchByEndPoint: 'category/section-topic-1' },
|
||||
{ searchByEndPoint: 'topics' },
|
||||
])('should display No Topic selected message on inContext topic pages when user has yet to select a topic %s',
|
||||
])(
|
||||
'should display No Topic selected message on inContext topic pages when user has yet to select a topic %s',
|
||||
async ({ searchByEndPoint }) => {
|
||||
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
|
||||
enableInContext: true, provider: 'openedx', hasModerationPrivileges: true,
|
||||
@@ -193,7 +194,8 @@ describe('DiscussionsHome', () => {
|
||||
await renderComponent(`/${courseId}/${searchByEndPoint}`);
|
||||
|
||||
expect(screen.queryByText('No topic selected')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should display empty page message for empty learners list', async () => {
|
||||
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { PageBanner } from '@edx/paragon';
|
||||
|
||||
import { selectBlackoutDate } from '../data/selectors';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { selectconfigLoadingStatus, selectIsPostingEnabled } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { inBlackoutDateRange } from '../utils';
|
||||
|
||||
const BlackoutInformationBanner = () => {
|
||||
const DiscussionsRestrictionBanner = () => {
|
||||
const intl = useIntl();
|
||||
const blackoutDate = useSelector(selectBlackoutDate);
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const configLoadingStatus = useSelector(selectconfigLoadingStatus);
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
|
||||
const isDiscussionsBlackout = useMemo(() => (
|
||||
inBlackoutDateRange(blackoutDate)
|
||||
), [blackoutDate]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setShowBanner(false);
|
||||
}, []);
|
||||
@@ -25,7 +22,7 @@ const BlackoutInformationBanner = () => {
|
||||
return (
|
||||
<PageBanner
|
||||
variant="accentB"
|
||||
show={isDiscussionsBlackout && showBanner}
|
||||
show={!isPostingEnabled && showBanner && configLoadingStatus === RequestStatus.SUCCESSFUL}
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
@@ -36,4 +33,4 @@ const BlackoutInformationBanner = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BlackoutInformationBanner;
|
||||
export default DiscussionsRestrictionBanner;
|
||||
@@ -8,7 +8,7 @@ import { initializeStore } from '../../store';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { fetchConfigSuccess } from '../data/slices';
|
||||
import messages from '../messages';
|
||||
import BlackoutInformationBanner from './BlackoutInformationBanner';
|
||||
import DiscussionsRestrictionBanner from './DiscussionsRestrictionBanner';
|
||||
|
||||
let store;
|
||||
let container;
|
||||
@@ -20,13 +20,13 @@ activeEndDate.setDate(activeEndDate.getDate() + 2);
|
||||
activeStartDate = activeStartDate.toISOString();
|
||||
activeEndDate = activeEndDate.toISOString();
|
||||
|
||||
const getConfigData = (blackouts = []) => ({
|
||||
const getConfigData = (isPostingEnabled) => ({
|
||||
id: 'course-v1:edX+DemoX+Demo_Course',
|
||||
userRoles: ['Admin', 'Student'],
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isUserAdmin: false,
|
||||
blackouts,
|
||||
isPostingEnabled,
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
@@ -34,7 +34,7 @@ function renderComponent() {
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<BlackoutInformationBanner />
|
||||
<DiscussionsRestrictionBanner />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
@@ -43,7 +43,7 @@ function renderComponent() {
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('Blackout Information Banner', () => {
|
||||
describe('Discussions Restriction Banner', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -56,13 +56,11 @@ describe('Blackout Information Banner', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ blackouts: [], visibility: false },
|
||||
{ blackouts: ['2021-12-31T10:15', '2021-12-31T10:20'], visibility: false },
|
||||
{ blackouts: [{ start: activeStartDate, end: activeEndDate }], visibility: true },
|
||||
{ blackouts: [{ start: activeEndDate, end: activeEndDate }], visibility: false },
|
||||
])('Test Blackout Banner is visible on app load if blackout date is active', async ({ blackouts, visibility }) => {
|
||||
{ isPostingEnabled: false, visibility: true },
|
||||
{ isPostingEnabled: true, visibility: false },
|
||||
])('Test Discussions Restriction is visible on app load if posting is disabled', async ({ isPostingEnabled, visibility }) => {
|
||||
store = initializeStore();
|
||||
await store.dispatch(fetchConfigSuccess(getConfigData(blackouts)));
|
||||
await store.dispatch(fetchConfigSuccess(getConfigData(isPostingEnabled)));
|
||||
renderComponent();
|
||||
if (visibility) {
|
||||
const element = await screen.findByRole('alert');
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
@@ -22,9 +23,9 @@ export default function useFeedbackWrapper() {
|
||||
|
||||
useEffect(() => {
|
||||
if (configStatus === RequestStatus.SUCCESSFUL) {
|
||||
let url = '//w.usabilla.com/9e6036348fa1.js';
|
||||
let url = getConfig().LEARNER_FEEDBACK_URL;
|
||||
if (isStaff || isUserGroupTA || isCourseAdmin || isCourseStaff) {
|
||||
url = '//w.usabilla.com/767740a06856.js';
|
||||
url = getConfig().STAFF_FEEDBACK_URL;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, PageBanner } from '@edx/paragon';
|
||||
|
||||
import { selectUserIsStaff, selectUserRoles } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
|
||||
const InformationBanner = () => {
|
||||
const intl = useIntl();
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
const userRoles = useSelector(selectUserRoles);
|
||||
const isAdmin = useSelector(selectUserIsStaff);
|
||||
const learnMoreLink = 'https://openedx.atlassian.net/wiki/spaces/COMM/pages/3509551260/Overview+New+discussions+experience';
|
||||
const TAFeedbackLink = process.env.TA_FEEDBACK_FORM;
|
||||
const staffFeedbackLink = process.env.STAFF_FEEDBACK_FORM;
|
||||
const hideLearnMoreButton = ((userRoles.includes('Student') && userRoles.length === 1) || !userRoles.length) && !isAdmin;
|
||||
const showStaffLink = isAdmin || userRoles.includes('Moderator') || userRoles.includes('Administrator');
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setShowBanner(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageBanner
|
||||
variant="light"
|
||||
show={showBanner}
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
<div className="font-weight-500">
|
||||
{intl.formatMessage(messages.bannerMessage)}
|
||||
{!hideLearnMoreButton
|
||||
&& (
|
||||
<Hyperlink
|
||||
destination={learnMoreLink}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
className="pl-2.5"
|
||||
variant="muted"
|
||||
isInline
|
||||
>
|
||||
{intl.formatMessage(messages.learnMoreBannerLink)}
|
||||
</Hyperlink>
|
||||
)}
|
||||
<Hyperlink
|
||||
destination={showStaffLink ? staffFeedbackLink : TAFeedbackLink}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
variant="muted"
|
||||
className="pl-2.5"
|
||||
isInline
|
||||
>
|
||||
{intl.formatMessage(messages.shareFeedback)}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
</PageBanner>
|
||||
);
|
||||
};
|
||||
|
||||
export default InformationBanner;
|
||||
@@ -1,136 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { initializeStore } from '../../store';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { fetchConfigSuccess } from '../data/slices';
|
||||
import messages from '../messages';
|
||||
import InformationBanner from './InformationBanner';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
|
||||
let store;
|
||||
let container;
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
const getConfigData = (isAdmin = true, roles = []) => ({
|
||||
id: 'course-v1:edX+DemoX+Demo_Course',
|
||||
userRoles: roles,
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isUserAdmin: isAdmin,
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<InformationBanner />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('Information Banner learner view', () => {
|
||||
let element;
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: ['Student'],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
store.dispatch(fetchConfigSuccess(getConfigData(false, ['Student'])));
|
||||
renderComponent(true);
|
||||
element = await screen.findByRole('alert');
|
||||
});
|
||||
|
||||
test('Test Banner is visible on app load', async () => {
|
||||
expect(element).toHaveTextContent(messages.bannerMessage.defaultMessage);
|
||||
});
|
||||
|
||||
test('Test Banner do not have learn more button', async () => {
|
||||
expect(element).not.toHaveTextContent(messages.learnMoreBannerLink.defaultMessage);
|
||||
});
|
||||
test('Test Banner has share feedback button', async () => {
|
||||
expect(element).toHaveTextContent(messages.shareFeedback.defaultMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Information Banner moderators/staff/admin view', () => {
|
||||
let element;
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
store.dispatch(fetchConfigSuccess(getConfigData(true, ['Student', 'Moderator'])));
|
||||
renderComponent(true);
|
||||
element = await screen.findByRole('alert');
|
||||
});
|
||||
|
||||
test('Test Banner is visible on app load', async () => {
|
||||
expect(element).toHaveTextContent(messages.bannerMessage.defaultMessage);
|
||||
});
|
||||
|
||||
test('Test Banner has learn more button', async () => {
|
||||
expect(element).toHaveTextContent(messages.learnMoreBannerLink.defaultMessage);
|
||||
});
|
||||
test('Test Banner has share feedback button', async () => {
|
||||
expect(element).toHaveTextContent(messages.shareFeedback.defaultMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User is redirected according to url according to role', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
test('TAs are redirected to learners feedback form', async () => {
|
||||
store.dispatch(fetchConfigSuccess(getConfigData(false, ['Student', 'Community TA'])));
|
||||
renderComponent(true);
|
||||
expect(screen.getByText(messages.shareFeedback.defaultMessage)
|
||||
.closest('a'))
|
||||
.toHaveAttribute('href', process.env.TA_FEEDBACK_FORM);
|
||||
});
|
||||
|
||||
test('moderators/administrators are redirected to moderators feedback form', async () => {
|
||||
store.dispatch(fetchConfigSuccess(getConfigData(false, ['Student', 'Moderator', 'Administrator'])));
|
||||
renderComponent(true);
|
||||
expect(screen.getByText(messages.shareFeedback.defaultMessage)
|
||||
.closest('a'))
|
||||
.toHaveAttribute('href', process.env.STAFF_FEEDBACK_FORM);
|
||||
});
|
||||
|
||||
test('user with only isAdmin true are redirected to moderators feedback form', async () => {
|
||||
store.dispatch(fetchConfigSuccess(getConfigData(true, ['Student'])));
|
||||
renderComponent(true);
|
||||
expect(screen.getByText(messages.shareFeedback.defaultMessage)
|
||||
.closest('a'))
|
||||
.toHaveAttribute('href', process.env.STAFF_FEEDBACK_FORM);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIsOnDesktop } from '../data/hooks';
|
||||
import { selectAreThreadsFiltered, selectPostThreadCount } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { messages as postMessages, showPostEditor } from '../posts';
|
||||
import EmptyPage from './EmptyPage';
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ALL_ROUTES } from '../../data/constants';
|
||||
import { useIsOnDesktop, useTotalTopicThreadCount } from '../data/hooks';
|
||||
import { selectTopicThreadCount } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { messages as postMessages, showPostEditor } from '../posts';
|
||||
import EmptyPage from './EmptyPage';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as EmptyLearners } from './EmptyLearners';
|
||||
export { default as EmptyPage } from './EmptyPage';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
export { default as EmptyPosts } from './EmptyPosts';
|
||||
export { default as EmptyTopics } from './EmptyTopics';
|
||||
|
||||
@@ -92,7 +92,6 @@ describe('InContext Topic Posts View', () => {
|
||||
enableInContext: true,
|
||||
provider: 'openedx',
|
||||
hasModerationPrivileges: true,
|
||||
blackouts: [],
|
||||
},
|
||||
});
|
||||
Factory.resetAll();
|
||||
@@ -206,7 +205,8 @@ describe('InContext Topic Posts View', () => {
|
||||
test.each([
|
||||
{ searchText: 'hello world', output: 'Showing 0 results for', resultCount: 0 },
|
||||
{ searchText: 'introduction', output: 'Showing 8 results for', resultCount: 8 },
|
||||
])('It should have a search bar with a clear button and \'$output\' results found text.',
|
||||
])(
|
||||
'It should have a search bar with a clear button and \'$output\' results found text.',
|
||||
async ({ searchText, output, resultCount }) => {
|
||||
await setupTopicsMockResponse();
|
||||
await renderComponent();
|
||||
@@ -226,7 +226,8 @@ describe('InContext Topic Posts View', () => {
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
expect(units).toHaveLength(resultCount);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('When click on the clear button it should move to main topics pages.', async () => {
|
||||
await setupTopicsMockResponse();
|
||||
@@ -253,7 +254,8 @@ describe('InContext Topic Posts View', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should display Nothing here yet and No topic exists message when topics and selectedSubsectionUnits are empty',
|
||||
it(
|
||||
'should display Nothing here yet and No topic exists message when topics and selectedSubsectionUnits are empty',
|
||||
async () => {
|
||||
await setupTopicsMockResponse(0, 0, 0);
|
||||
await renderComponent({ topicId: 'test-topic', category: 'test-category' });
|
||||
@@ -261,7 +263,8 @@ describe('InContext Topic Posts View', () => {
|
||||
await waitFor(() => expect(within(container).queryByText('Nothing here yet')).toBeInTheDocument());
|
||||
expect(within(container).queryByText('No topic exists')).toBeInTheDocument();
|
||||
expect(within(container).queryByText('Unnamed Topic')).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should display all topics when search by an empty search string', async () => {
|
||||
await setupTopicsMockResponse();
|
||||
|
||||
@@ -9,9 +9,9 @@ import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function BackButton({
|
||||
const BackButton = ({
|
||||
intl, path, title, loading,
|
||||
}) {
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
@@ -32,7 +32,7 @@ function BackButton({
|
||||
<div className="border-bottom border-light-400" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BackButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -20,12 +20,21 @@ Factory.define('sub-section')
|
||||
.sequence('id', ['topicPrefix'], (idx, topicPrefix) => `${topicPrefix}-topic-${idx}`)
|
||||
.sequence('display-name', ['sectionPrefix'], (idx, sectionPrefix) => `Introduction ${sectionPrefix + idx}`)
|
||||
.option('courseId', null, 'course-v1:edX+DemoX+Demo_Course')
|
||||
.sequence('legacy_web_url', ['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}?experience=legacy`)
|
||||
.sequence('lms_web_url', ['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}`)
|
||||
.sequence('student_view_url', ['id', 'courseId'],
|
||||
(idx, id) => `${getApiBaseUrl}/xblock/block-v1:${id}`)
|
||||
.sequence(
|
||||
'legacy_web_url',
|
||||
['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}?experience=legacy`,
|
||||
)
|
||||
.sequence(
|
||||
'lms_web_url',
|
||||
['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/block-v1:${id}`,
|
||||
)
|
||||
.sequence(
|
||||
'student_view_url',
|
||||
['id', 'courseId'],
|
||||
(idx, id) => `${getApiBaseUrl}/xblock/block-v1:${id}`,
|
||||
)
|
||||
.attr('type', null, 'sequential')
|
||||
.attr('activeFlags', null, true)
|
||||
.attr('thread_counts', ['discussionCount', 'questionCount'], (discCount, questCount) => {
|
||||
@@ -51,12 +60,21 @@ Factory.define('section')
|
||||
.attr('courseware', null, true)
|
||||
.sequence('display-name', (idx) => `Introduction ${idx}`)
|
||||
.option('courseId', null, 'course-v1:edX+DemoX+Demo_Course')
|
||||
.sequence('legacy_web_url', ['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}?experience=legacy`)
|
||||
.sequence('lms_web_url', ['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`)
|
||||
.sequence('student_view_url', ['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/xblock/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`)
|
||||
.sequence(
|
||||
'legacy_web_url',
|
||||
['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}?experience=legacy`,
|
||||
)
|
||||
.sequence(
|
||||
'lms_web_url',
|
||||
['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/courses/${courseId}/jump_to/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`,
|
||||
)
|
||||
.sequence(
|
||||
'student_view_url',
|
||||
['id', 'courseId'],
|
||||
(idx, id, courseId) => `${getApiBaseUrl}/xblock/${courseId.replace('course-v1:', 'block-v1:')}+type@chapter+block@${id}`,
|
||||
)
|
||||
.attr('type', null, 'chapter')
|
||||
.attr('children', ['id', 'display-name'], (id, name) => {
|
||||
Factory.reset('sub-section');
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SearchField } from '@edx/paragon';
|
||||
import { setFilter } from '../data';
|
||||
import messages from '../messages';
|
||||
|
||||
function TopicSearchResultBar({ intl }) {
|
||||
const TopicSearchResultBar = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
@@ -21,7 +21,7 @@ function TopicSearchResultBar({ intl }) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
TopicSearchResultBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable no-unused-vars, react/forbid-prop-types */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -110,7 +110,8 @@ describe('Learner Posts View', () => {
|
||||
expect(backButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Learner title bar should redirect to the learners list when clicking on the back button',
|
||||
test(
|
||||
'Learner title bar should redirect to the learners list when clicking on the back button',
|
||||
async () => {
|
||||
await renderComponent();
|
||||
|
||||
@@ -122,7 +123,8 @@ describe('Learner Posts View', () => {
|
||||
await waitFor(() => {
|
||||
expect(lastLocation.pathname.endsWith('/learners')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should display a post-filter bar and All posts sorted by recent activity text.', async () => {
|
||||
await renderComponent();
|
||||
|
||||
@@ -66,6 +66,7 @@ const LearnersView = () => {
|
||||
courseConfigLoadingStatus === RequestStatus.SUCCESSFUL && learnersTabEnabled && learners.map((learner) => (
|
||||
<LearnerCard learner={learner} key={learner.username} />
|
||||
))
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
) || <></>
|
||||
), [courseConfigLoadingStatus, learnersTabEnabled, learners]);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable default-param-last */
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
@@ -201,7 +202,8 @@ describe('LearnersView', () => {
|
||||
username:
|
||||
['learner-1', 'learner-2'],
|
||||
},
|
||||
])('should have a search bar with a clear button and \'$output\' results found text.',
|
||||
])(
|
||||
'should have a search bar with a clear button and \'$output\' results found text.',
|
||||
async ({
|
||||
searchText, output, learnersCount, username,
|
||||
}) => {
|
||||
@@ -227,7 +229,8 @@ describe('LearnersView', () => {
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
expect(leaners).toHaveLength(learnersCount);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('When click on the clear button it should move to a list of all learners.', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
@@ -257,7 +260,8 @@ describe('LearnersView', () => {
|
||||
expect(learners).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should display reported and previously reported message by passing activeFlags or inactiveFlags',
|
||||
it(
|
||||
'should display reported and previously reported message by passing activeFlags or inactiveFlags',
|
||||
async () => {
|
||||
await setUpLearnerMockResponse(2, 2, 1, ['learner-1', 'learner-2'], '', 1, 1);
|
||||
await assignPrivilages(true);
|
||||
@@ -274,7 +278,8 @@ describe('LearnersView', () => {
|
||||
expect(reportedIcon).toBeInTheDocument();
|
||||
expect(reported).toBeInTheDocument();
|
||||
expect(previouslyReported).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should display load more button and display more learners by clicking on button.', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
|
||||
@@ -29,7 +29,8 @@ describe('Learner api test cases', () => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it('Successfully get and store API response for the learner\'s list and learners posts in redux',
|
||||
it(
|
||||
'Successfully get and store API response for the learner\'s list and learners posts in redux',
|
||||
async () => {
|
||||
const learners = await setupLearnerMockResponse();
|
||||
const threads = await setupPostsMockResponse();
|
||||
@@ -38,20 +39,23 @@ describe('Learner api test cases', () => {
|
||||
expect(Object.values(learners.learnerProfiles)).toHaveLength(3);
|
||||
expect(threads.status).toEqual('successful');
|
||||
expect(Object.values(threads.threadsById)).toHaveLength(2);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ status: 'statusUnread', search: 'Title', cohort: 'post' },
|
||||
{ status: 'statusUnanswered', search: 'Title', cohort: 'post' },
|
||||
{ status: 'statusReported', search: 'Title', cohort: 'post' },
|
||||
{ status: 'statusUnresponded', search: 'Title', cohort: 'post' },
|
||||
])('Successfully fetch user posts based on %s filters',
|
||||
])(
|
||||
'Successfully fetch user posts based on %s filters',
|
||||
async ({ status, search, cohort }) => {
|
||||
const threads = await setupPostsMockResponse({ filters: { status, search, cohort } });
|
||||
|
||||
expect(threads.status).toEqual('successful');
|
||||
expect(Object.values(threads.threadsById)).toHaveLength(2);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('Failed to fetch learners', async () => {
|
||||
const learners = await setupLearnerMockResponse({ learnerCourseId: courseId2 });
|
||||
|
||||
@@ -37,7 +37,8 @@ describe('Learner redux test cases', () => {
|
||||
test('Successfully load initial states in redux', async () => {
|
||||
executeThunk(
|
||||
fetchLearners('course-v1:edX+DemoX+Demo_Course', { usernameSearch: 'learner-1' }),
|
||||
store.dispatch, store.getState,
|
||||
store.dispatch,
|
||||
store.getState,
|
||||
);
|
||||
const { learners } = store.getState();
|
||||
|
||||
@@ -55,7 +56,8 @@ describe('Learner redux test cases', () => {
|
||||
expect(learners.postFilter.cohort).toEqual('');
|
||||
});
|
||||
|
||||
test('Successfully store a learner posts stats data as pages object in redux',
|
||||
test(
|
||||
'Successfully store a learner posts stats data as pages object in redux',
|
||||
async () => {
|
||||
const learners = await setupLearnerMockResponse();
|
||||
const page = learners.pages[0];
|
||||
@@ -65,9 +67,11 @@ describe('Learner redux test cases', () => {
|
||||
expect(statsObject.responses).toEqual(3);
|
||||
expect(statsObject.threads).toEqual(1);
|
||||
expect(statsObject.replies).toEqual(0);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux',
|
||||
test(
|
||||
'Successfully store the nextPage, totalPages, totalLearners, and sortedBy data in redux',
|
||||
async () => {
|
||||
const learners = await setupLearnerMockResponse();
|
||||
|
||||
@@ -75,7 +79,8 @@ describe('Learner redux test cases', () => {
|
||||
expect(learners.totalPages).toEqual(2);
|
||||
expect(learners.totalLearners).toEqual(3);
|
||||
expect(learners.sortedBy).toEqual('activity');
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('Successfully updated the learner\'s sort data in redux', async () => {
|
||||
const learners = await setupLearnerMockResponse();
|
||||
@@ -106,7 +111,8 @@ describe('Learner redux test cases', () => {
|
||||
expect(updatedLearners.pages).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Successfully update the learner\'s search query in redux when searching for a learner',
|
||||
test(
|
||||
'Successfully update the learner\'s search query in redux when searching for a learner',
|
||||
async () => {
|
||||
const learners = await setupLearnerMockResponse();
|
||||
|
||||
@@ -116,5 +122,6 @@ describe('Learner redux test cases', () => {
|
||||
const updatedLearners = store.getState().learners;
|
||||
|
||||
expect(updatedLearners.usernameSearch).toEqual('learner-2');
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { fetchCourseCohorts } from '../../cohorts/data/thunks';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import { setPostFilter } from '../data/slices';
|
||||
|
||||
function LearnerPostFilterBar() {
|
||||
const LearnerPostFilterBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { courseId } = useParams();
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
@@ -98,6 +98,6 @@ function LearnerPostFilterBar() {
|
||||
showCohortsFilter={userHasModerationPrivileges || userIsGroupTa}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default LearnerPostFilterBar;
|
||||
|
||||
@@ -168,24 +168,9 @@ const messages = defineMessages({
|
||||
defaultMessage: 'anonymous',
|
||||
description: 'Author name displayed when a post is anonymous',
|
||||
},
|
||||
bannerMessage: {
|
||||
id: 'discussion.banner.welcomeMessage',
|
||||
defaultMessage: '🎉 Welcome to the new and improved discussions experience!',
|
||||
description: 'Information banner welcome text',
|
||||
},
|
||||
learnMoreBannerLink: {
|
||||
id: 'discussion.banner.learnMore',
|
||||
defaultMessage: 'Learn more',
|
||||
description: 'learn more button to redirect users to know more about new discussion experience ',
|
||||
},
|
||||
shareFeedback: {
|
||||
id: 'discussion.banner.shareFeedback',
|
||||
defaultMessage: 'Share feedback',
|
||||
description: 'Share feedback button to open feedback forms',
|
||||
},
|
||||
blackoutDiscussionInformation: {
|
||||
id: 'discussion.blackoutBanner.information',
|
||||
defaultMessage: 'Posting in discussions is temporarily disabled by the course team',
|
||||
defaultMessage: 'Posting in discussions is disabled by the course team',
|
||||
description: 'Informative text when discussion posting is disabled',
|
||||
},
|
||||
imageWarningMessage: {
|
||||
|
||||
@@ -74,6 +74,7 @@ const PostCommentsView = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
<PostCommentsContext.Provider value={{
|
||||
isClosed: closed,
|
||||
postType: type,
|
||||
|
||||
@@ -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 { getCohortsApiUrl } from '../cohorts/data/api';
|
||||
@@ -20,12 +21,13 @@ import { fetchCourseConfig } from '../data/thunks';
|
||||
import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThread, fetchThreads } from '../posts/data/thunks';
|
||||
import { fetchCourseTopics } from '../topics/data/thunks';
|
||||
import { getDiscussionTourUrl } from '../tours/data/api';
|
||||
import { selectTours } from '../tours/data/selectors';
|
||||
import { fetchDiscussionTours } from '../tours/data/thunks';
|
||||
import discussionTourFactory from '../tours/data/tours.factory';
|
||||
import { getCommentsApiUrl } from './data/api';
|
||||
import { fetchCommentResponses, fetchThreadComments, removeComment } from './data/thunks';
|
||||
import { fetchCommentResponses, removeComment } from './data/thunks';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
import './data/__factories__';
|
||||
@@ -39,44 +41,21 @@ const discussionPostId = 'thread-1';
|
||||
const questionPostId = 'thread-2';
|
||||
const closedPostId = 'thread-2';
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const reverseOrder = true;
|
||||
const enableInContextSidebar = false;
|
||||
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
|
||||
let store;
|
||||
let axiosMock;
|
||||
let testLocation;
|
||||
let container;
|
||||
let unmount;
|
||||
|
||||
async function mockAxiosReturnPagedComments() {
|
||||
const endorsedArray = [null, false, true];
|
||||
const pageArray = [1, 2];
|
||||
|
||||
endorsedArray.forEach(async (endorsed) => {
|
||||
const postId = endorsed === null ? discussionPostId : questionPostId;
|
||||
|
||||
pageArray.forEach(async (page) => {
|
||||
const params = {
|
||||
thread_id: postId,
|
||||
page,
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
endorsed,
|
||||
reverse_order: reverseOrder,
|
||||
enable_in_context_sidebar: enableInContextSidebar,
|
||||
signal: {},
|
||||
};
|
||||
axiosMock.onGet(commentsApiUrl, { ...params }).reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
threadId: postId,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
endorsed,
|
||||
childCount: page === 1 ? 2 : 0,
|
||||
}));
|
||||
|
||||
await executeThunk(fetchThreadComments(postId, { ...params }), store.dispatch, store.getState);
|
||||
});
|
||||
});
|
||||
async function mockAxiosReturnPagedComments(threadId, endorsed = false, page = 1, count = 2) {
|
||||
axiosMock.onGet(commentsApiUrl).reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
threadId,
|
||||
endorsed,
|
||||
pageSize: 1,
|
||||
count,
|
||||
childCount: page === 1 ? 2 : 0,
|
||||
}));
|
||||
}
|
||||
|
||||
async function mockAxiosReturnPagedCommentsResponses() {
|
||||
@@ -90,13 +69,16 @@ async function mockAxiosReturnPagedCommentsResponses() {
|
||||
};
|
||||
|
||||
[1, 2].forEach(async (page) => {
|
||||
axiosMock.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } }).reply(200,
|
||||
axiosMock.onGet(commentsResponsesApiUrl, { params: { ...paramsTemplate, page } }).reply(
|
||||
200,
|
||||
Factory.build('commentsResult', null, {
|
||||
threadId: discussionPostId,
|
||||
parentId,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
await executeThunk(fetchCommentResponses(parentId), store.dispatch, store.getState);
|
||||
});
|
||||
@@ -107,12 +89,30 @@ async function getThreadAPIResponse(attr = null) {
|
||||
await executeThunk(fetchThread(discussionPostId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
function renderComponent(postId) {
|
||||
async function setupCourseConfig(reasonCodesEnabled = true) {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
has_moderation_privileges: true,
|
||||
isPostingEnabled: true,
|
||||
reason_codes_enabled: reasonCodesEnabled,
|
||||
editReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
postCloseReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
function renderComponent(postId, isClosed = false) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{ courseId, postId }}
|
||||
value={{ courseId, postId, isClosed }}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/posts/${postId}`]}>
|
||||
<DiscussionContent />
|
||||
@@ -132,6 +132,45 @@ function renderComponent(postId) {
|
||||
unmount = wrapper.unmount;
|
||||
}
|
||||
|
||||
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({ id: 'thread-1', topic_id: 'non-courseware-topic-1' });
|
||||
await waitFor(() => 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({ id: 'thread-2', topic_id: 'courseware-topic-2' });
|
||||
|
||||
await waitFor(() => renderComponent('thread-2'));
|
||||
expect(await screen.findByText('Related to')).toBeInTheDocument();
|
||||
expect(await screen.findByText('category-1 / courseware-topic 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ThreadView', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
@@ -165,10 +204,12 @@ describe('ThreadView', () => {
|
||||
thread_id: threadId,
|
||||
})];
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { isPostingEnabled: true });
|
||||
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await executeThunk(fetchCourseCohorts(courseId), store.dispatch, store.getState);
|
||||
await mockAxiosReturnPagedComments(discussionPostId);
|
||||
await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
|
||||
await mockAxiosReturnPagedComments();
|
||||
await mockAxiosReturnPagedCommentsResponses();
|
||||
});
|
||||
|
||||
@@ -181,6 +222,16 @@ describe('ThreadView', () => {
|
||||
expect(JSON.parse(axiosMock.history.patch[axiosMock.history.patch.length - 1].data)).toMatchObject(data);
|
||||
}
|
||||
|
||||
it('should not allow posting a comment on a closed post', async () => {
|
||||
axiosMock.reset();
|
||||
await mockAxiosReturnPagedComments(closedPostId, true);
|
||||
await waitFor(() => renderComponent(closedPostId, true));
|
||||
const comments = await waitFor(() => screen.findAllByTestId('comment-comment-4'));
|
||||
const hoverCard = within(comments[0]).getByTestId('hover-card-comment-4');
|
||||
|
||||
expect(within(hoverCard).getByRole('button', { name: /Add comment/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should display post content', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
const post = await waitFor(() => screen.getByTestId('post-thread-1'));
|
||||
@@ -261,7 +312,7 @@ describe('ThreadView', () => {
|
||||
await act(async () => { fireEvent.click(screen.getByText(/submit/i)); });
|
||||
|
||||
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
|
||||
await waitFor(async () => expect(await screen.findByTestId('reply-comment-7')).toBeInTheDocument());
|
||||
await waitFor(async () => expect(await screen.findByTestId('reply-comment-2')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should allow editing an existing comment', async () => {
|
||||
@@ -279,23 +330,6 @@ describe('ThreadView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
async function setupCourseConfig(reasonCodesEnabled = true) {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
has_moderation_privileges: true,
|
||||
reason_codes_enabled: reasonCodesEnabled,
|
||||
editReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
postCloseReasons: [
|
||||
{ code: 'reason-1', label: 'reason 1' },
|
||||
{ code: 'reason-2', label: 'reason 2' },
|
||||
],
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
it('should show reason codes when closing a post', async () => {
|
||||
await setupCourseConfig();
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
@@ -325,6 +359,39 @@ describe('ThreadView', () => {
|
||||
assertLastUpdateData({ closed: true, close_reason_code: 'reason-1' });
|
||||
});
|
||||
|
||||
it('should show reason codes when editing an existing comment', async () => {
|
||||
setupCourseConfig();
|
||||
renderComponent(discussionPostId);
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(hoverCard).getByRole('button', { name: /actions menu/i }),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
||||
});
|
||||
expect(screen.queryByRole('combobox', { name: /reason for editing/i })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('option', { name: /reason \d/i })).toHaveLength(2);
|
||||
await act(async () => {
|
||||
fireEvent.change(
|
||||
screen.queryByRole('combobox', { name: /reason for editing/i }),
|
||||
{ target: { value: null } },
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.queryByRole('combobox', { name: /reason for editing/i }), { target: { value: 'reason-1' } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('tinymce-editor'), { target: { value: 'testing123' } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
|
||||
});
|
||||
assertLastUpdateData({ edit_reason_code: 'reason-1' });
|
||||
});
|
||||
|
||||
it('should close the post directly if reason codes are not enabled', async () => {
|
||||
await setupCourseConfig(false);
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
@@ -398,6 +465,20 @@ describe('ThreadView', () => {
|
||||
assertLastUpdateData({ pinned: false });
|
||||
});
|
||||
|
||||
it('should allow copying a link to the post', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
const hoverCard = within(post).getByTestId('hover-card-thread-1');
|
||||
Object.assign(navigator, { clipboard: { writeText: jest.fn() } });
|
||||
await act(async () => {
|
||||
fireEvent.click(within(hoverCard).getByRole('button', { name: /actions menu/i }));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(within(hoverCard).getByRole('button', { name: /copy link/i }));
|
||||
});
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(`http://localhost/${courseId}/posts/${discussionPostId}`);
|
||||
});
|
||||
|
||||
it('should allow reporting the post', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
@@ -483,8 +564,10 @@ describe('ThreadView', () => {
|
||||
});
|
||||
|
||||
describe('for discussion thread', () => {
|
||||
const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments');
|
||||
|
||||
it('shown post not found when post id does not belong to course', async () => {
|
||||
renderComponent('unloaded-id');
|
||||
await waitFor(() => renderComponent('unloaded-id'));
|
||||
expect(await screen.findByText('Thread not found', { exact: true }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
@@ -497,6 +580,70 @@ describe('ThreadView', () => {
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of comments', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
await mockAxiosReturnPagedComments(discussionPostId, false, 2);
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
await act(async () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('comment-comment-1');
|
||||
await screen.findByTestId('comment-comment-4');
|
||||
});
|
||||
|
||||
it('newly loaded comments are appended to the old ones', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
await mockAxiosReturnPagedComments(discussionPostId, false, 2);
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
await act(async () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('comment-comment-1');
|
||||
// check that comments from the first page are also displayed
|
||||
expect(screen.queryByTestId('comment-comment-4'))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('for question thread', () => {
|
||||
const findLoadMoreCommentsButtons = () => screen.findByTestId('load-more-comments');
|
||||
|
||||
it('initially loads only the first page', async () => {
|
||||
await mockAxiosReturnPagedComments(questionPostId);
|
||||
act(() => renderComponent(questionPostId));
|
||||
|
||||
expect(await screen.findByTestId('comment-comment-4'))
|
||||
.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('comment-comment-5'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of comments', async () => {
|
||||
await mockAxiosReturnPagedComments(questionPostId);
|
||||
await waitFor(() => renderComponent(questionPostId));
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButtons();
|
||||
|
||||
expect(await screen.findByTestId('comment-comment-4'))
|
||||
.toBeInTheDocument();
|
||||
// Comments from next page should not be loaded yet.
|
||||
expect(await screen.queryByTestId('comment-comment-5'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
await mockAxiosReturnPagedComments(questionPostId, false, 2, 1);
|
||||
await act(async () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
// Endorsed comment from next page should be loaded now.
|
||||
await waitFor(() => expect(screen.queryByTestId('comment-comment-5'))
|
||||
.toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
describe('for comments replies', () => {
|
||||
@@ -505,8 +652,8 @@ describe('ThreadView', () => {
|
||||
it('initially loads only the first page', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
expect(screen.queryByTestId('reply-comment-8')).not.toBeInTheDocument();
|
||||
await waitFor(() => screen.findByTestId('reply-comment-2'));
|
||||
expect(screen.queryByTestId('reply-comment-3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of responses', async () => {
|
||||
@@ -516,8 +663,7 @@ describe('ThreadView', () => {
|
||||
await act(async () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
await screen.findByTestId('reply-comment-3');
|
||||
});
|
||||
|
||||
it('newly loaded responses are appended to the old ones', async () => {
|
||||
@@ -528,9 +674,9 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
await screen.findByTestId('reply-comment-3');
|
||||
// check that comments from the first page are also displayed
|
||||
expect(screen.queryByTestId('reply-comment-7')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('reply-comment-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('load more button is hidden when no more responses pages to load', async () => {
|
||||
@@ -541,8 +687,7 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
await expect(findLoadMoreCommentsResponsesButton()).rejects.toThrow();
|
||||
await screen.findByTestId('reply-comment-3');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -576,21 +721,21 @@ describe('ThreadView', () => {
|
||||
it('shows action dropdown for replies', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-2'));
|
||||
expect(within(reply).getByRole('button', { name: /actions menu/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display reply content', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
expect(within(reply).queryByTestId('comment-7')).toBeInTheDocument();
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-2'));
|
||||
expect(within(reply).queryByTestId('comment-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows delete confirmation modal', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-2'));
|
||||
await act(async () => { fireEvent.click(within(reply).getByRole('button', { name: /actions menu/i })); });
|
||||
await act(async () => { fireEvent.click(screen.queryByRole('button', { name: /Delete/i })); });
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Spinner } from '@edx/paragon';
|
||||
|
||||
import { EndorsementStatus } from '../../../data/constants';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { isLastElementOfList } from '../../utils';
|
||||
import { usePostComments } from '../data/hooks';
|
||||
import messages from '../messages';
|
||||
@@ -16,7 +16,8 @@ const CommentsView = ({ endorsed }) => {
|
||||
const intl = useIntl();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const { isClosed } = useContext(PostCommentsContext);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
|
||||
const {
|
||||
endorsedCommentsIds,
|
||||
unEndorsedCommentsIds,
|
||||
@@ -35,7 +36,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
|
||||
const handleDefinition = useCallback((message, commentsLength) => (
|
||||
<div
|
||||
className="mx-4 my-14px text-gray-700 font-style"
|
||||
className="comment-line mx-4 my-14px text-gray-700 font-style"
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
>
|
||||
@@ -72,6 +73,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
), [hasMorePages, isLoading, handleLoadMoreResponses]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{((hasMorePages && isLoading) || !isLoading) && (
|
||||
<>
|
||||
@@ -88,7 +90,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
{handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)}
|
||||
{unEndorsedCommentsIds.length === 0 && <br />}
|
||||
{handleComments(unEndorsedCommentsIds, false)}
|
||||
{(userCanAddThreadInBlackoutDate && !!unEndorsedCommentsIds.length && !isClosed) && (
|
||||
{(isUserPrivilagedInPostingRestriction && !!unEndorsedCommentsIds.length && !isClosed) && (
|
||||
<div className="mx-4">
|
||||
{!addingResponse && (
|
||||
<Button
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../../common'
|
||||
import { DiscussionContext } from '../../../common/context';
|
||||
import HoverCard from '../../../common/HoverCard';
|
||||
import { ContentTypes } from '../../../data/constants';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../../data/hooks';
|
||||
import { useUserPostingEnabled } from '../../../data/hooks';
|
||||
import { fetchThread } from '../../../posts/data/thunks';
|
||||
import LikeButton from '../../../posts/post/LikeButton';
|
||||
import { useActions } from '../../../utils';
|
||||
@@ -61,7 +61,7 @@ const Comment = ({
|
||||
const currentPage = useSelector(selectCommentCurrentPage(id));
|
||||
const sortedOrder = useSelector(selectCommentSortOrder);
|
||||
const actions = useActions(ContentTypes.COMMENT, id);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
|
||||
useEffect(() => {
|
||||
// If the comment has a parent comment, it won't have any children, so don't fetch them.
|
||||
@@ -119,10 +119,10 @@ const Comment = ({
|
||||
), [id, currentPage, sortedOrder]);
|
||||
|
||||
const handleAddCommentButton = useCallback(() => {
|
||||
if (userCanAddThreadInBlackoutDate) {
|
||||
if (isUserPrivilagedInPostingRestriction) {
|
||||
setReplying(true);
|
||||
}
|
||||
}, [userCanAddThreadInBlackoutDate]);
|
||||
}, [isUserPrivilagedInPostingRestriction]);
|
||||
|
||||
const handleCommentLike = useCallback(async () => {
|
||||
await dispatch(editComment(id, { voted: !voted }));
|
||||
@@ -265,7 +265,7 @@ const Comment = ({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
!isClosed && userCanAddThreadInBlackoutDate && (inlineReplies.length >= 5) && (
|
||||
!isClosed && isUserPrivilagedInPostingRestriction && (inlineReplies.length >= 5) && (
|
||||
<Button
|
||||
className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500"
|
||||
variant="plain"
|
||||
|
||||
@@ -24,12 +24,12 @@ import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
|
||||
import { addComment, editComment } from '../../data/thunks';
|
||||
import messages from '../../messages';
|
||||
|
||||
function CommentEditor({
|
||||
const CommentEditor = ({
|
||||
comment,
|
||||
edit,
|
||||
formClasses,
|
||||
onCloseEditor,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
id, threadId, parentId, rawBody, author, lastEdit,
|
||||
} = comment;
|
||||
@@ -60,7 +60,8 @@ function CommentEditor({
|
||||
|
||||
const initialValues = {
|
||||
comment: rawBody,
|
||||
editReasonCode: lastEdit?.reasonCode || (userIsStaff ? 'violates-guidelines' : ''),
|
||||
// eslint-disable-next-line react/prop-types
|
||||
editReasonCode: lastEdit?.reasonCode || (userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined),
|
||||
};
|
||||
|
||||
const handleCloseEditor = useCallback((resetForm) => {
|
||||
@@ -176,13 +177,13 @@ function CommentEditor({
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CommentEditor.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
author: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
lastEdit: PropTypes.object,
|
||||
lastEdit: PropTypes.shape({}),
|
||||
parentId: PropTypes.string,
|
||||
rawBody: PropTypes.string,
|
||||
threadId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -59,6 +59,7 @@ Factory.define('commentsResult')
|
||||
return Factory.buildList('comment', len, {
|
||||
thread_id: threadId,
|
||||
parent_id: parentId,
|
||||
endorsed,
|
||||
}, {
|
||||
endorsedBy: endorsed ? 'staff' : null,
|
||||
childCount,
|
||||
|
||||
@@ -20,16 +20,14 @@ export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussi
|
||||
* @param enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getThreadComments(
|
||||
threadId, {
|
||||
endorsed,
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
enableInContextSidebar = false,
|
||||
signal,
|
||||
} = {},
|
||||
) {
|
||||
export const getThreadComments = async (threadId, {
|
||||
endorsed,
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
enableInContextSidebar = false,
|
||||
signal,
|
||||
} = {}) => {
|
||||
const params = snakeCaseObject({
|
||||
threadId,
|
||||
endorsed: EndorsementValue[endorsed],
|
||||
@@ -42,7 +40,7 @@ export async function getThreadComments(
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient().get(getCommentsApiUrl(), { params: { ...params, signal } });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a responses to a comment.
|
||||
@@ -51,13 +49,11 @@ export async function getThreadComments(
|
||||
* @param {number=} pageSize
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getCommentResponses(
|
||||
commentId, {
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
} = {},
|
||||
) {
|
||||
export const getCommentResponses = async (commentId, {
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
} = {}) => {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const params = snakeCaseObject({
|
||||
page,
|
||||
@@ -68,7 +64,7 @@ export async function getCommentResponses(
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(url, { params });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a comment.
|
||||
@@ -78,13 +74,13 @@ export async function getCommentResponses(
|
||||
* @param {boolean} enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function postComment(comment, threadId, parentId = null, enableInContextSidebar = false) {
|
||||
export const postComment = async (comment, threadId, parentId = null, enableInContextSidebar = false) => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getCommentsApiUrl(), snakeCaseObject({
|
||||
threadId, raw_body: comment, parentId, enableInContextSidebar,
|
||||
}));
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates existing comment.
|
||||
@@ -96,13 +92,13 @@ export async function postComment(comment, threadId, parentId = null, enableInCo
|
||||
* @param {string=} editReasonCode The moderation reason code for editing.
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function updateComment(commentId, {
|
||||
export const updateComment = async (commentId, {
|
||||
comment,
|
||||
voted,
|
||||
flagged,
|
||||
endorsed,
|
||||
editReasonCode,
|
||||
}) {
|
||||
}) => {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const postData = snakeCaseObject({
|
||||
raw_body: comment,
|
||||
@@ -115,14 +111,14 @@ export async function updateComment(commentId, {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(url, postData, { headers: { 'Content-Type': 'application/merge-patch+json' } });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes existing comment.
|
||||
* @param {string} commentId ID of comment to delete
|
||||
*/
|
||||
export async function deleteComment(commentId) {
|
||||
export const deleteComment = async (commentId) => {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -141,7 +141,7 @@ const commentsSlice = createSlice({
|
||||
const commentRemoveListType = !endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED;
|
||||
|
||||
state.commentsInThreads[threadId][commentRemoveListType] = (
|
||||
state.commentsInThreads[threadId]?.[commentRemoveListType]?.filter(item => item !== commentId)
|
||||
state.commentsInThreads[threadId]?.[commentRemoveListType]?.filter(item => item !== commentId)
|
||||
);
|
||||
state.commentsInThreads[threadId][commentAddListtype] = [
|
||||
...state.commentsInThreads[threadId][commentAddListtype], payload.id,
|
||||
|
||||
@@ -26,6 +26,7 @@ Factory.define('thread')
|
||||
'type',
|
||||
'voted',
|
||||
'pinned',
|
||||
'copy_link',
|
||||
],
|
||||
author: 'test_user',
|
||||
author_label: 'Staff',
|
||||
|
||||
@@ -28,22 +28,20 @@ export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussio
|
||||
* @param {number} cohort
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getThreads(
|
||||
courseId, {
|
||||
topicIds,
|
||||
page,
|
||||
pageSize,
|
||||
textSearch,
|
||||
orderBy,
|
||||
following,
|
||||
view,
|
||||
author,
|
||||
flagged,
|
||||
threadType,
|
||||
countFlagged,
|
||||
cohort,
|
||||
} = {},
|
||||
) {
|
||||
export const getThreads = async (courseId, {
|
||||
topicIds,
|
||||
page,
|
||||
pageSize,
|
||||
textSearch,
|
||||
orderBy,
|
||||
following,
|
||||
view,
|
||||
author,
|
||||
flagged,
|
||||
threadType,
|
||||
countFlagged,
|
||||
cohort,
|
||||
} = {}) => {
|
||||
const params = snakeCaseObject({
|
||||
courseId,
|
||||
page,
|
||||
@@ -62,19 +60,19 @@ export async function getThreads(
|
||||
});
|
||||
const { data } = await getAuthenticatedHttpClient().get(getThreadsApiUrl(), { params });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a single thread.
|
||||
* @param {string} threadId
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getThread(threadId, courseId) {
|
||||
export const getThread = async (threadId, courseId) => {
|
||||
const params = { requested_fields: 'profile_image', course_id: courseId };
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url, { params });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Posts a new thread.
|
||||
@@ -90,7 +88,7 @@ export async function getThread(threadId, courseId) {
|
||||
* @param {boolean} enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function postThread(
|
||||
export const postThread = async (
|
||||
courseId,
|
||||
topicId,
|
||||
type,
|
||||
@@ -103,7 +101,7 @@ export async function postThread(
|
||||
anonymousToPeers,
|
||||
} = {},
|
||||
enableInContextSidebar = false,
|
||||
) {
|
||||
) => {
|
||||
const postData = snakeCaseObject({
|
||||
courseId,
|
||||
topicId,
|
||||
@@ -119,7 +117,7 @@ export async function postThread(
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getThreadsApiUrl(), postData);
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an existing thread.
|
||||
@@ -138,7 +136,7 @@ export async function postThread(
|
||||
* @param {string} closeReasonCode
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function updateThread(threadId, {
|
||||
export const updateThread = async (threadId, {
|
||||
flagged,
|
||||
voted,
|
||||
read,
|
||||
@@ -151,7 +149,7 @@ export async function updateThread(threadId, {
|
||||
pinned,
|
||||
editReasonCode,
|
||||
closeReasonCode,
|
||||
} = {}) {
|
||||
} = {}) => {
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
const patchData = snakeCaseObject({
|
||||
topicId,
|
||||
@@ -170,17 +168,17 @@ export async function updateThread(threadId, {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(url, patchData, { headers: { 'Content-Type': 'application/merge-patch+json' } });
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a thread.
|
||||
* @param {string} threadId
|
||||
*/
|
||||
export async function deleteThread(threadId) {
|
||||
export const deleteThread = async (threadId) => {
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a file.
|
||||
@@ -190,7 +188,7 @@ export async function deleteThread(threadId) {
|
||||
* @param {string} threadKey
|
||||
* @returns {Promise<{ location: string }>}
|
||||
*/
|
||||
export async function uploadFile(blob, filename, courseId, threadKey) {
|
||||
export const uploadFile = async (blob, filename, courseId, threadKey) => {
|
||||
const uploadUrl = `${getCoursesApiUrl()}${courseId}/upload`;
|
||||
const formData = new FormData();
|
||||
formData.append('thread_key', threadKey);
|
||||
@@ -200,4 +198,4 @@ export async function uploadFile(blob, filename, courseId, threadKey) {
|
||||
throw new Error(data.developer_message);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
export { showPostEditor } from './data';
|
||||
export { default as Post } from './post/Post';
|
||||
export { default as messages } from './post-actions-bar/messages';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
export { default as PostEditor } from './post-editor/PostEditor';
|
||||
export { default as PostsView } from './PostsView';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Close } from '@edx/paragon/icons';
|
||||
import Search from '../../../components/Search';
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { selectconfigLoadingStatus, selectEnableInContext } from '../../data/selectors';
|
||||
import { TopicSearchBar as IncontextSearch } from '../../in-context-topics/topic-search';
|
||||
import { postMessageToParent } from '../../utils';
|
||||
@@ -26,7 +26,7 @@ const PostActionsBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const loadingStatus = useSelector(selectconfigLoadingStatus);
|
||||
const enableInContext = useSelector(selectEnableInContext);
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
const { enableInContextSidebar, page } = useContext(DiscussionContext);
|
||||
|
||||
const handleCloseInContext = useCallback(() => {
|
||||
@@ -49,13 +49,15 @@ const PostActionsBar = () => {
|
||||
{intl.formatMessage(messages.title)}
|
||||
</h4>
|
||||
)}
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && userCanAddThreadInBlackoutDate && (
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && isUserPrivilagedInPostingRestriction && (
|
||||
<>
|
||||
{!enableInContextSidebar && <div className="border-right border-light-400 mx-3" />}
|
||||
<Button
|
||||
variant={enableInContextSidebar ? 'plain' : 'brand'}
|
||||
className={classNames('my-0 font-style border-0 line-height-24',
|
||||
{ 'px-3 py-10px border-0': enableInContextSidebar })}
|
||||
className={classNames(
|
||||
'my-0 font-style border-0 line-height-24',
|
||||
{ 'px-3 py-10px border-0': enableInContextSidebar },
|
||||
)}
|
||||
onClick={handleAddPost}
|
||||
size={enableInContextSidebar ? 'md' : 'sm'}
|
||||
>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsStaff,
|
||||
} from '../../data/selectors';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { EmptyPage } from '../../empty-posts';
|
||||
import {
|
||||
selectArchivedTopics,
|
||||
@@ -131,9 +132,11 @@ const PostEditor = ({
|
||||
}, [postId, topicId, post?.author, category, editExisting, commentsPagePath, location]);
|
||||
|
||||
// null stands for no cohort restriction ("All learners" option)
|
||||
const selectedCohort = useCallback((cohort) => (
|
||||
cohort === 'default' ? null : cohort),
|
||||
[]);
|
||||
const selectedCohort = useCallback(
|
||||
(cohort) => (
|
||||
cohort === 'default' ? null : cohort),
|
||||
[],
|
||||
);
|
||||
|
||||
const submitForm = useCallback(async (values, { resetForm }) => {
|
||||
if (editExisting) {
|
||||
@@ -288,18 +291,18 @@ const PostEditor = ({
|
||||
{enableInContext ? (
|
||||
<>
|
||||
{coursewareTopics?.map(section => (
|
||||
section?.children?.map(subsection => (
|
||||
<optgroup
|
||||
label={handleInContextSelectLabel(section, subsection)}
|
||||
key={subsection.id}
|
||||
>
|
||||
{subsection?.children?.map(unit => (
|
||||
<option key={unit.id} value={unit.id}>
|
||||
{unit.name || intl.formatMessage(messages.unnamedSubTopics)}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))
|
||||
section?.children?.map(subsection => (
|
||||
<optgroup
|
||||
label={handleInContextSelectLabel(section, subsection)}
|
||||
key={subsection.id}
|
||||
>
|
||||
{subsection?.children?.map(unit => (
|
||||
<option key={unit.id} value={unit.id}>
|
||||
{unit.name || intl.formatMessage(messages.unnamedSubTopics)}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))
|
||||
))}
|
||||
{(userIsStaff || userIsGroupTa || userHasModerationPrivileges) && (
|
||||
<optgroup label={intl.formatMessage(messages.archivedTopics)}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, useToggle } from '@edx/paragon';
|
||||
|
||||
import HTMLLoader from '../../../components/HTMLLoader';
|
||||
import { ContentActions } from '../../../data/constants';
|
||||
import { ContentActions, getFullUrl } from '../../../data/constants';
|
||||
import { selectorForUnitSubsection, selectTopicContext } from '../../../data/selectors';
|
||||
import { AlertBanner, Confirmation } from '../../common';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
@@ -37,7 +37,7 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const dispatch = useDispatch();
|
||||
const courseId = useSelector((state) => state.config.id);
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
const topic = useSelector(selectTopic(topicId));
|
||||
const getTopicSubsection = useSelector(selectorForUnitSubsection);
|
||||
const topicContext = useSelector(selectTopicContext(topicId));
|
||||
@@ -77,13 +77,13 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
}
|
||||
}, [closed, postId, reasonCodesEnabled, showClosePostModal]);
|
||||
|
||||
const handlePostCopyLink = useCallback(() => navigator.clipboard.writeText(
|
||||
`${window.location.origin}/${courseId}/posts/${postId}`,
|
||||
), [window.location.origin, postId, courseId]);
|
||||
const handlePostCopyLink = useCallback(() => {
|
||||
navigator.clipboard.writeText(getFullUrl(`${courseId}/posts/${postId}`));
|
||||
}, [window.location.origin, postId, courseId]);
|
||||
|
||||
const handlePostPin = useCallback(() => dispatch(updateExistingThread(
|
||||
postId, { pinned: !pinned },
|
||||
)), [postId, pinned]);
|
||||
const handlePostPin = useCallback(() => dispatch(
|
||||
updateExistingThread(postId, { pinned: !pinned }),
|
||||
), [postId, pinned]);
|
||||
|
||||
const handlePostReport = useCallback(() => {
|
||||
if (abuseFlagged) {
|
||||
@@ -188,8 +188,10 @@ const Post = ({ handleAddResponseButton }) => {
|
||||
</div>
|
||||
{(topicContext || topic) && (
|
||||
<div
|
||||
className={classNames('mt-14px font-style font-size-12',
|
||||
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter })}
|
||||
className={classNames(
|
||||
'mt-14px font-style font-size-12',
|
||||
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter },
|
||||
)}
|
||||
style={{ lineHeight: '20px' }}
|
||||
>
|
||||
<span className="text-gray-500" style={{ lineHeight: '20px' }}>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -7,7 +6,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Badge, Icon, Truncate } from '@edx/paragon';
|
||||
import { Badge, Icon } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
|
||||
import { PushPin } from '../../../components/icons';
|
||||
@@ -51,105 +50,107 @@ const PostLink = ({
|
||||
const canSeeReportedBadge = abuseFlagged || abuseFlaggedCount;
|
||||
const isPostRead = read || (!read && commentCount !== unreadCommentCount);
|
||||
|
||||
const checkIsSelected = useMemo(() => (
|
||||
window.location.pathname.includes(postId)),
|
||||
[window.location.pathname]);
|
||||
const checkIsSelected = useMemo(
|
||||
() => (
|
||||
window.location.pathname.includes(postId)),
|
||||
[window.location.pathname],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
className={
|
||||
<Link
|
||||
className={
|
||||
classNames('discussion-post p-0 text-decoration-none text-gray-900', {
|
||||
'border-bottom border-light-400': showDivider,
|
||||
})
|
||||
}
|
||||
to={linkUrl}
|
||||
aria-current={checkIsSelected ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(checkIsSelected || idx === 0) ? 0 : -1}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
classNames('d-flex flex-row pt-2 pb-2 px-4 border-primary-500 position-relative',
|
||||
to={linkUrl}
|
||||
aria-current={checkIsSelected ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(checkIsSelected || idx === 0) ? 0 : -1}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
classNames(
|
||||
'd-flex flex-row pt-2 pb-2 px-4 border-primary-500 position-relative',
|
||||
{ 'bg-light-300': isPostRead },
|
||||
{ 'post-summary-card-selected': id === selectedPostId })
|
||||
{ 'post-summary-card-selected': id === selectedPostId },
|
||||
)
|
||||
}
|
||||
>
|
||||
<PostAvatar
|
||||
postType={type}
|
||||
author={author}
|
||||
authorLabel={authorLabel}
|
||||
fromPostLink
|
||||
read={isPostRead}
|
||||
/>
|
||||
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill" style={{ marginBottom: '-3px' }}>
|
||||
<div className="d-flex align-items-center pb-0 mb-0 flex-fill font-weight-500">
|
||||
<Truncate lines={1} className="mr-1.5" whiteSpace>
|
||||
<span
|
||||
class={
|
||||
classNames('font-weight-500 font-size-14 text-primary-500 font-style align-bottom',
|
||||
{ 'font-weight-bolder': !read })
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span class="align-bottom"> </span>
|
||||
<span
|
||||
class="text-gray-700 font-weight-normal font-size-14 font-style align-bottom"
|
||||
>
|
||||
{isPostPreviewAvailable(previewBody)
|
||||
? previewBody
|
||||
: intl.formatMessage(messages.postWithoutPreview)}
|
||||
</span>
|
||||
</Truncate>
|
||||
{showAnsweredBadge && (
|
||||
<Icon src={CheckCircle} className="text-success font-weight-500 ml-auto badge-padding" data-testid="check-icon">
|
||||
<span className="sr-only">{' '}answered</span>
|
||||
</Icon>
|
||||
)}
|
||||
{canSeeReportedBadge && (
|
||||
<Badge
|
||||
variant="danger"
|
||||
data-testid="reported-post"
|
||||
className={`font-weight-500 badge-padding ${showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
>
|
||||
{intl.formatMessage(messages.contentReported)}
|
||||
<span className="sr-only">{' '}reported</span>
|
||||
</Badge>
|
||||
)}
|
||||
{pinned && (
|
||||
<Icon
|
||||
src={PushPin}
|
||||
className={`post-summary-icons-dimensions text-gray-700
|
||||
${canSeeReportedBadge || showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
/>
|
||||
>
|
||||
<PostAvatar
|
||||
postType={type}
|
||||
author={author}
|
||||
authorLabel={authorLabel}
|
||||
fromPostLink
|
||||
read={isPostRead}
|
||||
/>
|
||||
<div className="d-flex flex-column flex-fill" style={{ minWidth: 0 }}>
|
||||
<div className="d-flex flex-column justify-content-start mw-100 flex-fill" style={{ marginBottom: '-3px' }}>
|
||||
<div className="d-flex align-items-center pb-0 mb-0 flex-fill">
|
||||
<div className="text-truncate mr-1">
|
||||
<span className={classNames(
|
||||
'font-weight-500 font-size-14 text-primary-500 font-style align-bottom mr-1',
|
||||
{ 'font-weight-bolder': !read },
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span className="text-gray-700 font-weight-normal font-size-14 font-style align-bottom">
|
||||
{isPostPreviewAvailable(previewBody) ? previewBody : intl.formatMessage(messages.postWithoutPreview)}
|
||||
</span>
|
||||
</div>
|
||||
{showAnsweredBadge && (
|
||||
<Icon
|
||||
data-testid="check-icon"
|
||||
src={CheckCircle}
|
||||
className="text-success font-weight-500 ml-auto badge-padding"
|
||||
>
|
||||
<span className="sr-only">{' '}answered</span>
|
||||
</Icon>
|
||||
)}
|
||||
{canSeeReportedBadge && (
|
||||
<Badge
|
||||
variant="danger"
|
||||
data-testid="reported-post"
|
||||
className={`font-weight-500 badge-padding ${showAnsweredBadge ? 'ml-2' : 'ml-auto'}`}
|
||||
>
|
||||
{intl.formatMessage(messages.contentReported)}
|
||||
<span className="sr-only">{' '}reported</span>
|
||||
</Badge>
|
||||
)}
|
||||
{pinned && (
|
||||
<Icon
|
||||
src={PushPin}
|
||||
className={classNames('post-summary-icons-dimensions text-gray-700', {
|
||||
'ml-2': canSeeReportedBadge || showAnsweredBadge,
|
||||
'ml-auto': !canSeeReportedBadge && !showAnsweredBadge,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<AuthorLabel
|
||||
author={author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
/>
|
||||
<PostSummaryFooter
|
||||
postId={id}
|
||||
voted={voted}
|
||||
voteCount={voteCount}
|
||||
following={following}
|
||||
commentCount={commentCount}
|
||||
unreadCommentCount={unreadCommentCount}
|
||||
groupId={groupId}
|
||||
groupName={groupName}
|
||||
createdAt={createdAt}
|
||||
preview
|
||||
showNewCountLabel={isPostRead}
|
||||
/>
|
||||
</div>
|
||||
<AuthorLabel
|
||||
author={author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
/>
|
||||
<PostSummaryFooter
|
||||
postId={id}
|
||||
voted={voted}
|
||||
voteCount={voteCount}
|
||||
following={following}
|
||||
commentCount={commentCount}
|
||||
unreadCommentCount={unreadCommentCount}
|
||||
groupId={groupId}
|
||||
groupName={groupName}
|
||||
createdAt={createdAt}
|
||||
preview
|
||||
showNewCountLabel={isPostRead}
|
||||
/>
|
||||
</div>
|
||||
{!showDivider && pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
</>
|
||||
</div>
|
||||
{!showDivider && pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const TopicGroupBase = ({
|
||||
|
||||
const renderFilteredTopics = useMemo(() => {
|
||||
if (!hasFilteredSubtopics) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable no-unused-vars, react/forbid-prop-types */
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function countFilteredTopics(topicsSelector, provider) {
|
||||
? item.name.toLowerCase().includes(query)
|
||||
: true
|
||||
));
|
||||
count += nonCoursewareTopicsList?.length;
|
||||
count += nonCoursewareTopicsList?.length ?? 0;
|
||||
// Counting legacy topics
|
||||
if (provider === DiscussionProvider.LEGACY) {
|
||||
const categories = topicsSelector?.categoryIds;
|
||||
|
||||
@@ -17,6 +17,7 @@ const DiscussionsProductTour = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{!isEmpty(config) && (
|
||||
<ProductTour
|
||||
|
||||
@@ -267,28 +267,6 @@ export const filterPosts = (posts, filterBy) => uniqBy(posts, 'id').filter(
|
||||
post => (filterBy.startsWith('un') ? !post[filterBy.slice(2)] : post[filterBy]),
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to make a check if date is in given range
|
||||
* @param {Date} date this date to be checked in range
|
||||
* @param {Date} start start date
|
||||
* @param {Date} end end date
|
||||
*/
|
||||
export function dateInDateRange(date, start, end) {
|
||||
return date >= start && date <= end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to make a check if date is in given range
|
||||
* @param {array} blackoutDateRanges start date
|
||||
* @return Boolean
|
||||
*/
|
||||
export function inBlackoutDateRange(blackoutDateRanges) {
|
||||
const now = new Date();
|
||||
return blackoutDateRanges.some(
|
||||
(blackoutDateRange) => dateInDateRange(now, new Date(blackoutDateRange.start), new Date(blackoutDateRange.end)),
|
||||
);
|
||||
}
|
||||
|
||||
export function handleKeyDown(event) {
|
||||
const { key } = event;
|
||||
if (key !== 'ArrowDown' && key !== 'ArrowUp') { return; }
|
||||
|
||||
51
src/i18n/index.js
Normal file
51
src/i18n/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
|
||||
import arMessages from './messages/ar.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
import csMessages from './messages/cs.json';
|
||||
import deMessages from './messages/de_DE.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import esARMessages from './messages/es_AR.json';
|
||||
import esESMessages from './messages/es_ES.json';
|
||||
import faIRMessages from './messages/fa_IR.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import frCAMessages from './messages/fr_CA.json';
|
||||
import frFRMessages from './messages/fr_FR.json';
|
||||
import hiMessages from './messages/hi.json';
|
||||
import itITMessages from './messages/it_IT.json';
|
||||
import plMessages from './messages/pl.json';
|
||||
import ptPTMessages from './messages/pt_PT.json';
|
||||
import ruMessages from './messages/ru.json';
|
||||
import trTRMessages from './messages/tr_TR.json';
|
||||
import ukMessages from './messages/uk.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
|
||||
const appMessages = {
|
||||
ar: arMessages,
|
||||
de: deMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
'fr-ca': frCAMessages,
|
||||
'fr-fr': frFRMessages,
|
||||
'it-it': itITMessages,
|
||||
pl: plMessages,
|
||||
'tr-tr': trTRMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
'pt-pt': ptPTMessages,
|
||||
uk: ukMessages,
|
||||
ru: ruMessages,
|
||||
hi: hiMessages,
|
||||
cs: csMessages,
|
||||
'es-AR': esARMessages,
|
||||
'es-ES': esESMessages,
|
||||
'fa-IR': faIRMessages,
|
||||
};
|
||||
|
||||
export default [
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
paragonMessages,
|
||||
appMessages,
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
import arMessages from './messages/ar.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
import deMessages from './messages/de_DE.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import frCAMessages from './messages/fr_CA.json';
|
||||
import frFRMessages from './messages/fr_FR.json';
|
||||
import itITMessages from './messages/it_IT.json';
|
||||
import plMessages from './messages/pl.json';
|
||||
import trTRMessages from './messages/tr_TR.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
|
||||
const messages = {
|
||||
ar: arMessages,
|
||||
de: deMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
'fr-ca': frCAMessages,
|
||||
'fr-fr': frFRMessages,
|
||||
'it-it': itITMessages,
|
||||
pl: plMessages,
|
||||
'tr-tr': trTRMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "مواد المساق",
|
||||
"learn.course.tabs.navigation.overflow.menu": "المزيد...",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
"discussions.topics.backAlt": "العودة إلى قائمة المواضيع",
|
||||
"discussions.topics.discussions": "{count، plural, =0 {لا مناقشات} one {مناقشة واحدة} two {مناقشتان} few {# مناقشات} many {# مناقشة} other {# مناقشات}",
|
||||
"discussions.topics.questions": "{count، plural, =0 {لا مناقشات} one {سؤال واحد} two {سؤالان} few {# اسئلة} many {# سؤالًا} other {# أسئلة}",
|
||||
"discussions.topics.reported": "تم الإبلاغ عن {reported}",
|
||||
"discussions.topics.previouslyReported": "تم الإبلاغ عن {previouslyReported} من قبل",
|
||||
"discussions.topics.find.label": "البحث في المواضيع",
|
||||
"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.unnamed.section.label": "قسم بدون اسم",
|
||||
"discussions.topics.unnamed.subsection.label": "قسم فرعي بدون اسم",
|
||||
"discussions.subtopics.unnamed.topic.label": "موضوع لم يذكر اسمه",
|
||||
"discussions.topics.title": "لا يوجد موضوع",
|
||||
"discussions.topics.createTopic": "Please contact you admin to create a topic",
|
||||
"discussions.topics.nothing": "Nothing here yet",
|
||||
"discussions.topics.nothing": "لا شيء هنا حتى الان",
|
||||
"discussions.topics.archived.label": "مؤرشف",
|
||||
"discussions.learner.reported": "تم الإبلاغ عن {reported}",
|
||||
"discussions.learner.previouslyReported": "تم الإبلاغ عن {previouslyReported} من قبل",
|
||||
@@ -59,10 +59,7 @@
|
||||
"discussions.authors.label.ta": "أستاذ مساعد",
|
||||
"discussions.learner.loadMostPosts": "تواريخ تعطيل نشطة حاليا. لا يمكن النشر في المناقشات خلال هذه الفترة.",
|
||||
"discussions.post.anonymous.author": "مجهول",
|
||||
"discussion.banner.welcomeMessage": "🎉 مرحبًا بك في تجربة المناقشات الجديدة والمحسّنة!",
|
||||
"discussion.banner.learnMore": "معرفة المزيد",
|
||||
"discussion.banner.shareFeedback": "شاركنا رأيك",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is temporarily disabled by the course team",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is disabled by the course team",
|
||||
"discussions.editor.image.warning.message": "لن تظهر الصور التي يزيد عرضها أو ارتفاعها عن 999 بكسل عند عرض المنشور أو الرد او التعليق باستخدام مناقشات المساق المضمّنة",
|
||||
"discussions.editor.image.warning.title": "تحذير!",
|
||||
"discussions.editor.image.warning.dismiss": "حسنًا",
|
||||
@@ -142,7 +139,7 @@
|
||||
"discussions.post.editor.anonymousPost": "النشر كمجهول",
|
||||
"discussions.post.editor.anonymousToPeersPost": "انشر ﻷقرانك كمجهول",
|
||||
"discussions.editor.posts.editReasonCode": "سبب التعديل",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.editor.posts.showPreview.button": "عرض المعاينة",
|
||||
"discussions.topic.noName.label": "تصنيف دون اسم",
|
||||
"discussions.subtopic.noName.label": "تصنيف فرعي دون اسم",
|
||||
"discussions.posts.filter.showALl": "عرض الكل",
|
||||
@@ -164,22 +161,22 @@
|
||||
"discussions.posts.sort.voteCount": "الأكثر إعجابًا",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {جميع}\n true {ما كتبته من}\n other {{own}}\n } \n {type, select,\ndiscussion {المناقشات}\nquestion {الأسئلة}\nall {المنشورات}\nother {{type}}\n}\n {status, select,\n statusAll {}\n statusUnread {غير المقروءة}\n statusFollowing {التي تتابعها}\n statusReported {المُبلّغ عنها}\n statusUnanswered {دون إجابة}\n statusUnresponded {دون رد}\n other {{status}}\n } و المنشورة {cohortType, select,\n all {}\n group {ضمن {cohort}}\n other {{cohortType}}\n }، مرتبة حسب {sort, select,\n lastActivityAt {أحدث نشاط}\n commentCount {أكثر نشاط}\n voteCount {أكثر إعجاب}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "مجهول",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"discussions.post.addResponse": "أضف الرد",
|
||||
"discussions.post.lastResponse": "آخر رد {time}",
|
||||
"discussions.post.postedOn": "منشور في {time} من طرف {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "تم الإبلاغ",
|
||||
"discussions.post.following": "جاري المتابعة",
|
||||
"discussions.post.follow": "متابعة",
|
||||
"discussions.post.followed": "Followed",
|
||||
"discussions.post.notFollowed": "Not Followed",
|
||||
"discussions.post.followed": "تابع",
|
||||
"discussions.post.notFollowed": "ليس متابع",
|
||||
"discussions.post.answered": "تمّت الإجابة",
|
||||
"discussions.post.unFollow": "إلغاء المتابعة",
|
||||
"discussions.post.like": "أعجبني",
|
||||
"discussions.post.removeLike": "إلغاء الإغجاب",
|
||||
"discussions.post.liked": "liked",
|
||||
"discussions.post.likes": "likes",
|
||||
"discussions.post.liked": "اعحبني",
|
||||
"discussions.post.likes": "الإعجابات",
|
||||
"discussions.post.viewActivity": "عرض النشاط",
|
||||
"discussions.post.activity": "Activity",
|
||||
"discussions.post.activity": "النشاط",
|
||||
"discussions.post.closed": "منشور مقفل أمام الردود والتعليقات",
|
||||
"discussions.post.relatedTo": "متعلق بـ",
|
||||
"discussions.editor.delete.post.title": "حذف المنشور",
|
||||
@@ -196,7 +193,7 @@
|
||||
"discussions.post.editedBy": "عدّله",
|
||||
"discussions.post.editReason": "السبب",
|
||||
"discussions.post.postWithoutPreview": "المعاينة غير متاحة",
|
||||
"discussions.post.follow.description": "you are following this post",
|
||||
"discussions.post.follow.description": "أنت تتابع هذا المنشور/الرد",
|
||||
"discussions.post.unfollow.description": "you are not following this post",
|
||||
"discussions.topics.sort.message": "مرتبة حسب {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "الأحدث نشاطًا",
|
||||
@@ -204,11 +201,11 @@
|
||||
"discussions.topics.sort.courseStructure": "هيكل المساق",
|
||||
"discussions.topics.unnamed.label": "فئة بدون اسم",
|
||||
"discussions.subtopics.unnamed.label": "فئة فرعية بدون اسم",
|
||||
"tour.action.advance": "Next",
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!",
|
||||
"tour.action.advance": "التالي",
|
||||
"tour.action.dismiss": "تجاهل",
|
||||
"tour.action.end": "حسنًا",
|
||||
"tour.body.notRespondedFilter": "يمكنك الآن تصفية المناقشات للعثور على مشاركات بدون رد.",
|
||||
"tour.title.notRespondedFilter": "خيار تصفية جديد!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.responseSortTour": "فرز الردود!"
|
||||
}
|
||||
211
src/i18n/messages/cs.json
Normal file
211
src/i18n/messages/cs.json
Normal file
@@ -0,0 +1,211 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Materiály ke kurzu",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Více...",
|
||||
"discussions.topics.backAlt": "Zpět na seznam témat",
|
||||
"discussions.topics.discussions": "{count, plural,\n =0 {Diskuzí}\n one {# Diskuze}\n other {# Diskuzí}\n }",
|
||||
"discussions.topics.questions": "{count, plural,\n =0 {Otázek}\n one {# Otázka}\n other {# Otázky}\n }",
|
||||
"discussions.topics.reported": "{reported} nahlášeno",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} minule nahlášeno",
|
||||
"discussions.topics.find.label": "Hledat témata",
|
||||
"discussions.topics.unnamed.section.label": "Nejmenovaná sekce",
|
||||
"discussions.topics.unnamed.subsection.label": "Nepojmenovaná podsekce",
|
||||
"discussions.subtopics.unnamed.topic.label": "Nepojmenované téma",
|
||||
"discussions.topics.title": "Žádné téma neexistuje",
|
||||
"discussions.topics.createTopic": "Chcete-li vytvořit téma, kontaktujte svého správce",
|
||||
"discussions.topics.nothing": "Ještě tu nic není",
|
||||
"discussions.topics.archived.label": "Archivováno",
|
||||
"discussions.learner.reported": "{reported} nahlášeno",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} minule nahlášeno",
|
||||
"discussions.learner.lastLogin": "Poslední aktivita {lastActiveTime}",
|
||||
"discussions.learner.loadMostLearners": "Načíst více",
|
||||
"discussions.learner.back": "Zpět",
|
||||
"discussions.learner.activityForLearner": "Aktivita pro {username}",
|
||||
"discussions.learner.mostActivity": "Nejvíce aktivity",
|
||||
"discussions.learner.reportedActivity": "Nahlášené aktivity",
|
||||
"discussions.learner.recentActivity": "Nedávné aktivity",
|
||||
"discussions.learner.sortFilterStatus": "Všichni studenti seřazení podle {sort, select,\n flagged {nahlášené aktivity}\n activity {nejvíce aktivity}\n other {{sort}}\n }",
|
||||
"discussion.learner.allActivity": "Všechna aktivita",
|
||||
"discussion.learner.posts": "Příspěvky",
|
||||
"discussions.actions.button.alt": "Nabídka akcí",
|
||||
"discussions.actions.copylink": "Kopírovat odkaz",
|
||||
"discussions.actions.edit": "Editovat",
|
||||
"discussions.actions.pin": "Připnout",
|
||||
"discussions.actions.unpin": "Odepnout",
|
||||
"discussions.actions.delete": "Smazat",
|
||||
"discussions.confirmation.button.confirm": "Potvrdit",
|
||||
"discussions.actions.close": "Zavřít",
|
||||
"discussions.actions.reopen": "Znovu otevřít",
|
||||
"discussions.actions.report": "Nahlásit",
|
||||
"discussions.actions.unreport": "Zrušit nahlášení",
|
||||
"discussions.actions.endorse": "Schválit",
|
||||
"discussions.actions.unendorse": "Nepodporovat",
|
||||
"discussions.actions.markAnswered": "Označit jako zodpovězené",
|
||||
"discussions.actions.unMarkAnswered": "Označit jako nezodpovězené",
|
||||
"discussions.modal.confirmation.button.cancel": "Zrušit",
|
||||
"discussions.empty.allTopics": "Veškerá aktivita diskuze pro tato témata se zobrazí zde.",
|
||||
"discussions.empty.allPosts": "Veškerá aktivita diskuze pro vaše kurzy se zobrazí zde.",
|
||||
"discussions.empty.myPosts": "Příspěvky, se kterými jste interagovali, se zobrazí zde.",
|
||||
"discussions.empty.topic": "Veškerá aktivita diskuze pro toto téma se zobrazí zde.",
|
||||
"discussions.empty.title": "Ještě tu nic není",
|
||||
"discussions.empty.noPostSelected": "Nevybrány žádné příspěvky",
|
||||
"discussions.empty.noTopicSelected": "Nevybrána žádná témata",
|
||||
"discussions.sidebar.noResultsFound": "Nebyly nalezeny žádné výsledky",
|
||||
"discussions.sidebar.differentKeywords": "Zkuste vyhledávat různá klíčová slova",
|
||||
"discussions.sidebar.removeKeywords": "Zkuste vyhledávat klíčová slova nebo odstranit některé filtry",
|
||||
"discussions.sidebar.removeKeywordsOnly": "Zkuste vyhledávat různá klíčová slova",
|
||||
"discussions.sidebar.removeFilters": "Zkuste odstranit některé filtry",
|
||||
"discussions.empty.iconAlt": "Prázdný",
|
||||
"discussions.authors.label.staff": "Učitelé",
|
||||
"discussions.authors.label.ta": "TA",
|
||||
"discussions.learner.loadMostPosts": "Načíst další příspěvky",
|
||||
"discussions.post.anonymous.author": "anonymní",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is disabled by the course team",
|
||||
"discussions.editor.image.warning.message": "Obrázky mající šířku nebo výšku větší než 999px nebudou viditelné, když příspěvek, odpověď nebo komentář zobrazíte pomocí diskuzí v rámci kurzu",
|
||||
"discussions.editor.image.warning.title": "Varování!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Témata",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Zobrazit vše",
|
||||
"discussions.navigation.navigationBar.allPosts": "Všechny příspěvky",
|
||||
"discussions.navigation.navigationBar.allTopics": "Témata",
|
||||
"discussions.navigation.navigationBar.myPosts": "Moje příspěvky",
|
||||
"discussions.navigation.navigationBar.learners": "Studenti",
|
||||
"discussions.comments.comment.addComment": "Přidat komentář",
|
||||
"discussions.comments.comment.addResponse": "Přidat odpověď",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Obsah byl nahlášen učiteli ke kontrole",
|
||||
"discussions.actions.back.alt": "Zpět na seznam",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\n =0 {Žádné odpovědi}\n one {Zobrazení # odpovědi}\n other {Zobrazení # odpovědí}\n }",
|
||||
"discussions.comments.comment.endorsedResponseCount": "{num, plural,\n =0 {Žádné schválené odpovědi}\n one {Zobrazení # schválené odpovědi}\n other {Zobrazení # schválených odpovědí}\n }",
|
||||
"discussions.comments.comment.loadMoreComments": "Načíst další komentáře",
|
||||
"discussions.comments.comment.loadMoreResponses": "Načíst další odpovědi",
|
||||
"discussions.comments.comment.visibility": "Tento příspěvek je viditelný pro {group, select,\n null {Všichni}\n other {{group}}\n }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select,\n discussion {Discussion}\n question {Question}\n other {{postType}}\n } odesláno {relativeTime} uživatelem",
|
||||
"discussions.comments.comment.commentTime": "Odesláno {relativeTime}",
|
||||
"discussions.comments.comment.answer": "Odpověď",
|
||||
"discussions.comments.comment.answeredlabel": "Označit jako zodpovězené",
|
||||
"discussions.comments.comment.endorsed": "Schválené",
|
||||
"discussions.comments.comment.endorsedlabel": "Schváleno uživatelem",
|
||||
"discussions.actions.label": "Nabídka akcí",
|
||||
"discussions.editor.submit": "Odeslat",
|
||||
"discussions.editor.submitting": "Odesílám",
|
||||
"discussions.editor.cancel": "Zrušit",
|
||||
"discussions.editor.error.empty": "Obsah příspěvku nemůže být prázdný.",
|
||||
"discussions.editor.delete.response.title": "Smazat obsah",
|
||||
"discussions.editor.delete.response.description": "Opravdu chcete trvale smazat tento obsah?",
|
||||
"discussions.editor.delete.comment.title": "Smazat komentář",
|
||||
"discussions.editor.delete.comment.description": "Opravdu chcete smazat tento komentář?",
|
||||
"discussions.delete.confirmation.button.delete": "Smazat",
|
||||
"discussions.editor.response.response.title": "Nahlásit nevhodný obsah?",
|
||||
"discussions.editor.response.description": "Tým pro moderování diskuze zkontroluje tento obsah a podnikne příslušné kroky.",
|
||||
"discussions.editor.report.comment.title": "Nahlásit nevhodný obsah?",
|
||||
"discussions.editor.report.comment.description": "Tým pro moderování diskuze zkontroluje tento obsah a podnikne příslušné kroky.",
|
||||
"discussions.editor.comments.editReasonCode": "Důvod úpravy",
|
||||
"discussions.editor.posts.editReasonCode.error": "Zvolit důvod úpravy",
|
||||
"discussions.comment.comments.editedBy": "Úprava od",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Důvod",
|
||||
"discussions.post.closedBy": "Příspěvek zavřen uživatelem",
|
||||
"discussion.comment.time": "Před {time}",
|
||||
"discussion.thread.notFound": "Vlákno nebylo nelezeno",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select,\n false {Nejstarší první}\n true {Nejnovější první}\n other {{sort}}\n }",
|
||||
"discussions.app.title": "Diskuze",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Prohledejte příspěvky",
|
||||
"discussions.posts.actionBar.search": "{page, select,\n topics {Hledat témata}\n posts {Hledat všechny příspěvky}\n learners {Hledat studenty}\n myPosts {Hledat všechny příspěvky}\n other {{page}}\n }",
|
||||
"discussions.actionBar.searchInfo": "Zobrazuji {count} výsledků pro \"{text}\"",
|
||||
"discussions.actionBar.searchRewriteInfo": "Nenylezeny žádné výsledky pro \"{searchString}\". Zobrazuji {count} výsledků pro \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchInfoSearching": "Vyhledávám...",
|
||||
"discussions.actionBar.clearSearch": "Vymazat vyhledávání",
|
||||
"discussion.posts.actionBar.add": "Přidat příspěvek",
|
||||
"discussion.posts.actionBar.close": "Zavřít",
|
||||
"discussions.post.editor.type": "Typ příspěvku",
|
||||
"discussions.post.editor.addPostHeading": "Přidat příspěvek",
|
||||
"discussions.post.editor.editPostHeading": "Upravit příspěvek",
|
||||
"discussions.post.editor.typeDescription": "Otázky vyvolávají problémy, které vyžadují odpovědi. Diskuse sdílejí nápady a zahajují konverzace.",
|
||||
"discussions.post.editor.required": "Povinný",
|
||||
"discussions.post.editor.questionType": "Otázka",
|
||||
"discussions.post.editor.questionDescription": "Upozorňovat na problémy, které vyžadují odpovědi",
|
||||
"discussions.post.editor.discussionType": "Diskuze",
|
||||
"discussions.post.editor.discussionDescription": "Sdílet nápady a začít konverzaci",
|
||||
"discussions.post.editor.topicArea": "Tematická oblast",
|
||||
"discussions.post.editor.topicAreaDescription": "Přidejte svůj příspěvek k relevantnímu tématu, aby ho ostatní mohli najít.",
|
||||
"discussions.post.editor.cohortVisibility": "Viditelnost kohorty",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "Všichni studenti",
|
||||
"discussions.post.editor.title": "Název příspěvku",
|
||||
"discussions.post.editor.titleDescription": "Přidejte jasný a popisný název, abyste podpořili účast.",
|
||||
"discussions.post.editor.title.error": "Název příspěvku nemůže být prázdný.",
|
||||
"discussions.post.editor.content.error": "Obsah příspěvku nemůže být prázdný.",
|
||||
"discussions.post.editor.questionText": "Vaše otázka či nápad (povinné)",
|
||||
"discussions.post.editor.preview": "Náhled",
|
||||
"discussions.post.editor.followPost": "Sledovat tento příspěvek",
|
||||
"discussions.post.editor.anonymousPost": "Odeslat anonymně",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Anonymně přispívat spolužákům",
|
||||
"discussions.editor.posts.editReasonCode": "Důvod úpravy",
|
||||
"discussions.editor.posts.showPreview.button": "Zobrazit náhled",
|
||||
"discussions.topic.noName.label": "Nepojmenovaná kategorie",
|
||||
"discussions.subtopic.noName.label": "Nepojmenovaná podkategorie",
|
||||
"discussions.posts.filter.showALl": "Zobrazit vše",
|
||||
"discussions.posts.filter.discussions": "Diskuze",
|
||||
"discussions.posts.filter.questions": "Otázky",
|
||||
"discussions.posts.filter.message": "Stav: {filterBy}",
|
||||
"discussions.posts.status.filter.anyStatus": "Jakýkoli stav",
|
||||
"discussions.posts.status.filter.unread": "Nepřečtený",
|
||||
"discussions.posts.status.filter.following": "Sledováno",
|
||||
"discussions.posts.status.filter.reported": "Nahlášeno",
|
||||
"discussions.posts.status.filter.unanswered": "Nezodpovězené",
|
||||
"discussions.posts.status.filter.unresponded": "Nereagované",
|
||||
"discussions.posts.filter.myPosts": "Moje příspěvky",
|
||||
"discussions.posts.filter.myDiscussions": "Moje diskuze",
|
||||
"discussions.posts.filter.myQuestions": "Moje otázky",
|
||||
"discussions.posts.sort.message": "Seřazeno podle {sortBy}",
|
||||
"discussions.posts.sort.lastActivity": "Nedávné aktivity",
|
||||
"discussions.posts.sort.commentCount": "Největší aktivity",
|
||||
"discussions.posts.sort.voteCount": "Nejvíce lajků",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {Vše}\n true {Vlastní}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {nepřečtené}\n statusFollowing {sledované}\n statusReported {nahlášené}\n statusUnanswered {nezodpovězené}\n statusUnresponded {nereagované}\n other {{status}}\n } {type, select,\n discussion {diskuze}\n question {otázky}\n all {příspěvky}\n other {{type}}\n } {cohortType, select,\n all {}\n group {v {cohort}}\n other {{cohortType}}\n } seřazeno podle {sort, select,\n lastActivityAt {nedávné aktivity}\n commentCount {nejvíce aktivity}\n voteCount {nejvíce lajků}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonymní",
|
||||
"discussions.post.addResponse": "Přidat odpověď",
|
||||
"discussions.post.lastResponse": "Poslední odpověď {time}",
|
||||
"discussions.post.postedOn": "Odesláno {time} uživatelem {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Nahlášeno",
|
||||
"discussions.post.following": "Sledováno",
|
||||
"discussions.post.follow": "Sledovat",
|
||||
"discussions.post.followed": "Sledoval",
|
||||
"discussions.post.notFollowed": "Nesledoval",
|
||||
"discussions.post.answered": "Zodpovězeno",
|
||||
"discussions.post.unFollow": "Zrušit sledování",
|
||||
"discussions.post.like": "Lajk",
|
||||
"discussions.post.removeLike": "Zrušit lajk",
|
||||
"discussions.post.liked": "Lajknul",
|
||||
"discussions.post.likes": "Lajky",
|
||||
"discussions.post.viewActivity": "Zobrazit aktivitu",
|
||||
"discussions.post.activity": "Aktivita",
|
||||
"discussions.post.closed": "Příspěvek uzavřen pro odpovědi a komentáře",
|
||||
"discussions.post.relatedTo": "Související s",
|
||||
"discussions.editor.delete.post.title": "Odstranit příspěvek",
|
||||
"discussions.editor.delete.post.description": "Opravdu chcete smazat tento příspěvek?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Smazat",
|
||||
"discussions.editor.report.post.title": "Nahlásit nevhodný obsah?",
|
||||
"discussions.editor.report.post.description": "Tým pro moderování diskuze zkontroluje tento obsah a podnikne příslušné kroky.",
|
||||
"discussions.post.closePostModal.title": "Zavřít příspěvek",
|
||||
"discussions.post.closePostModal.text": "Zadejte důvod pro uzavření tohoto příspěvku. Toto se zobrazí pouze ostatním moderátorům.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "Důvod",
|
||||
"discussions.post.closePostModal.cancel": "Zrušit",
|
||||
"discussions.post.closePostModal.confirm": "Zavřít příspěvek",
|
||||
"discussions.post.label.new": "{count} nových",
|
||||
"discussions.post.editedBy": "Úprava od",
|
||||
"discussions.post.editReason": "Důvod",
|
||||
"discussions.post.postWithoutPreview": "Není k dispozici žádný náhled",
|
||||
"discussions.post.follow.description": "sledujete tento příspěvek",
|
||||
"discussions.post.unfollow.description": "nesledujete tento příspěvek",
|
||||
"discussions.topics.sort.message": "Seřazeno podle {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "Nedávná aktivita",
|
||||
"discussions.topics.sort.commentCount": "Nejvíce aktivity",
|
||||
"discussions.topics.sort.courseStructure": "Struktura kurzu",
|
||||
"discussions.topics.unnamed.label": "Nepojmenovaná kategorie",
|
||||
"discussions.subtopics.unnamed.label": "Nepojmenovaná podkategorie",
|
||||
"tour.action.advance": "Další",
|
||||
"tour.action.dismiss": "Zamítnout",
|
||||
"tour.action.end": "Dobře",
|
||||
"tour.body.notRespondedFilter": "Nyní můžete filtrovat diskuse a najít příspěvky bez odpovědi.",
|
||||
"tour.title.notRespondedFilter": "Nová možnost filtrování!",
|
||||
"tour.body.responseSortTour": "Odpovědi a komentáře jsou nyní seřazeny podle nejnovějších. Tuto možnost použijte ke změně pořadí řazení",
|
||||
"tour.title.responseSortTour": "Seřadit odpovědi!"
|
||||
}
|
||||
@@ -59,10 +59,7 @@
|
||||
"discussions.authors.label.ta": "TA",
|
||||
"discussions.learner.loadMostPosts": "Mehr Beiträge laden",
|
||||
"discussions.post.anonymous.author": "Anonym",
|
||||
"discussion.banner.welcomeMessage": "🎉 Willkommen beim neuen und verbesserten Diskussionserlebnis!",
|
||||
"discussion.banner.learnMore": "Lernen Sie mehr",
|
||||
"discussion.banner.shareFeedback": "Feedback teilen",
|
||||
"discussion.blackoutBanner.information": "Das Posten in Diskussionen wird vom Kursteam vorübergehend deaktiviert",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is disabled by the course team",
|
||||
"discussions.editor.image.warning.message": "Bilder mit einer Breite oder Höhe von mehr als 999 Pixel sind nicht sichtbar, wenn der Beitrag, die Antwort oder der Kommentar über Inline-Kursdiskussionen angezeigt werden",
|
||||
"discussions.editor.image.warning.title": "Warnung!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
|
||||
@@ -59,10 +59,7 @@
|
||||
"discussions.authors.label.ta": "ejército de reserva",
|
||||
"discussions.learner.loadMostPosts": "Cargar más mensajes\n",
|
||||
"discussions.post.anonymous.author": "anónimo",
|
||||
"discussion.banner.welcomeMessage": "🎉 ¡Bienvenido a la nueva y mejorada experiencia de debates!",
|
||||
"discussion.banner.learnMore": "Aprender más",
|
||||
"discussion.banner.shareFeedback": "Compartir comentarios",
|
||||
"discussion.blackoutBanner.information": "El equipo del curso deshabilita temporalmente la publicación en discusiones",
|
||||
"discussion.blackoutBanner.information": "El equipo del curso ha desactivado la publicación en los debates.",
|
||||
"discussions.editor.image.warning.message": "Las imágenes que tengan un ancho o alto superior a 999 px no serán visibles cuando la publicación, la respuesta o el comentario se vean mediante debates en línea del curso.",
|
||||
"discussions.editor.image.warning.title": "¡Advertencia!",
|
||||
"discussions.editor.image.warning.dismiss": "Aceptar",
|
||||
@@ -209,6 +206,6 @@
|
||||
"tour.action.end": "Okey",
|
||||
"tour.body.notRespondedFilter": "Ahora puede filtrar debates para encontrar publicaciones sin respuesta.",
|
||||
"tour.title.notRespondedFilter": "¡Nueva opción de filtrado!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.body.responseSortTour": "Las respuestas y los comentarios ahora se ordenan por los más recientes primero. Utilice esta opción para cambiar el orden de clasificación",
|
||||
"tour.title.responseSortTour": "¡Clasificar respuestas!"
|
||||
}
|
||||
211
src/i18n/messages/es_AR.json
Normal file
211
src/i18n/messages/es_AR.json
Normal file
@@ -0,0 +1,211 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Material del curso",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Más...",
|
||||
"discussions.topics.backAlt": "Volver a la lista de temas",
|
||||
"discussions.topics.discussions": "{count, plural, =0 {Discusión} one {# Discusión} other {# Discusiones} }",
|
||||
"discussions.topics.questions": "{count, plural, =0 {Pregunta} one {# Pregunta} other {# Preguntas} }",
|
||||
"discussions.topics.reported": "{reported} denunciado",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} informado anteriormente",
|
||||
"discussions.topics.find.label": "Buscar temas",
|
||||
"discussions.topics.unnamed.section.label": "Sección sin nombre",
|
||||
"discussions.topics.unnamed.subsection.label": "Subsección sin nombre",
|
||||
"discussions.subtopics.unnamed.topic.label": "Tema sin nombre",
|
||||
"discussions.topics.title": "No existe ningún tema",
|
||||
"discussions.topics.createTopic": "Póngase en contacto con su administrador para crear un tema",
|
||||
"discussions.topics.nothing": "Nada aquí todavía",
|
||||
"discussions.topics.archived.label": "Archivado",
|
||||
"discussions.learner.reported": "{reported} reportado",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} informado anteriormente",
|
||||
"discussions.learner.lastLogin": "Último activo {lastActiveTime}",
|
||||
"discussions.learner.loadMostLearners": "Cargar más",
|
||||
"discussions.learner.back": "atrás",
|
||||
"discussions.learner.activityForLearner": "Actividad para {username}",
|
||||
"discussions.learner.mostActivity": "La mayoría de la actividad",
|
||||
"discussions.learner.reportedActivity": "Actividad reportada",
|
||||
"discussions.learner.recentActivity": "Actividad reciente",
|
||||
"discussions.learner.sortFilterStatus": "Todos los alumnos ordenados por {sort, select, flagged {actividad notificada} activity {mayor actividad} other {{sort}} }",
|
||||
"discussion.learner.allActivity": "Toda la actividad",
|
||||
"discussion.learner.posts": "Publicaciones",
|
||||
"discussions.actions.button.alt": "Menú de acciones",
|
||||
"discussions.actions.copylink": "Copiar link",
|
||||
"discussions.actions.edit": "Editar",
|
||||
"discussions.actions.pin": "Alfiler",
|
||||
"discussions.actions.unpin": "Desprender",
|
||||
"discussions.actions.delete": "Borrar",
|
||||
"discussions.confirmation.button.confirm": "Confirmar",
|
||||
"discussions.actions.close": "Cerrar",
|
||||
"discussions.actions.reopen": "Reabrir",
|
||||
"discussions.actions.report": "Informar",
|
||||
"discussions.actions.unreport": "No informar",
|
||||
"discussions.actions.endorse": "Validar",
|
||||
"discussions.actions.unendorse": "Invalidar",
|
||||
"discussions.actions.markAnswered": "Marcar como respondida",
|
||||
"discussions.actions.unMarkAnswered": "Desmarcar como respondida",
|
||||
"discussions.modal.confirmation.button.cancel": "Cancelar",
|
||||
"discussions.empty.allTopics": "Toda la actividad de debate de estos temas se mostrará aquí.",
|
||||
"discussions.empty.allPosts": "Toda la actividad de debate de su curso se mostrará aquí.",
|
||||
"discussions.empty.myPosts": "Las publicaciones con las que has interactuado se mostrarán aquí.",
|
||||
"discussions.empty.topic": "Toda la actividad de debate sobre este tema se mostrará aquí.",
|
||||
"discussions.empty.title": "Nada aquí todavía",
|
||||
"discussions.empty.noPostSelected": "Ninguna publicación seleccionada",
|
||||
"discussions.empty.noTopicSelected": "Ningún tema seleccionado",
|
||||
"discussions.sidebar.noResultsFound": "No se han encontrado resultados",
|
||||
"discussions.sidebar.differentKeywords": "Intenta buscar diferentes palabras clave",
|
||||
"discussions.sidebar.removeKeywords": "Intente buscar diferentes palabras clave o elimine algunos filtros.",
|
||||
"discussions.sidebar.removeKeywordsOnly": "Intenta buscar diferentes palabras clave",
|
||||
"discussions.sidebar.removeFilters": "Prueba a eliminar algunos filtros.",
|
||||
"discussions.empty.iconAlt": "Vacío",
|
||||
"discussions.authors.label.staff": "Personal",
|
||||
"discussions.authors.label.ta": "ejército de reserva",
|
||||
"discussions.learner.loadMostPosts": "Cargar más entradas",
|
||||
"discussions.post.anonymous.author": "anónimo",
|
||||
"discussion.blackoutBanner.information": "El equipo del curso ha desactivado la publicación en los debates.",
|
||||
"discussions.editor.image.warning.message": "Las imágenes que tengan un ancho o alto superior a 999 px no serán visibles cuando la publicación, la respuesta o el comentario se vean mediante debates en línea del curso.",
|
||||
"discussions.editor.image.warning.title": "¡Advertencia!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Temas",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Mostrar todo",
|
||||
"discussions.navigation.navigationBar.allPosts": "Todos los mensajes",
|
||||
"discussions.navigation.navigationBar.allTopics": "Temas",
|
||||
"discussions.navigation.navigationBar.myPosts": "Mis publicaciones",
|
||||
"discussions.navigation.navigationBar.learners": "Estudiantes",
|
||||
"discussions.comments.comment.addComment": "Agregar 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 {Sin respuestas} one {Mostrando # respuestas} other {Mostrando # respuestas} }",
|
||||
"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 {Todos} other {{group}} }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select, discussion {Discusión} question {Pregunta} other {{postType}} } publicado {a0917e9bee}14c5z0",
|
||||
"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": "Enviar",
|
||||
"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": "Motivo 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": "{time} hace",
|
||||
"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 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}} }",
|
||||
"discussions.actionBar.searchInfo": "Mostrando resultados {count} para \"{text}\"",
|
||||
"discussions.actionBar.searchRewriteInfo": "No results found for \"{searchString}\". Showing {count} results for \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchInfoSearching": "Buscando...",
|
||||
"discussions.actionBar.clearSearch": "Borrar resultados",
|
||||
"discussion.posts.actionBar.add": "Agregar una publicación",
|
||||
"discussion.posts.actionBar.close": "Cerrar",
|
||||
"discussions.post.editor.type": "Tipo de publicación",
|
||||
"discussions.post.editor.addPostHeading": "Agregar una publicación",
|
||||
"discussions.post.editor.editPostHeading": "Editar publicación",
|
||||
"discussions.post.editor.typeDescription": "Las preguntas plantean problemas que necesitan respuestas. Las discusiones comparten ideas y comienzan conversaciones.",
|
||||
"discussions.post.editor.required": "Requerido",
|
||||
"discussions.post.editor.questionType": "Pregunta",
|
||||
"discussions.post.editor.questionDescription": "Plantear problemas que necesitan respuestas",
|
||||
"discussions.post.editor.discussionType": "Debate",
|
||||
"discussions.post.editor.discussionDescription": "Comparte ideas e inicia conversaciones",
|
||||
"discussions.post.editor.topicArea": "Área de temas",
|
||||
"discussions.post.editor.topicAreaDescription": "Agregar la publicación a un tema relevante para ayudar a otros a encontrarla.",
|
||||
"discussions.post.editor.cohortVisibility": "Visibilidad de la cohorte",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "Todos los estudiantes",
|
||||
"discussions.post.editor.title": "Título de la entrada",
|
||||
"discussions.post.editor.titleDescription": "Agregue un título claro y descriptivo para fomentar la participación.",
|
||||
"discussions.post.editor.title.error": "El título de la publicación no puede estar vacío.",
|
||||
"discussions.post.editor.content.error": "El contenido de la publicación no puede estar vacío.",
|
||||
"discussions.post.editor.questionText": "Su pregunta o idea (obligatorio)",
|
||||
"discussions.post.editor.preview": "Vista previa",
|
||||
"discussions.post.editor.followPost": "Seguir esta publicación",
|
||||
"discussions.post.editor.anonymousPost": "Publicar de forma anónima",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Publicar de forma anónima a los compañeros",
|
||||
"discussions.editor.posts.editReasonCode": "Motivo de la edición",
|
||||
"discussions.editor.posts.showPreview.button": "Mostrar vista previa",
|
||||
"discussions.topic.noName.label": "Categoría sin nombre",
|
||||
"discussions.subtopic.noName.label": "Subcategoría sin nombre",
|
||||
"discussions.posts.filter.showALl": "Mostrar todo",
|
||||
"discussions.posts.filter.discussions": "Debates",
|
||||
"discussions.posts.filter.questions": "Preguntas",
|
||||
"discussions.posts.filter.message": "Estado: {filterBy}",
|
||||
"discussions.posts.status.filter.anyStatus": "Cualquier estatus",
|
||||
"discussions.posts.status.filter.unread": "No leído",
|
||||
"discussions.posts.status.filter.following": "Siguiendo",
|
||||
"discussions.posts.status.filter.reported": "Reportado",
|
||||
"discussions.posts.status.filter.unanswered": "Sin respuesta",
|
||||
"discussions.posts.status.filter.unresponded": "Sin respuesta",
|
||||
"discussions.posts.filter.myPosts": "Mis publicaciones",
|
||||
"discussions.posts.filter.myDiscussions": "Mis debates",
|
||||
"discussions.posts.filter.myQuestions": "Mis preguntas",
|
||||
"discussions.posts.sort.message": "Ordenado por {sortBy}",
|
||||
"discussions.posts.sort.lastActivity": "Actividad reciente",
|
||||
"discussions.posts.sort.commentCount": "La mayoría de la actividad",
|
||||
"discussions.posts.sort.voteCount": "La mayoría me gusta",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anónimo",
|
||||
"discussions.post.addResponse": "Añadir respuesta",
|
||||
"discussions.post.lastResponse": "Última respuesta {time}",
|
||||
"discussions.post.postedOn": "Publicado {time} por {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Reportado",
|
||||
"discussions.post.following": "Siguiendo",
|
||||
"discussions.post.follow": "Seguir",
|
||||
"discussions.post.followed": "Seguido",
|
||||
"discussions.post.notFollowed": "No seguido",
|
||||
"discussions.post.answered": "Una vez respondido",
|
||||
"discussions.post.unFollow": "Dejar de seguir",
|
||||
"discussions.post.like": "Como",
|
||||
"discussions.post.removeLike": "A diferencia de",
|
||||
"discussions.post.liked": "apreciado",
|
||||
"discussions.post.likes": "gustos",
|
||||
"discussions.post.viewActivity": "Ver actividad",
|
||||
"discussions.post.activity": "Actividad ",
|
||||
"discussions.post.closed": "Publicación cerrada por respuestas y comentarios.",
|
||||
"discussions.post.relatedTo": "Relacionado con",
|
||||
"discussions.editor.delete.post.title": "Eliminar mensaje",
|
||||
"discussions.editor.delete.post.description": "¿Seguro que quieres eliminar esta publicación de forma permanente?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Borrar",
|
||||
"discussions.editor.report.post.title": "¿Denunciar contenido inapropiado?",
|
||||
"discussions.editor.report.post.description": "El equipo de moderación de debates revisará este contenido y tomará las medidas adecuadas.",
|
||||
"discussions.post.closePostModal.title": "Cerrar publicación",
|
||||
"discussions.post.closePostModal.text": "Introduce un motivo para cerrar esta publicación. Esto solo se mostrará a otros moderadores.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "Razón",
|
||||
"discussions.post.closePostModal.cancel": "Cancelar",
|
||||
"discussions.post.closePostModal.confirm": "Cerrar publicación",
|
||||
"discussions.post.label.new": "{count} Nuevo",
|
||||
"discussions.post.editedBy": "Editado por",
|
||||
"discussions.post.editReason": "Razón",
|
||||
"discussions.post.postWithoutPreview": "No hay vista previa disponible",
|
||||
"discussions.post.follow.description": "estás siguiendo esta publicación",
|
||||
"discussions.post.unfollow.description": "no estas siguiendo esta publicación",
|
||||
"discussions.topics.sort.message": "Ordenado por {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "Actividad reciente",
|
||||
"discussions.topics.sort.commentCount": "La mayoría de la actividad",
|
||||
"discussions.topics.sort.courseStructure": "Estructura del curso",
|
||||
"discussions.topics.unnamed.label": "Categoría sin nombre",
|
||||
"discussions.subtopics.unnamed.label": "Subcategoría sin nombre",
|
||||
"tour.action.advance": "Próximo",
|
||||
"tour.action.dismiss": "Descartar",
|
||||
"tour.action.end": "Ok",
|
||||
"tour.body.notRespondedFilter": "Ahora puede filtrar debates para encontrar publicaciones sin respuesta.",
|
||||
"tour.title.notRespondedFilter": "¡Nueva opción de filtrado!",
|
||||
"tour.body.responseSortTour": "Las respuestas y los comentarios ahora se ordenan por los más recientes primero. Utilice esta opción para cambiar el orden de clasificación",
|
||||
"tour.title.responseSortTour": "¡Ordenar respuestas!"
|
||||
}
|
||||
211
src/i18n/messages/es_ES.json
Normal file
211
src/i18n/messages/es_ES.json
Normal file
@@ -0,0 +1,211 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "Material del curso",
|
||||
"learn.course.tabs.navigation.overflow.menu": "Más...",
|
||||
"discussions.topics.backAlt": "Volver a la lista de temas",
|
||||
"discussions.topics.discussions": "{count, plural,\n =0 {Discussion}\n one {# Discussion}\n other {# Discussions}\n }",
|
||||
"discussions.topics.questions": "{count, plural,\n =0 {Question}\n one {# Question}\n other {# Questions}\n }",
|
||||
"discussions.topics.reported": "{reported} informado",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} informado anteriormente",
|
||||
"discussions.topics.find.label": "Buscar temas",
|
||||
"discussions.topics.unnamed.section.label": "Sección sin nombre",
|
||||
"discussions.topics.unnamed.subsection.label": "Subsección sin nombre",
|
||||
"discussions.subtopics.unnamed.topic.label": "Tema sin nombre",
|
||||
"discussions.topics.title": "No existe ningún tema",
|
||||
"discussions.topics.createTopic": "Póngase en contacto con su administrador para crear un tema",
|
||||
"discussions.topics.nothing": "Nada aquí todavía",
|
||||
"discussions.topics.archived.label": "Archivado",
|
||||
"discussions.learner.reported": "{reported} informado",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} informado anteriormente",
|
||||
"discussions.learner.lastLogin": "Último vez activo {lastActiveTime}",
|
||||
"discussions.learner.loadMostLearners": "Cargar más",
|
||||
"discussions.learner.back": "Volver",
|
||||
"discussions.learner.activityForLearner": "Actividad para {username}",
|
||||
"discussions.learner.mostActivity": "La mayoría de la actividad",
|
||||
"discussions.learner.reportedActivity": "Actividad reportada",
|
||||
"discussions.learner.recentActivity": "Actividad reciente",
|
||||
"discussions.learner.sortFilterStatus": "Todos los alumnos por {sort, select, flagged {actividad notificada} activity {mayor actividad} other {{sort}} }",
|
||||
"discussion.learner.allActivity": "Toda la actividad",
|
||||
"discussion.learner.posts": "Publicación",
|
||||
"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": "Eliminar",
|
||||
"discussions.confirmation.button.confirm": "Confirmar",
|
||||
"discussions.actions.close": "Cerrar",
|
||||
"discussions.actions.reopen": "Reabrir",
|
||||
"discussions.actions.report": "Informar",
|
||||
"discussions.actions.unreport": "No informar",
|
||||
"discussions.actions.endorse": "Apoyar",
|
||||
"discussions.actions.unendorse": "No apoyar",
|
||||
"discussions.actions.markAnswered": "Marcar como respondida",
|
||||
"discussions.actions.unMarkAnswered": "Desmarcar como respondida",
|
||||
"discussions.modal.confirmation.button.cancel": "Cancelar",
|
||||
"discussions.empty.allTopics": "Toda la actividad de debate de estos temas se mostrará aquí.",
|
||||
"discussions.empty.allPosts": "Toda la actividad de debate sobre este tema se mostrará aquí.",
|
||||
"discussions.empty.myPosts": "Las publicaciones con las que has interactuado se mostrarán aquí.",
|
||||
"discussions.empty.topic": "Toda la actividad de debate sobre este tema se mostrará aquí.",
|
||||
"discussions.empty.title": "Nada aquí todavía",
|
||||
"discussions.empty.noPostSelected": "Ninguna publicación seleccionada",
|
||||
"discussions.empty.noTopicSelected": "Ningún tema seleccionado",
|
||||
"discussions.sidebar.noResultsFound": "No se han encontrado resultados",
|
||||
"discussions.sidebar.differentKeywords": "Intente buscar diferentes palabras clave",
|
||||
"discussions.sidebar.removeKeywords": "Intente buscar diferentes palabras clave o elimine algunos filtros.",
|
||||
"discussions.sidebar.removeKeywordsOnly": "Intenta buscar diferentes palabras clave",
|
||||
"discussions.sidebar.removeFilters": "Pruebe eliminar algunos filtros.",
|
||||
"discussions.empty.iconAlt": "Vacío",
|
||||
"discussions.authors.label.staff": "Equipo docente",
|
||||
"discussions.authors.label.ta": "TA",
|
||||
"discussions.learner.loadMostPosts": "Cargar más entradas",
|
||||
"discussions.post.anonymous.author": "anónimo",
|
||||
"discussion.blackoutBanner.information": "El equipo del curso ha desactivado temporalmente la publicación en los debates.",
|
||||
"discussions.editor.image.warning.message": "Las imágenes que tengan un ancho o alto superior a 999 px no serán visibles cuando la publicación, la respuesta o el comentario se vean mediante debates en línea del curso.",
|
||||
"discussions.editor.image.warning.title": "¡Advertencia!",
|
||||
"discussions.editor.image.warning.dismiss": "Aceptar",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Temas",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Mostrar todo",
|
||||
"discussions.navigation.navigationBar.allPosts": "Todos los mensajes",
|
||||
"discussions.navigation.navigationBar.allTopics": "Temas",
|
||||
"discussions.navigation.navigationBar.myPosts": "Mis publicaciones",
|
||||
"discussions.navigation.navigationBar.learners": "Estudiantes",
|
||||
"discussions.comments.comment.addComment": "Añadir comentario",
|
||||
"discussions.comments.comment.addResponse": "Agregar una respuesta",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "Contenido informado para que el personal lo revise",
|
||||
"discussions.actions.back.alt": "Volver a la lista",
|
||||
"discussions.comments.comment.responseCount": "{num, plural,\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": "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 {Todos} other {{group}} }.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select,\n discussion {Discussion}\n question {Question}\n other {{postType}}\n } posted {relativeTime} by",
|
||||
"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 desea eliminar este comentario de forma permanente?",
|
||||
"discussions.delete.confirmation.button.delete": "Eliminar",
|
||||
"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": "Motivo 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": "Tema no encontrado",
|
||||
"discussions.comment.sortFilterStatus": "{sort, select, false {Primero los más antiguos} true {Primero los más nuevos} other {{sort}} }",
|
||||
"discussions.app.title": "Foros",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Buscar todas las entradas",
|
||||
"discussions.posts.actionBar.search": "{page, select,\n topics {Search topics}\n posts {Search all posts}\n learners {Search learners}\n myPosts {Search all posts}\n other {{page}}\n }",
|
||||
"discussions.actionBar.searchInfo": "Mostrando resultados {count} para \"{text}\"",
|
||||
"discussions.actionBar.searchRewriteInfo": "No se han encontrado resultados para \"{searchString}\". Mostrando resultados de {count} para \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchInfoSearching": "Buscando...",
|
||||
"discussions.actionBar.clearSearch": "Borrar resultados",
|
||||
"discussion.posts.actionBar.add": "Agregar una publicación",
|
||||
"discussion.posts.actionBar.close": "Cerrar",
|
||||
"discussions.post.editor.type": "Tipo de publicación",
|
||||
"discussions.post.editor.addPostHeading": "Agregar una publicación",
|
||||
"discussions.post.editor.editPostHeading": "Editar publicación",
|
||||
"discussions.post.editor.typeDescription": "Las preguntas suscitan cuestiones que necesitan respuestas. Los debates permiten compartir ideas e iniciar conversaciones.",
|
||||
"discussions.post.editor.required": "Obligatorio",
|
||||
"discussions.post.editor.questionType": "Pregunta",
|
||||
"discussions.post.editor.questionDescription": "Plantear problemas que necesitan respuestas",
|
||||
"discussions.post.editor.discussionType": "Foro",
|
||||
"discussions.post.editor.discussionDescription": "Comparte ideas e inicia conversaciones",
|
||||
"discussions.post.editor.topicArea": "Tema",
|
||||
"discussions.post.editor.topicAreaDescription": "Añadir tu comentario a un tema relevante ayuda a los demás a encontrarlo.",
|
||||
"discussions.post.editor.cohortVisibility": "Visibilidad de la cohorte",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "Todos los estudiantes",
|
||||
"discussions.post.editor.title": "Título de la publicación",
|
||||
"discussions.post.editor.titleDescription": "Añadir un título claro y descriptivo para animar a la participación.",
|
||||
"discussions.post.editor.title.error": "El título de la publicación no puede estar vacío.",
|
||||
"discussions.post.editor.content.error": "El contenido de la publicación no puede estar vacío.",
|
||||
"discussions.post.editor.questionText": "Su pregunta o idea (obligatorio)",
|
||||
"discussions.post.editor.preview": "Vista previa",
|
||||
"discussions.post.editor.followPost": "Seguir este post",
|
||||
"discussions.post.editor.anonymousPost": "Publicar de forma anónima",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Publicar de forma anónima a los compañeros",
|
||||
"discussions.editor.posts.editReasonCode": "Motivo de la edición",
|
||||
"discussions.editor.posts.showPreview.button": "Mostrar vista previa",
|
||||
"discussions.topic.noName.label": "Categoría sin nombre",
|
||||
"discussions.subtopic.noName.label": "Subcategoría sin nombre",
|
||||
"discussions.posts.filter.showALl": "Mostrar todo",
|
||||
"discussions.posts.filter.discussions": "Foros",
|
||||
"discussions.posts.filter.questions": "Preguntas",
|
||||
"discussions.posts.filter.message": "Estado: {filterBy}",
|
||||
"discussions.posts.status.filter.anyStatus": "Cualquier estado",
|
||||
"discussions.posts.status.filter.unread": "No leído",
|
||||
"discussions.posts.status.filter.following": "Siguiendo",
|
||||
"discussions.posts.status.filter.reported": "Informado",
|
||||
"discussions.posts.status.filter.unanswered": "Sin responder",
|
||||
"discussions.posts.status.filter.unresponded": "Sin respuesta",
|
||||
"discussions.posts.filter.myPosts": "Mis publicaciones",
|
||||
"discussions.posts.filter.myDiscussions": "Mis debates",
|
||||
"discussions.posts.filter.myQuestions": "Mis preguntas",
|
||||
"discussions.posts.sort.message": "Ordenado por {sortBy}",
|
||||
"discussions.posts.sort.lastActivity": "Actividad reciente",
|
||||
"discussions.posts.sort.commentCount": "La mayoría de la actividad",
|
||||
"discussions.posts.sort.voteCount": "La mayoría me gusta",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anónimo",
|
||||
"discussions.post.addResponse": "Agregar respuesta",
|
||||
"discussions.post.lastResponse": "Última respuesta {time}",
|
||||
"discussions.post.postedOn": "Publicado {time} por {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Informado",
|
||||
"discussions.post.following": "Siguiendo",
|
||||
"discussions.post.follow": "Seguir",
|
||||
"discussions.post.followed": "Seguido",
|
||||
"discussions.post.notFollowed": "No seguido",
|
||||
"discussions.post.answered": "Respondido",
|
||||
"discussions.post.unFollow": "Dejar de seguir",
|
||||
"discussions.post.like": "Me gusta",
|
||||
"discussions.post.removeLike": "No me gusta",
|
||||
"discussions.post.liked": "Me gustó",
|
||||
"discussions.post.likes": "gustos",
|
||||
"discussions.post.viewActivity": "Ver actividad",
|
||||
"discussions.post.activity": "Actividad",
|
||||
"discussions.post.closed": "Publicación cerrada por respuestas y comentarios.",
|
||||
"discussions.post.relatedTo": "Relacionado con",
|
||||
"discussions.editor.delete.post.title": "Eliminar publicación",
|
||||
"discussions.editor.delete.post.description": "¿Seguro que quiere eliminar esta publicación de forma permanente?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Eliminar",
|
||||
"discussions.editor.report.post.title": "¿Denunciar contenido inapropiado?",
|
||||
"discussions.editor.report.post.description": "El equipo de moderación de debates revisará este contenido y tomará las medidas adecuadas.",
|
||||
"discussions.post.closePostModal.title": "Cerrar publicación",
|
||||
"discussions.post.closePostModal.text": "Introduce un motivo para cerrar esta publicación. Esto solo se mostrará a otros moderadores.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "Motivo",
|
||||
"discussions.post.closePostModal.cancel": "Cancelar",
|
||||
"discussions.post.closePostModal.confirm": "Cerrar publicación",
|
||||
"discussions.post.label.new": "{count} Nuevo",
|
||||
"discussions.post.editedBy": "Editado por",
|
||||
"discussions.post.editReason": "Motivo",
|
||||
"discussions.post.postWithoutPreview": "No hay vista previa disponible",
|
||||
"discussions.post.follow.description": "Está siguiendo esta publicación",
|
||||
"discussions.post.unfollow.description": "No está siguiendo esta publicación",
|
||||
"discussions.topics.sort.message": "Ordenado por {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "Actividad reciente",
|
||||
"discussions.topics.sort.commentCount": "La mayoría de la actividad",
|
||||
"discussions.topics.sort.courseStructure": "Estructura del curso",
|
||||
"discussions.topics.unnamed.label": "Categoría sin nombre",
|
||||
"discussions.subtopics.unnamed.label": "Subcategoría sin nombre",
|
||||
"tour.action.advance": "Siguiente",
|
||||
"tour.action.dismiss": "Descartar",
|
||||
"tour.action.end": "De acuerdo",
|
||||
"tour.body.notRespondedFilter": "Ahora puede filtrar debates para encontrar publicaciones sin respuesta.",
|
||||
"tour.title.notRespondedFilter": "¡Nueva opción de filtrado!",
|
||||
"tour.body.responseSortTour": "Las respuestas y los comentarios ahora se ordenan por los más recientes primero. Utilice esta opción para cambiar el orden de clasificación",
|
||||
"tour.title.responseSortTour": "¡Ordenar respuestas!"
|
||||
}
|
||||
211
src/i18n/messages/fa_IR.json
Normal file
211
src/i18n/messages/fa_IR.json
Normal file
@@ -0,0 +1,211 @@
|
||||
{
|
||||
"navigation.course.tabs.label": "منابع دوره آموزشی",
|
||||
"learn.course.tabs.navigation.overflow.menu": "بیشتر...",
|
||||
"discussions.topics.backAlt": "بازگشت به لیست موضوعات",
|
||||
"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} گزارش شده",
|
||||
"discussions.topics.previouslyReported": "{previouslyReported} قبلا گزارش شدهاست",
|
||||
"discussions.topics.find.label": "جستجوی موضوعات",
|
||||
"discussions.topics.unnamed.section.label": "بخش بدون نام",
|
||||
"discussions.topics.unnamed.subsection.label": "زیربخش بدون نام",
|
||||
"discussions.subtopics.unnamed.topic.label": "موضوع بی نام",
|
||||
"discussions.topics.title": "هیچ موضوعی وجود ندارد",
|
||||
"discussions.topics.createTopic": "لطفا برای ایجاد موضوع با ادمین خود تماس بگیرید",
|
||||
"discussions.topics.nothing": "اینجا هنوز چیزی نیست",
|
||||
"discussions.topics.archived.label": "آرشیو شده",
|
||||
"discussions.learner.reported": "{reported} گزارش شده",
|
||||
"discussions.learner.previouslyReported": "{previouslyReported} قبلا گزارش شدهاست",
|
||||
"discussions.learner.lastLogin": "آخرین فعال {lastActiveTime}",
|
||||
"discussions.learner.loadMostLearners": "بارگیری بیشتر",
|
||||
"discussions.learner.back": "بازگشت",
|
||||
"discussions.learner.activityForLearner": "فعالیت برای {username}",
|
||||
"discussions.learner.mostActivity": "بیشترین فعالیت",
|
||||
"discussions.learner.reportedActivity": "فعالیت گزارششده",
|
||||
"discussions.learner.recentActivity": "فعالیت اخیر",
|
||||
"discussions.learner.sortFilterStatus": "همه فراگیران بر اساس {sort, select,\n علامت گذاری شده {reported activity}\n فعالیت {most activity}\n دیگر {{sort}}\n } مرتب شده اند.",
|
||||
"discussion.learner.allActivity": "تمام فعالیت",
|
||||
"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": "گشایش مجدد",
|
||||
"discussions.actions.report": "گزارش",
|
||||
"discussions.actions.unreport": "لغو گزارش",
|
||||
"discussions.actions.endorse": "تأیید",
|
||||
"discussions.actions.unendorse": "عدم تأیید",
|
||||
"discussions.actions.markAnswered": "علامت گذاری به عنوان پاسخ",
|
||||
"discussions.actions.unMarkAnswered": "علامت را بهعنوان پاسخ بردارید",
|
||||
"discussions.modal.confirmation.button.cancel": "لغو",
|
||||
"discussions.empty.allTopics": "همه فعالیتهای گفتگو برای این موضوعات در اینجا نشان داده میشود.",
|
||||
"discussions.empty.allPosts": "همه فعالیتهای گفتگو برای دوره آموزشی شما در اینجا نمایش داده میشود.",
|
||||
"discussions.empty.myPosts": "مطالبی که با آنها تعامل داشتهاید در اینجا نشان داده میشوند.",
|
||||
"discussions.empty.topic": "همه فعالیتهای بحث برای این موضوع در این قسمت نشان داده میشود.",
|
||||
"discussions.empty.title": "اینجا هنوز چیزی نیست",
|
||||
"discussions.empty.noPostSelected": "هیچ مطلبی انتخاب نشدهاست",
|
||||
"discussions.empty.noTopicSelected": "هیچ موضوعی انتخاب نشدهاست",
|
||||
"discussions.sidebar.noResultsFound": "هیچ نتیجهای پیدا نشد",
|
||||
"discussions.sidebar.differentKeywords": "کلیدواژههای دیگری استفاده کرده و دوباره جستجو کنید",
|
||||
"discussions.sidebar.removeKeywords": "سعی کنید کلمات کلیدی مختلف را جستجو کنید یا برخی از فیلترها را حذف کنید",
|
||||
"discussions.sidebar.removeKeywordsOnly": "کلیدواژههای دیگری استفاده کرده و دوباره جستجو کنید",
|
||||
"discussions.sidebar.removeFilters": "سعی کنید برخی از فیلترها را حذف کنید",
|
||||
"discussions.empty.iconAlt": "خالی",
|
||||
"discussions.authors.label.staff": "کارکنان",
|
||||
"discussions.authors.label.ta": "کمک مربی",
|
||||
"discussions.learner.loadMostPosts": "بارگیری مطالب بیشتر",
|
||||
"discussions.post.anonymous.author": "بینام",
|
||||
"discussion.blackoutBanner.information": "ارسال پست در بحث ها توسط تیم دوره غیرفعال شده است",
|
||||
"discussions.editor.image.warning.message": "هنگامی که مطلب یا پاسخ یا نظری در گفتگوهای دوره به صورت درونخطی مشاهده می شود، تصاویری که عرض یا ارتفاع آنها بیشتر از 999 پیکسل باشد، قابل مشاهده نخواهند بود.",
|
||||
"discussions.editor.image.warning.title": "هشدار!",
|
||||
"discussions.editor.image.warning.dismiss": "بسیارخوب",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "عناوین ",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "نمایش همه",
|
||||
"discussions.navigation.navigationBar.allPosts": "همه مطالب",
|
||||
"discussions.navigation.navigationBar.allTopics": "عناوین ",
|
||||
"discussions.navigation.navigationBar.myPosts": "مطالب ارسالی من",
|
||||
"discussions.navigation.navigationBar.learners": "یادگیرندگان",
|
||||
"discussions.comments.comment.addComment": "اضافه کردن نظر",
|
||||
"discussions.comments.comment.addResponse": "افزودن پاسخ",
|
||||
"discussions.comments.comment.abuseFlaggedMessage": "محتوا برای بررسی کارکنان گزارش شده است",
|
||||
"discussions.actions.back.alt": "بازگشت به لیست",
|
||||
"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": "بارگیری نظرات بیشتر",
|
||||
"discussions.comments.comment.loadMoreResponses": "بارگیری پاسخهای بیشتر",
|
||||
"discussions.comments.comment.visibility": "این مطلب برای {group, select,\nnull {Everyone}\nother {{group}}\n}. قابل مشاهده است.",
|
||||
"discussions.comments.comment.postedTime": "{postType, select,\ndiscussion {Discussion}\nquestion {Question}\nother {{postType}}\n} ارسال شده {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 اشتباه {Oldest first}\n صحیح {Newest first}\n سایر {{sort}}\n }",
|
||||
"discussions.app.title": "گفتگوها",
|
||||
"discussions.posts.actionBar.searchAllPosts": "جستجوی همۀ مطالب ارسالی",
|
||||
"discussions.posts.actionBar.search": "{page, select,\ntopics {Search topics}\nposts {Search all posts}\nlearners {Search learners}\nmyPosts {Search all posts}\nother {{page}}\n}",
|
||||
"discussions.actionBar.searchInfo": "نمایش {count} نتایج برای \"{text}\"",
|
||||
"discussions.actionBar.searchRewriteInfo": "هیچ نتیجه ای برای \"{searchString}\" یافت نشد. نمایش نتایج {count} برای \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchInfoSearching": "در حال جستجو...",
|
||||
"discussions.actionBar.clearSearch": "پاککردن نتایج",
|
||||
"discussion.posts.actionBar.add": "افزودن مطلب",
|
||||
"discussion.posts.actionBar.close": "بستن",
|
||||
"discussions.post.editor.type": "نوع مطلب ارسالی",
|
||||
"discussions.post.editor.addPostHeading": "افزودن مطلب",
|
||||
"discussions.post.editor.editPostHeading": "ویرایش مطلب",
|
||||
"discussions.post.editor.typeDescription": "پرسشها مسائلی را مطرح میکنند که نیاز به پاسخ دارند. گفتگوها ایدهها را به اشتراک میگذارند و مکالمه را شروع میکنند.",
|
||||
"discussions.post.editor.required": "ضروری",
|
||||
"discussions.post.editor.questionType": "سوال",
|
||||
"discussions.post.editor.questionDescription": "مسائلی را مطرح کنید که نیاز به پاسخ دارند",
|
||||
"discussions.post.editor.discussionType": "گفتگو",
|
||||
"discussions.post.editor.discussionDescription": "ایدهها را به اشتراک بگذارید و گفتگو را شروع کنید",
|
||||
"discussions.post.editor.topicArea": "محل موضوع",
|
||||
"discussions.post.editor.topicAreaDescription": "مطلب ارسالی خود را به یک موضوع مرتبط اضافه کنید تا به دیگران در یافتن آن کمک کنید.",
|
||||
"discussions.post.editor.cohortVisibility": "رؤیتپذیری انجمن",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "همه يادگيرندگان",
|
||||
"discussions.post.editor.title": "ارسال عنوان",
|
||||
"discussions.post.editor.titleDescription": "برای تشویق مشارکت سایرین، عنوانی واضح و توصیفی بیفزایید.",
|
||||
"discussions.post.editor.title.error": "عنوان مطلب نمیتواند خالی باشد.",
|
||||
"discussions.post.editor.content.error": "محتوای مطلب نمیتواند خالی باشد.",
|
||||
"discussions.post.editor.questionText": "پرسش یا ایده شما (الزامی)",
|
||||
"discussions.post.editor.preview": "پیشنمایش",
|
||||
"discussions.post.editor.followPost": "این مطلب را دنبال میکنم",
|
||||
"discussions.post.editor.anonymousPost": "بهصورت ناشناس ارسال کنید",
|
||||
"discussions.post.editor.anonymousToPeersPost": "بهصورت ناشناس برای همتایان ارسال کنید",
|
||||
"discussions.editor.posts.editReasonCode": "علت ویرایش",
|
||||
"discussions.editor.posts.showPreview.button": "پیشنمایش",
|
||||
"discussions.topic.noName.label": "دسته بی نام",
|
||||
"discussions.subtopic.noName.label": "زیرمجموعه بی نام",
|
||||
"discussions.posts.filter.showALl": "نمایش همه",
|
||||
"discussions.posts.filter.discussions": "گفتگوها",
|
||||
"discussions.posts.filter.questions": "سوالات ",
|
||||
"discussions.posts.filter.message": "وضعیت: {filterBy}",
|
||||
"discussions.posts.status.filter.anyStatus": "هر وضعیتی",
|
||||
"discussions.posts.status.filter.unread": "نخوانده",
|
||||
"discussions.posts.status.filter.following": "دنبال میکنم",
|
||||
"discussions.posts.status.filter.reported": "گزارششده",
|
||||
"discussions.posts.status.filter.unanswered": "پاسخ دادهنشده",
|
||||
"discussions.posts.status.filter.unresponded": "پاسخ داده نشد",
|
||||
"discussions.posts.filter.myPosts": "مطالب ارسالی من",
|
||||
"discussions.posts.filter.myDiscussions": "گفتگوهای من",
|
||||
"discussions.posts.filter.myQuestions": "سوالات من",
|
||||
"discussions.posts.sort.message": "منظم شده برحسب {sortBy}",
|
||||
"discussions.posts.sort.lastActivity": "فعالیت اخیر",
|
||||
"discussions.posts.sort.commentCount": "بیشترین فعالیت",
|
||||
"discussions.posts.sort.voteCount": "بیشترین لایک",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n نادرست {All}\n صحیح {Own}\n سایر {{own}}\n } {status, select,\n وضعیت همه {}\n وضعیت خوانده نشده {unread}\n وضعیت دنبال کردن {followed}\n وضعیت گزارش شده {reported}\n وضعیت بی پاسخ {unanswered}\n وضعیت بی پاسخ {unresponded}\n سایر {{status}}\n } {type, select,\n بحث {discussions}\n سوال {questions}\n همه {posts}\n سایر {{type}}\n } {cohortType, select,\n همه {}\n گروه {in {cohort}}\n سایر {{cohortType}}\n } مرتب شده بر اساس {sort, select,\n آخرین فعالیت بر {recent activity}\n تعداد نظرات {most activity}\n تعداد آرا {most likes}\n سایر {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "بینام",
|
||||
"discussions.post.addResponse": "افزودن پاسخ",
|
||||
"discussions.post.lastResponse": "پاسخ اخیر {time}",
|
||||
"discussions.post.postedOn": "ارسال شده {time} به دست {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "گزارششده",
|
||||
"discussions.post.following": "دنبال میکنم",
|
||||
"discussions.post.follow": "دنبال میکنم",
|
||||
"discussions.post.followed": "دنبال شد",
|
||||
"discussions.post.notFollowed": "دنبال نشده است",
|
||||
"discussions.post.answered": "پاسخ دادهشده",
|
||||
"discussions.post.unFollow": "دنبال نمیکنم",
|
||||
"discussions.post.like": "میپسندم",
|
||||
"discussions.post.removeLike": "غیرمشابه",
|
||||
"discussions.post.liked": "دوست داشت",
|
||||
"discussions.post.likes": "دوست دارد",
|
||||
"discussions.post.viewActivity": "مشاهده فعالیت",
|
||||
"discussions.post.activity": "فعالیت",
|
||||
"discussions.post.closed": "این مطلب برای پاسخ و نظر بسته است",
|
||||
"discussions.post.relatedTo": "مربوط به ",
|
||||
"discussions.editor.delete.post.title": "حذف مطلب",
|
||||
"discussions.editor.delete.post.description": "از حذف دایمی این مطلب اطمینان دارید؟",
|
||||
"discussions.post.delete.confirmation.button.delete": "حذف",
|
||||
"discussions.editor.report.post.title": "گزارش محتوای نامناسب؟",
|
||||
"discussions.editor.report.post.description": "تیم نظارت بر بحث این محتوا را بررسی کرده و اقدامات لازم را انجام خواهد داد.",
|
||||
"discussions.post.closePostModal.title": "بستن مطلب",
|
||||
"discussions.post.closePostModal.text": "دلیل بستن این مطلب را وارد کنید. این دلیل فقط برای مدیران دیگر نمایش داده میشود.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "دلیل",
|
||||
"discussions.post.closePostModal.cancel": "لغو",
|
||||
"discussions.post.closePostModal.confirm": "بستن مطلب",
|
||||
"discussions.post.label.new": "{count} جدید",
|
||||
"discussions.post.editedBy": "ویرایششده به دست",
|
||||
"discussions.post.editReason": "علت",
|
||||
"discussions.post.postWithoutPreview": "پیشنمایش در دسترس نیست",
|
||||
"discussions.post.follow.description": "شما این پست را دنبال می کنید",
|
||||
"discussions.post.unfollow.description": "شما این پست را دنبال نمی کنید",
|
||||
"discussions.topics.sort.message": "منظم برحسب {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "فعالیت اخیر",
|
||||
"discussions.topics.sort.commentCount": "بیشترین فعالیت",
|
||||
"discussions.topics.sort.courseStructure": "ساختار دوره آموزشی",
|
||||
"discussions.topics.unnamed.label": "دسته بی نام",
|
||||
"discussions.subtopics.unnamed.label": "زیرمجموعه بی نام",
|
||||
"tour.action.advance": "بعدی",
|
||||
"tour.action.dismiss": "لطفا نادیده بگیرید",
|
||||
"tour.action.end": "بسیار خوب",
|
||||
"tour.body.notRespondedFilter": "اکنون می توانید بحث ها را فیلتر کنید تا پست های بدون پاسخ را بیابید.",
|
||||
"tour.title.notRespondedFilter": "گزینه فیلتر جدید!",
|
||||
"tour.body.responseSortTour": "پاسخ ها و نظرات اکنون بر اساس جدیدترین ها مرتب شده اند. لطفا از این گزینه برای تغییر ترتیب مرتب سازی استفاده کنید",
|
||||
"tour.title.responseSortTour": "مرتب سازی پاسخ ها!"
|
||||
}
|
||||
@@ -59,10 +59,7 @@
|
||||
"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",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is 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",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"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.createTopic": "Veuillez contacter votre admin pour créer un sujet",
|
||||
"discussions.topics.nothing": "Rien ici encore",
|
||||
"discussions.topics.archived.label": "Archivé",
|
||||
"discussions.learner.reported": "{reported} signalé",
|
||||
@@ -20,12 +20,12 @@
|
||||
"discussions.learner.loadMostLearners": "Afficher plus",
|
||||
"discussions.learner.back": "Retour",
|
||||
"discussions.learner.activityForLearner": "Activité pour {username}",
|
||||
"discussions.learner.mostActivity": "La plupart des activités",
|
||||
"discussions.learner.mostActivity": "Les plus actifs",
|
||||
"discussions.learner.reportedActivity": "Activité signalée",
|
||||
"discussions.learner.recentActivity": "Activité récente",
|
||||
"discussions.learner.sortFilterStatus": "Tous les apprenants triés pas {sort, select,\n flagged {reported activity}\n activity {most activity}\n other {{sort}}\n }",
|
||||
"discussions.learner.sortFilterStatus": "Tous les apprenants triés par {sort, select,\n flagged {l'activité signalée}\n activity {les plus actifs}\n other {{sort}}\n }",
|
||||
"discussion.learner.allActivity": "Toutes les activités",
|
||||
"discussion.learner.posts": "Posts",
|
||||
"discussion.learner.posts": "Publications",
|
||||
"discussions.actions.button.alt": "Menu Actions",
|
||||
"discussions.actions.copylink": "Copier le lien",
|
||||
"discussions.actions.edit": "Éditer",
|
||||
@@ -39,10 +39,10 @@
|
||||
"discussions.actions.unreport": "Ne pas signaler",
|
||||
"discussions.actions.endorse": "Approuver",
|
||||
"discussions.actions.unendorse": "Ne plus approuver",
|
||||
"discussions.actions.markAnswered": "Marquer comme répondu",
|
||||
"discussions.actions.unMarkAnswered": "Décocher comme répondu",
|
||||
"discussions.actions.markAnswered": "Marquer comme répondue",
|
||||
"discussions.actions.unMarkAnswered": "Décocher comme répondue",
|
||||
"discussions.modal.confirmation.button.cancel": "Annuler",
|
||||
"discussions.empty.allTopics": "Toutes les activités de discussion pour ces sujets apparaîtront ici.",
|
||||
"discussions.empty.allTopics": "Toutes les activités de discussion pour ces sujets s'afficheront ici.",
|
||||
"discussions.empty.allPosts": "Toutes les activités de discussion pour votre cours s'afficheront ici.",
|
||||
"discussions.empty.myPosts": "Les publications avec lesquelles vous avez interagi s'afficheront ici.",
|
||||
"discussions.empty.topic": "Toutes les activités de discussion pour ce sujet s'afficheront ici.",
|
||||
@@ -51,40 +51,37 @@
|
||||
"discussions.empty.noTopicSelected": "Aucun sujet sélectionné",
|
||||
"discussions.sidebar.noResultsFound": "Aucun résultat trouvé",
|
||||
"discussions.sidebar.differentKeywords": "Essayez de rechercher différents mots-clés",
|
||||
"discussions.sidebar.removeKeywords": "Essayez de rechercher différents mots clés ou de supprimer certains filtres",
|
||||
"discussions.sidebar.removeKeywords": "Essayez de rechercher différents mots-clés ou de retirer certains filtres",
|
||||
"discussions.sidebar.removeKeywordsOnly": "Essayez de rechercher différents mots-clés",
|
||||
"discussions.sidebar.removeFilters": "Essayez de supprimer certains filtres",
|
||||
"discussions.sidebar.removeFilters": "Essayez de retirer certains filtres",
|
||||
"discussions.empty.iconAlt": "Vide",
|
||||
"discussions.authors.label.staff": "Équipe pédagogique",
|
||||
"discussions.authors.label.staff": "Personnel",
|
||||
"discussions.authors.label.ta": "Assistant d'enseignement",
|
||||
"discussions.learner.loadMostPosts": "Charger plus de messages",
|
||||
"discussions.learner.loadMostPosts": "Charger plus de publications",
|
||||
"discussions.post.anonymous.author": "anonyme",
|
||||
"discussion.banner.welcomeMessage": "🎉 Bienvenue dans la nouvelle expérience améliorée de discussions!",
|
||||
"discussion.banner.learnMore": "En savoir plus",
|
||||
"discussion.banner.shareFeedback": "Partager vos commentaires",
|
||||
"discussion.blackoutBanner.information": "La publication dans les discussions est temporairement désactivée par l'équipe du cours",
|
||||
"discussion.blackoutBanner.information": "La publication dans les discussions est désactivée par l'équipe du cours",
|
||||
"discussions.editor.image.warning.message": "Les images dont la largeur ou la hauteur est supérieure à 999 pixels ne seront pas visibles lorsque la publication, la réponse ou le commentaire est affiché à l'aide de discussions de cours en ligne",
|
||||
"discussions.editor.image.warning.title": "Avertissement!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
"discussions.navigation.breadcrumbMenu.allTopics": "Sujets",
|
||||
"discussions.navigation.breadcrumbMenu.showAll": "Tout afficher",
|
||||
"discussions.navigation.navigationBar.allPosts": "Tous les messages",
|
||||
"discussions.navigation.navigationBar.allPosts": "Toutes les publications",
|
||||
"discussions.navigation.navigationBar.allTopics": "Sujets",
|
||||
"discussions.navigation.navigationBar.myPosts": "Mes messages",
|
||||
"discussions.navigation.navigationBar.myPosts": "Mes publications",
|
||||
"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.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 {Aucune réponse approuvée}\n one {Affiche # réponse approuvée}\n other {Affiche # réponses approuvées}\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.visibility": "Cette publication est visible par {group, select,\n null {Tous}\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.answeredlabel": "Marqué comme répondue par",
|
||||
"discussions.comments.comment.endorsed": "Approuvé",
|
||||
"discussions.comments.comment.endorsedlabel": "Approuvé par",
|
||||
"discussions.actions.label": "Menu Actions",
|
||||
@@ -106,22 +103,22 @@
|
||||
"discussions.comment.comments.editedBy": "Édité par",
|
||||
"discussions.comment.comments.fullStop": "•",
|
||||
"discussions.comment.comments.reason": "Raison",
|
||||
"discussions.post.closedBy": "Message fermé par",
|
||||
"discussions.post.closedBy": "Publication fermée 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.comment.sortFilterStatus": "{sort, select,\n false {plus ancien d'abord}\n true {plus récent d'abord}\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 }",
|
||||
"discussions.posts.actionBar.searchAllPosts": "Recherche dans les publications",
|
||||
"discussions.posts.actionBar.search": "{page, select,\n topics {Recherche sujets}\n posts {Recherche toutes publications}\n learners {Recherche apprenants}\n myPosts {Recherche mes publications}\n other {{page}}\n }",
|
||||
"discussions.actionBar.searchInfo": "Affichage des résultats {count} pour \"{text}\"",
|
||||
"discussions.actionBar.searchRewriteInfo": "Aucun résultat trouvé pour \"(searchString}\". Affichage des résultats {count} pour \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchRewriteInfo": "Aucun résultat trouvé pour \"{searchString}\". Affichage des résultats {count} pour \"{textSearchRewrite}\".",
|
||||
"discussions.actionBar.searchInfoSearching": "Recherche...",
|
||||
"discussions.actionBar.clearSearch": "Effacer les résultats",
|
||||
"discussion.posts.actionBar.add": "Ajouter un message",
|
||||
"discussion.posts.actionBar.add": "Ajouter une publication",
|
||||
"discussion.posts.actionBar.close": "Fermer",
|
||||
"discussions.post.editor.type": "Type de message",
|
||||
"discussions.post.editor.addPostHeading": "Ajouter un message",
|
||||
"discussions.post.editor.editPostHeading": "Modifier le message",
|
||||
"discussions.post.editor.type": "Type de publication",
|
||||
"discussions.post.editor.addPostHeading": "Ajouter une publication",
|
||||
"discussions.post.editor.editPostHeading": "Modifier la publication",
|
||||
"discussions.post.editor.typeDescription": "Les questions soulèvent des problèmes auxquels il faut répondre. Les discussions partagent des idées et démarrent des conversations.",
|
||||
"discussions.post.editor.required": "Obligatoire",
|
||||
"discussions.post.editor.questionType": "Question",
|
||||
@@ -129,16 +126,16 @@
|
||||
"discussions.post.editor.discussionType": "Discussion",
|
||||
"discussions.post.editor.discussionDescription": "Partagez des idées et démarrez des conversations",
|
||||
"discussions.post.editor.topicArea": "Secteur thématique",
|
||||
"discussions.post.editor.topicAreaDescription": "Ajouter votre message à un sujet pertinent afin d'aider les autres à le trouver.",
|
||||
"discussions.post.editor.topicAreaDescription": "Ajouter votre publication à un sujet pertinent afin d'aider les autres à la trouver.",
|
||||
"discussions.post.editor.cohortVisibility": "Visibilité de la cohorte",
|
||||
"discussions.post.editor.cohortVisibilityAllLearners": "Tous les apprenants",
|
||||
"discussions.post.editor.title": "Titre de l'article",
|
||||
"discussions.post.editor.title": "Titre de la publication",
|
||||
"discussions.post.editor.titleDescription": "Ajouter un titre clair et descriptif afin d'encourager la participation.",
|
||||
"discussions.post.editor.title.error": "Le titre du message ne peut pas être vide.",
|
||||
"discussions.post.editor.title.error": "Le titre de la publication ne peut pas être vide.",
|
||||
"discussions.post.editor.content.error": "Le contenu de la publication ne peut pas être vide.",
|
||||
"discussions.post.editor.questionText": "Votre question ou idée (obligatoire)",
|
||||
"discussions.post.editor.preview": "Aperçu",
|
||||
"discussions.post.editor.followPost": "Suivez ce message",
|
||||
"discussions.post.editor.followPost": "Suivez cette publication",
|
||||
"discussions.post.editor.anonymousPost": "Publier anonymement",
|
||||
"discussions.post.editor.anonymousToPeersPost": "Publier anonymement à vos pairs",
|
||||
"discussions.editor.posts.editReasonCode": "Raison de la modification",
|
||||
@@ -150,65 +147,65 @@
|
||||
"discussions.posts.filter.questions": "Questions",
|
||||
"discussions.posts.filter.message": "Statut : {filterBy}",
|
||||
"discussions.posts.status.filter.anyStatus": "Tout statut",
|
||||
"discussions.posts.status.filter.unread": "Non lu",
|
||||
"discussions.posts.status.filter.following": "Suivi",
|
||||
"discussions.posts.status.filter.reported": "Signalé",
|
||||
"discussions.posts.status.filter.unanswered": "Non répondu",
|
||||
"discussions.posts.status.filter.unresponded": "Pas répondu",
|
||||
"discussions.posts.filter.myPosts": "Mes messages",
|
||||
"discussions.posts.status.filter.unread": "Non lues",
|
||||
"discussions.posts.status.filter.following": "Suivies",
|
||||
"discussions.posts.status.filter.reported": "Signalées",
|
||||
"discussions.posts.status.filter.unanswered": "Sans réponses",
|
||||
"discussions.posts.status.filter.unresponded": "Non répondues",
|
||||
"discussions.posts.filter.myPosts": "Mes publications",
|
||||
"discussions.posts.filter.myDiscussions": "Mes discussions",
|
||||
"discussions.posts.filter.myQuestions": "Mes questions",
|
||||
"discussions.posts.sort.message": "Trié par {sortBy}",
|
||||
"discussions.posts.sort.message": "Triées par {sortBy}",
|
||||
"discussions.posts.sort.lastActivity": "Activité récente",
|
||||
"discussions.posts.sort.commentCount": "La plupart des activités",
|
||||
"discussions.posts.sort.voteCount": "La plupart des aimés",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {All}\n true {Own}\n other {{own}}\n } {status, select,\n statusAll {}\n statusUnread {unread}\n statusFollowing {followed}\n statusReported {reported}\n statusUnanswered {unanswered}\n statusUnresponded {unresponded}\n other {{status}}\n } {type, select,\n discussion {discussions}\n question {questions}\n all {posts}\n other {{type}}\n } {cohortType, select,\n all {}\n group {in {cohort}}\n other {{cohortType}}\n } sorted by {sort, select,\n lastActivityAt {recent activity}\n commentCount {most activity}\n voteCount {most likes}\n other {{sort}}\n }",
|
||||
"discussions.posts.sort.commentCount": "Les plus actives",
|
||||
"discussions.posts.sort.voteCount": "Les plus aimées",
|
||||
"discussions.posts.sort-filter.sortFilterStatus": "{own, select,\n false {Toutes}\n true {Own}\n other {{own}}\n } {type, select,\n discussion {les discussions}\n question {les questions}\n all {les publications}\n other {{type}}\n } {status, select,\n statusAll {}\n statusUnread {non lues}\n statusFollowing {suivies}\n statusReported {signalées}\n statusUnanswered {sans réponses}\n statusUnresponded {non répondues}\n other {{status}}\n } {cohortType, select,\n all {}\n group {dans {cohort}}\n other {{cohortType}}\n } triées selon {sort, select,\n lastActivityAt {les plus récentes}\n commentCount {les plus actives}\n voteCount {les plus aimées}\n other {{sort}}\n }",
|
||||
"discussions.post.author.anonymous": "anonyme",
|
||||
"discussions.post.addResponse": "Ajouter une réponse",
|
||||
"discussions.post.lastResponse": "Dernière réponse {time}",
|
||||
"discussions.post.postedOn": "Publié {time} par {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Signalé",
|
||||
"discussions.post.following": "Suivi",
|
||||
"discussions.post.postedOn": "Publiée {time} par {author} {authorLabel}",
|
||||
"discussions.post.contentReported": "Signalées",
|
||||
"discussions.post.following": "Suivies",
|
||||
"discussions.post.follow": "Suivre",
|
||||
"discussions.post.followed": "Suivi",
|
||||
"discussions.post.notFollowed": "Non suivi",
|
||||
"discussions.post.answered": "Répondu",
|
||||
"discussions.post.followed": "Suivies",
|
||||
"discussions.post.notFollowed": "Non suivies",
|
||||
"discussions.post.answered": "Répondues",
|
||||
"discussions.post.unFollow": "Ne plus suivre",
|
||||
"discussions.post.like": "Aime",
|
||||
"discussions.post.removeLike": "Contrairement à",
|
||||
"discussions.post.liked": "aimé",
|
||||
"discussions.post.removeLike": "N'aime plus",
|
||||
"discussions.post.liked": "aimées",
|
||||
"discussions.post.likes": "aime",
|
||||
"discussions.post.viewActivity": "Afficher l'activité",
|
||||
"discussions.post.activity": "Activité",
|
||||
"discussions.post.closed": "Message fermé pour réponses et commentaires",
|
||||
"discussions.post.closed": "Publication fermée pour réponses et commentaires",
|
||||
"discussions.post.relatedTo": "Relative à",
|
||||
"discussions.editor.delete.post.title": "Supprimer le message",
|
||||
"discussions.editor.delete.post.description": "Êtes-vous sûr de vouloir supprimer définitivement ce message?",
|
||||
"discussions.editor.delete.post.title": "Supprimer la publication",
|
||||
"discussions.editor.delete.post.description": "Êtes-vous sûr de vouloir supprimer définitivement cette publication?",
|
||||
"discussions.post.delete.confirmation.button.delete": "Supprimer",
|
||||
"discussions.editor.report.post.title": "Signaler un contenu inapproprié?",
|
||||
"discussions.editor.report.post.description": "L'équipe de modération de la discussion examinera ce contenu et prendra les mesures appropriées.",
|
||||
"discussions.post.closePostModal.title": "Fermer le message",
|
||||
"discussions.post.closePostModal.text": "Entrez une raison pour fermer ce message. Cela ne sera affiché qu'aux autres modérateurs.",
|
||||
"discussions.post.closePostModal.title": "Fermer la publication",
|
||||
"discussions.post.closePostModal.text": "Entrez une raison pour fermer cette publication. Cela ne sera affiché qu'aux autres modérateurs.",
|
||||
"discussions.post.closePostModal.reasonCodeInput": "Raison",
|
||||
"discussions.post.closePostModal.cancel": "Annuler",
|
||||
"discussions.post.closePostModal.confirm": "Fermer le message",
|
||||
"discussions.post.closePostModal.confirm": "Fermer la publication",
|
||||
"discussions.post.label.new": "{count} Nouveau",
|
||||
"discussions.post.editedBy": "Édité par",
|
||||
"discussions.post.editedBy": "Éditée par",
|
||||
"discussions.post.editReason": "Raison",
|
||||
"discussions.post.postWithoutPreview": "Aucun aperçu disponible",
|
||||
"discussions.post.follow.description": "vous suivez ce post",
|
||||
"discussions.post.unfollow.description": "vous ne suivez pas ce post",
|
||||
"discussions.topics.sort.message": "Trié par {sortBy}",
|
||||
"discussions.post.follow.description": "vous suivez cette publication",
|
||||
"discussions.post.unfollow.description": "vous ne suivez pas cette publication",
|
||||
"discussions.topics.sort.message": "Triées par {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "Activité récente",
|
||||
"discussions.topics.sort.commentCount": "La plupart des activités",
|
||||
"discussions.topics.sort.commentCount": "Les plus actives",
|
||||
"discussions.topics.sort.courseStructure": "Structure du cours",
|
||||
"discussions.topics.unnamed.label": "Catégorie sans nom",
|
||||
"discussions.subtopics.unnamed.label": "Sous-catégorie sans nom",
|
||||
"tour.action.advance": "Suivant",
|
||||
"tour.action.dismiss": "Rejeter",
|
||||
"tour.action.end": "D'accord",
|
||||
"tour.body.notRespondedFilter": "Vous pouvez maintenant filtrer les discussions pour trouver les messages sans réponse.",
|
||||
"tour.body.notRespondedFilter": "Vous pouvez maintenant filtrer les discussions pour trouver les publications sans réponses.",
|
||||
"tour.title.notRespondedFilter": "Nouvelle option de filtrage!",
|
||||
"tour.body.responseSortTour": "Les réponses et les commentaires sont désormais triés par les plus récents en premier. Veuillez utiliser cette option pour modifier l'ordre de tri",
|
||||
"tour.title.responseSortTour": "Triez les réponses !"
|
||||
"tour.title.responseSortTour": "Triez les réponses!"
|
||||
}
|
||||
@@ -59,10 +59,7 @@
|
||||
"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",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is 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",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user