Compare commits
1 Commits
open-relea
...
inf-890
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddb7a2c13b |
5
.env
5
.env
@@ -20,5 +20,6 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
SUPPORT_URL=''
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
STAFF_FEEDBACK_URL=''
|
||||
TA_FEEDBACK_FORM= ''
|
||||
STAFF_FEEDBACK_FORM= ''
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
|
||||
@@ -21,5 +21,6 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SUPPORT_URL='https://support.edx.org'
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
STAFF_FEEDBACK_URL=''
|
||||
TA_FEEDBACK_FORM='https://learner-form.test'
|
||||
STAFF_FEEDBACK_FORM='https://staff-form.test'
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
|
||||
@@ -19,5 +19,6 @@ SEGMENT_KEY=''
|
||||
SITE_NAME='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SUPPORT_URL='https://support.edx.org'
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
STAFF_FEEDBACK_URL=''
|
||||
TA_FEEDBACK_FORM='https://learner-form.test'
|
||||
STAFF_FEEDBACK_FORM='https://staff-form.test'
|
||||
DISPLAY_FEEDBACK_BANNER='false'
|
||||
|
||||
14
.eslintrc.js
14
.eslintrc.js
@@ -1,10 +1,9 @@
|
||||
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',
|
||||
@@ -26,6 +25,7 @@ module.exports = createConfig(
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -9,17 +9,18 @@ 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: ${{ env.NODE_VER }}
|
||||
node-version: ${{ matrix.node }}
|
||||
- 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-v3.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
|
||||
2
.jest/setEnvVars.js
Normal file
2
.jest/setEnvVars.js
Normal file
@@ -0,0 +1,2 @@
|
||||
process.env.TA_FEEDBACK_FORM= 'https://learner-form.test';
|
||||
process.env.STAFF_FEEDBACK_FORM= 'https://staff-form.test';
|
||||
18
Makefile
18
Makefile
@@ -1,8 +1,7 @@
|
||||
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,pt_PT,uk,ru,hi,cs,es_AR,es_ES,fa_IR"
|
||||
transifex_langs = "ar,fr,es_419,zh_CN,tr_TR,pl,fr_CA,fr_FR,de_DE,it_IT"
|
||||
|
||||
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
|
||||
@@ -56,24 +55,9 @@ 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,15 +1,10 @@
|
||||
|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
|
||||
========================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|Build Status| |Codecov| |license|
|
||||
|
||||
Purpose
|
||||
-------
|
||||
|
||||
This repository is a React-based micro frontend for the Open edX discussion forums.
|
||||
|
||||
@@ -84,3 +79,10 @@ 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>/.env.test'],
|
||||
setupFiles: ['<rootDir>/.jest/setEnvVars.js'],
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
|
||||
32754
package-lock.json
generated
32754
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": "12.1.0",
|
||||
"@edx/frontend-component-header": "4.3.0",
|
||||
"@edx/frontend-platform": "4.6.0",
|
||||
"@edx/paragon": "20.44.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",
|
||||
"@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": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-redux": "7.2.9",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-redux": "7.2.6",
|
||||
"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": "12.8.27",
|
||||
"@edx/frontend-build": "11.0.1",
|
||||
"@edx/reactifex": "1.0.3",
|
||||
"@testing-library/jest-dom": "5.16.2",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"axios-mock-adapter": "1.20.0",
|
||||
"babel-plugin-react-intl": "8.2.25",
|
||||
|
||||
@@ -1,33 +1,9 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
"schedule:weekly",
|
||||
":automergeLinters",
|
||||
":automergeMinor",
|
||||
":automergeTesters",
|
||||
":enableVulnerabilityAlerts",
|
||||
":rebaseStalePrs",
|
||||
":semanticCommits",
|
||||
":updateNotScheduled"
|
||||
"config:base"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": [
|
||||
"devDependencies"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"lockFileMaintenance",
|
||||
"minor",
|
||||
"patch",
|
||||
"pin"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"timezone": "America/New_York"
|
||||
"patch": {
|
||||
"automerge": true
|
||||
},
|
||||
"rebaseStalePrs": true
|
||||
}
|
||||
|
||||
@@ -19,21 +19,19 @@ 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';
|
||||
|
||||
const FilterBar = ({
|
||||
function 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 = [
|
||||
{
|
||||
@@ -185,7 +183,7 @@ const FilterBar = ({
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
FilterBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -24,23 +24,25 @@ const CourseTabsNavigation = ({
|
||||
}, [courseId]);
|
||||
|
||||
return (
|
||||
<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 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,10 +56,8 @@ 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,11 +1,10 @@
|
||||
@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';
|
||||
|
||||
const Tabs = ({ children, className, ...attrs }) => {
|
||||
export default function Tabs({ children, className, ...attrs }) {
|
||||
const [
|
||||
indexOfLastVisibleChild,
|
||||
containerElementRef,
|
||||
@@ -31,28 +31,25 @@ const 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]);
|
||||
|
||||
@@ -65,7 +62,7 @@ const Tabs = ({ children, className, ...attrs }) => {
|
||||
{tabChildren}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Tabs.propTypes = {
|
||||
children: PropTypes.node,
|
||||
@@ -76,5 +73,3 @@ Tabs.defaultProps = {
|
||||
children: null,
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useRef, useState,
|
||||
useCallback, useContext, useEffect, useState,
|
||||
} from 'react';
|
||||
|
||||
import camelCase from 'lodash/camelCase';
|
||||
@@ -25,7 +25,6 @@ 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) {
|
||||
@@ -40,15 +39,14 @@ const Search = () => {
|
||||
dispatch(setSearchQuery(''));
|
||||
dispatch(setTopicFilter(''));
|
||||
dispatch(setUsernameSearch(''));
|
||||
previousSearchValueRef.current = '';
|
||||
}, [previousSearchValueRef]);
|
||||
}, []);
|
||||
|
||||
const onChange = useCallback((query) => {
|
||||
setSearchValue(query);
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback((query) => {
|
||||
if (query === '' || query === previousSearchValueRef.current) {
|
||||
if (query === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,8 +57,7 @@ const Search = () => {
|
||||
} else if (page === 'learners') {
|
||||
dispatch(setUsernameSearch(query));
|
||||
}
|
||||
previousSearchValueRef.current = query;
|
||||
}, [page, searchValue, previousSearchValueRef]);
|
||||
}, [page, searchValue]);
|
||||
|
||||
const handleIconClick = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { useLocation, useParams } from 'react-router';
|
||||
import { 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,14 +42,13 @@ 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 */
|
||||
const TinyMCEEditor = (props) => {
|
||||
function 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) => {
|
||||
@@ -100,29 +99,6 @@ const 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
|
||||
@@ -176,6 +152,6 @@ const TinyMCEEditor = (props) => {
|
||||
</AlertModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default React.memo(TinyMCEEditor);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function InsertLink() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function Issue() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function People() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function PushPin() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function Question() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function QuestionAnswer() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function QuestionAnswerOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function StarFilled() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function StarOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function ThumbUpFilled() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function ThumbUpOutline() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
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.
|
||||
@@ -140,7 +137,7 @@ export const DiscussionProvider = {
|
||||
OPEN_EDX: 'openedx',
|
||||
};
|
||||
|
||||
const BASE_PATH = '/:courseId';
|
||||
const BASE_PATH = `${getConfig().PUBLIC_PATH}:courseId`;
|
||||
|
||||
export const Routes = {
|
||||
DISCUSSIONS: {
|
||||
|
||||
@@ -13,23 +13,23 @@ import {
|
||||
import { MoreHoriz } from '@edx/paragon/icons';
|
||||
|
||||
import { ContentActions } from '../../data/constants';
|
||||
import { selectIsPostingEnabled } from '../data/selectors';
|
||||
import { selectBlackoutDate } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { useActions } from '../utils';
|
||||
import { inBlackoutDateRange, useActions } from '../utils';
|
||||
|
||||
const ActionsDropdown = ({
|
||||
function 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 isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
const actions = useActions(contentType, id);
|
||||
|
||||
const handleActions = useCallback((action) => {
|
||||
@@ -41,12 +41,12 @@ const ActionsDropdown = ({
|
||||
}
|
||||
}, [actionHandlers]);
|
||||
|
||||
// Find and remove edit action if in Posting is disabled.
|
||||
// Find and remove edit action if in blackout date range.
|
||||
useMemo(() => {
|
||||
if (!isPostingEnabled) {
|
||||
if (inBlackoutDateRange(blackoutDateRange)) {
|
||||
actions.splice(actions.findIndex(action => action.id === 'edit'), 1);
|
||||
}
|
||||
}, [actions, isPostingEnabled]);
|
||||
}, [actions, blackoutDateRange]);
|
||||
|
||||
const onClickButton = useCallback(() => {
|
||||
setTarget(buttonRef.current);
|
||||
@@ -109,7 +109,7 @@ const ActionsDropdown = ({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
ActionsDropdown.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
|
||||
@@ -13,8 +13,6 @@ 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';
|
||||
@@ -31,7 +29,6 @@ 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';
|
||||
@@ -173,7 +170,7 @@ const findOpenActionsDropdownButton = async () => (
|
||||
);
|
||||
|
||||
describe('ActionsDropdown', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
@@ -185,11 +182,6 @@ 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) => {
|
||||
|
||||
@@ -28,7 +28,7 @@ const AuthorLabel = ({
|
||||
}) => {
|
||||
timeago.register('time-locale', timeLocale);
|
||||
const intl = useIntl();
|
||||
const { courseId, enableInContextSidebar } = useContext(DiscussionContext);
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
let icon = null;
|
||||
let authorLabelMessage = null;
|
||||
|
||||
@@ -47,11 +47,11 @@ const AuthorLabel = ({
|
||||
const className = classNames('d-flex align-items-center', { 'mb-0.5': !postOrComment }, labelColor);
|
||||
|
||||
const showUserNameAsLink = useShowLearnersTab()
|
||||
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous) && !enableInContextSidebar;
|
||||
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
|
||||
|
||||
const authorName = useMemo(() => (
|
||||
<span
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500 author-name', {
|
||||
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
|
||||
'text-gray-700': isRetiredUser,
|
||||
'text-primary-500': !authorLabelMessage && !isRetiredUser,
|
||||
})}
|
||||
@@ -100,7 +100,7 @@ const AuthorLabel = ({
|
||||
{postCreatedAt && (
|
||||
<span
|
||||
title={postCreatedAt}
|
||||
className={classNames('align-content-center', {
|
||||
className={classNames('font-family-inter align-content-center', {
|
||||
'text-white': alert,
|
||||
'text-gray-500': !alert,
|
||||
})}
|
||||
@@ -114,7 +114,7 @@ const AuthorLabel = ({
|
||||
|
||||
return showUserNameAsLink
|
||||
? (
|
||||
<div className={`${className} flex-wrap`}>
|
||||
<div className={className}>
|
||||
<Link
|
||||
data-testid="learner-posts-link"
|
||||
id="learner-posts-link"
|
||||
@@ -127,7 +127,7 @@ const AuthorLabel = ({
|
||||
{labelContents}
|
||||
</div>
|
||||
)
|
||||
: <div className={`${className} flex-wrap`}>{authorName}{labelContents}</div>;
|
||||
: <div className={className}>{authorName}{labelContents}</div>;
|
||||
};
|
||||
|
||||
AuthorLabel.propTypes = {
|
||||
|
||||
@@ -21,11 +21,11 @@ let store;
|
||||
let axiosMock;
|
||||
let container;
|
||||
|
||||
function renderComponent(author, authorLabel, linkToProfile, labelColor, enableInContextSidebar) {
|
||||
function renderComponent(author, authorLabel, linkToProfile, labelColor) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId, enableInContextSidebar }}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<AuthorLabel
|
||||
author={author}
|
||||
authorLabel={authorLabel}
|
||||
@@ -66,42 +66,30 @@ 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} and enableInContextSidebar is false`,
|
||||
it(`it is "${!linkToProfile && 'not'}" clickable when linkToProfile is ${!!linkToProfile}`,
|
||||
async () => {
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor, false);
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor);
|
||||
|
||||
if (linkToProfile) {
|
||||
expect(screen.queryByTestId('learner-posts-link')).toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.queryByTestId('learner-posts-link')).not.toBeInTheDocument();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it(
|
||||
'it is not clickable when enableInContextSidebar is true',
|
||||
async () => {
|
||||
renderComponent(author, authorLabel, linkToProfile, labelColor, true);
|
||||
|
||||
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]');
|
||||
@@ -116,7 +104,6 @@ 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';
|
||||
|
||||
const Confirmation = ({
|
||||
function Confirmation({
|
||||
isOpen,
|
||||
title,
|
||||
description,
|
||||
@@ -15,7 +15,7 @@ const Confirmation = ({
|
||||
closeButtonVaraint,
|
||||
confirmButtonVariant,
|
||||
confirmButtonText,
|
||||
}) => {
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
@@ -40,7 +40,7 @@ const 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';
|
||||
|
||||
const EndorsedAlertBanner = ({
|
||||
function EndorsedAlertBanner({
|
||||
endorsed,
|
||||
endorsedAt,
|
||||
endorsedBy,
|
||||
endorsedByLabel,
|
||||
}) => {
|
||||
}) {
|
||||
timeago.register('time-locale', timeLocale);
|
||||
|
||||
const intl = useIntl();
|
||||
@@ -43,7 +43,7 @@ const EndorsedAlertBanner = ({
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
<strong className="ml-2">
|
||||
<strong className="ml-2 font-family-inter">
|
||||
{intl.formatMessage(isQuestion ? messages.answer : messages.endorsed)}
|
||||
</strong>
|
||||
</div>
|
||||
@@ -62,7 +62,7 @@ const EndorsedAlertBanner = ({
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
EndorsedAlertBanner.propTypes = {
|
||||
endorsed: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -22,7 +22,9 @@ function buildTestContent(type, buildParams) {
|
||||
return camelCaseObject(Factory.build(type, { ...buildParamsSnakeCase }, null));
|
||||
}
|
||||
|
||||
const renderComponent = (content, postType) => {
|
||||
function renderComponent(
|
||||
content, postType,
|
||||
) {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
@@ -45,7 +47,7 @@ const renderComponent = (content, postType) => {
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe.each([
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import {
|
||||
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
|
||||
} from '../../components/icons';
|
||||
import { useUserPostingEnabled } from '../data/hooks';
|
||||
import { useUserCanAddThreadInBlackoutDate } 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 isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -39,14 +39,12 @@ const HoverCard = ({
|
||||
data-testid={`hover-card-${id}`}
|
||||
id={`hover-card-${id}`}
|
||||
>
|
||||
{isUserPrivilagedInPostingRestriction && (
|
||||
{userCanAddThreadInBlackoutDate && (
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className={classNames(
|
||||
'px-2.5 py-2 border-0 font-style text-gray-700 font-size-12',
|
||||
{ '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' }}
|
||||
@@ -127,7 +125,6 @@ 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,17 +6,15 @@ import { IntlProvider } from 'react-intl';
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { camelCaseObject, 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 } from '../post-comments/data/thunks';
|
||||
import { fetchCommentResponses, fetchThreadComments } from '../post-comments/data/thunks';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThreads } from '../posts/data/thunks';
|
||||
import { DiscussionContext } from './context';
|
||||
@@ -27,11 +25,45 @@ 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}/`;
|
||||
@@ -43,15 +75,13 @@ 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);
|
||||
});
|
||||
@@ -86,7 +116,6 @@ describe('HoverCard', () => {
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
isPostingEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -94,17 +123,26 @@ describe('HoverCard', () => {
|
||||
Factory.resetAll();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(threadsApiUrl).reply(200, Factory.build('threadsResult'));
|
||||
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,
|
||||
}));
|
||||
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,
|
||||
})];
|
||||
});
|
||||
|
||||
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 } from '../utils';
|
||||
import { discussionsPath, inBlackoutDateRange } from '../utils';
|
||||
import {
|
||||
selectAreThreadsFiltered,
|
||||
selectBlackoutDate,
|
||||
selectEnableInContext,
|
||||
selectIsCourseAdmin,
|
||||
selectIsCourseStaff,
|
||||
selectIsPostingEnabled,
|
||||
selectLearnersTabEnabled,
|
||||
selectModerationSettings,
|
||||
selectPostThreadCount,
|
||||
@@ -40,14 +40,12 @@ 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;
|
||||
}
|
||||
@@ -192,16 +190,17 @@ export const useCurrentDiscussionTopic = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useUserPostingEnabled = () => {
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
export const useUserCanAddThreadInBlackoutDate = () => {
|
||||
const blackoutDateRange = useSelector(selectBlackoutDate);
|
||||
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 (isPostingEnabled || isPrivileged);
|
||||
return (!(isInBlackoutDateRange) || (isPrivileged));
|
||||
};
|
||||
|
||||
function camelToConstant(string) {
|
||||
@@ -223,7 +222,7 @@ export const useTourConfiguration = () => {
|
||||
), []);
|
||||
|
||||
const toursConfig = useMemo(() => (
|
||||
tours?.map((tour) => Object.keys(tourCheckpoints(intl)).includes(tour.tourName) && (
|
||||
tours?.map((tour) => (
|
||||
{
|
||||
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, useUserPostingEnabled } from './hooks';
|
||||
import { useCurrentDiscussionTopic, useUserCanAddThreadInBlackoutDate } 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 = (isPostingEnabled, isCourseAdmin = false) => ({
|
||||
isPostingEnabled,
|
||||
const generateApiResponse = (blackouts = [], isCourseAdmin = false) => ({
|
||||
blackouts,
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isCourseAdmin,
|
||||
@@ -30,14 +30,14 @@ const generateApiResponse = (isPostingEnabled, isCourseAdmin = false) => ({
|
||||
|
||||
describe('Hooks', () => {
|
||||
describe('useCurrentDiscussionTopic', () => {
|
||||
const ComponentWithHook = () => {
|
||||
function ComponentWithHook() {
|
||||
const topic = useCurrentDiscussionTopic();
|
||||
return (
|
||||
<div>
|
||||
{String(topic)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function renderComponent({ topicId, category }) {
|
||||
return render(
|
||||
@@ -102,15 +102,15 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUserPostingEnabled', () => {
|
||||
const ComponentWithHook = () => {
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
describe('useUserCanAddThreadInBlackoutDate', () => {
|
||||
function ComponentWithHook() {
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
return (
|
||||
<div>
|
||||
{String(isUserPrivilagedInPostingRestriction)}
|
||||
{String(userCanAddThreadInBlackoutDate)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function renderComponent() {
|
||||
return render(
|
||||
@@ -121,7 +121,7 @@ describe('Hooks', () => {
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
describe('User can add Thread in Posting Restrictions ', () => {
|
||||
describe('User can add Thread in blackoutdates ', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -136,34 +136,37 @@ describe('Hooks', () => {
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
test('when posting is not disabled and Role is Learner return true', async () => {
|
||||
test('when blackoutdates are not active and Role is Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse(true, false));
|
||||
.reply(200, generateApiResponse([], false));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when posting is not disabled and Role is not Learner return true', async () => {
|
||||
test('when blackoutdates are not active and Role is not Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse(true, true));
|
||||
.reply(200, generateApiResponse([], true));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when posting is disabled and Role is Learner return false', async () => {
|
||||
test('when blackoutdates are active and Role is Learner return false', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse(false, false));
|
||||
.reply(200, generateApiResponse([{
|
||||
start: '2022-11-25T00:00:00Z',
|
||||
end: '2050-11-25T23:59:00Z',
|
||||
}], false));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('false')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('when posting is not disabled and Role is not Learner return true', async () => {
|
||||
test('when blackoutdates are active and Role is not Learner return true', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`)
|
||||
.reply(200, generateApiResponse(false, true));
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
.reply(200, generateApiResponse([
|
||||
{ start: '2022-11-25T00:00:00Z', end: '2050-11-25T23:59:00Z' }], true));
|
||||
const { queryByText } = renderComponent();
|
||||
expect(queryByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -20,6 +20,8 @@ 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;
|
||||
@@ -28,8 +30,6 @@ 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,6 +7,7 @@ const configSlice = createSlice({
|
||||
name: 'config',
|
||||
initialState: {
|
||||
status: RequestStatus.IN_PROGRESS,
|
||||
blackouts: [],
|
||||
allowAnonymous: false,
|
||||
allowAnonymousToPeers: false,
|
||||
userRoles: [],
|
||||
@@ -17,7 +18,6 @@ const configSlice = createSlice({
|
||||
isCourseStaff: false,
|
||||
isUserAdmin: false,
|
||||
learnersTabEnabled: false,
|
||||
isPostingEnabled: false,
|
||||
settings: {
|
||||
divisionScheme: 'none',
|
||||
alwaysDivideInlineDiscussions: false,
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { PageBanner } from '@edx/paragon';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { selectconfigLoadingStatus, selectIsPostingEnabled } from '../data/selectors';
|
||||
import { selectBlackoutDate } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { inBlackoutDateRange } from '../utils';
|
||||
|
||||
const DiscussionsRestrictionBanner = () => {
|
||||
const BlackoutInformationBanner = () => {
|
||||
const intl = useIntl();
|
||||
const isPostingEnabled = useSelector(selectIsPostingEnabled);
|
||||
const configLoadingStatus = useSelector(selectconfigLoadingStatus);
|
||||
const blackoutDate = useSelector(selectBlackoutDate);
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
|
||||
const isDiscussionsBlackout = useMemo(() => (
|
||||
inBlackoutDateRange(blackoutDate)
|
||||
), [blackoutDate]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setShowBanner(false);
|
||||
}, []);
|
||||
@@ -22,7 +25,7 @@ const DiscussionsRestrictionBanner = () => {
|
||||
return (
|
||||
<PageBanner
|
||||
variant="accentB"
|
||||
show={!isPostingEnabled && showBanner && configLoadingStatus === RequestStatus.SUCCESSFUL}
|
||||
show={isDiscussionsBlackout && showBanner}
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
@@ -33,4 +36,4 @@ const DiscussionsRestrictionBanner = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscussionsRestrictionBanner;
|
||||
export default BlackoutInformationBanner;
|
||||
@@ -8,7 +8,7 @@ import { initializeStore } from '../../store';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { fetchConfigSuccess } from '../data/slices';
|
||||
import messages from '../messages';
|
||||
import DiscussionsRestrictionBanner from './DiscussionsRestrictionBanner';
|
||||
import BlackoutInformationBanner from './BlackoutInformationBanner';
|
||||
|
||||
let store;
|
||||
let container;
|
||||
@@ -20,13 +20,13 @@ activeEndDate.setDate(activeEndDate.getDate() + 2);
|
||||
activeStartDate = activeStartDate.toISOString();
|
||||
activeEndDate = activeEndDate.toISOString();
|
||||
|
||||
const getConfigData = (isPostingEnabled) => ({
|
||||
const getConfigData = (blackouts = []) => ({
|
||||
id: 'course-v1:edX+DemoX+Demo_Course',
|
||||
userRoles: ['Admin', 'Student'],
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isUserAdmin: false,
|
||||
isPostingEnabled,
|
||||
blackouts,
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
@@ -34,7 +34,7 @@ function renderComponent() {
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{ courseId }}>
|
||||
<DiscussionsRestrictionBanner />
|
||||
<BlackoutInformationBanner />
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
@@ -43,7 +43,7 @@ function renderComponent() {
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('Discussions Restriction Banner', () => {
|
||||
describe('Blackout Information Banner', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -56,11 +56,13 @@ describe('Discussions Restriction Banner', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ isPostingEnabled: false, visibility: true },
|
||||
{ isPostingEnabled: true, visibility: false },
|
||||
])('Test Discussions Restriction is visible on app load if posting is disabled', async ({ isPostingEnabled, visibility }) => {
|
||||
{ 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 }) => {
|
||||
store = initializeStore();
|
||||
await store.dispatch(fetchConfigSuccess(getConfigData(isPostingEnabled)));
|
||||
await store.dispatch(fetchConfigSuccess(getConfigData(blackouts)));
|
||||
renderComponent();
|
||||
if (visibility) {
|
||||
const element = await screen.findByRole('alert');
|
||||
@@ -55,7 +55,7 @@ const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => {
|
||||
'd-none': !displaySidebar,
|
||||
'd-flex overflow-auto box-shadow-centered-1': displaySidebar,
|
||||
'w-100': !isOnDesktop,
|
||||
'w-25 sidebar-desktop-width': isOnDesktop && !isOnXLDesktop,
|
||||
'sidebar-desktop-width': isOnDesktop && !isOnXLDesktop,
|
||||
'w-25 sidebar-XL-width': isOnXLDesktop,
|
||||
'min-content-height': !enableInContextSidebar,
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React, { lazy, Suspense, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
@@ -8,6 +7,7 @@ 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,9 +29,10 @@ 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 DiscussionsRestrictionBanner = lazy(() => import('./DiscussionsRestrictionBanner'));
|
||||
const BlackoutInformationBanner = lazy(() => import('./BlackoutInformationBanner'));
|
||||
const DiscussionContent = lazy(() => import('./DiscussionContent'));
|
||||
const DiscussionSidebar = lazy(() => import('./DiscussionSidebar'));
|
||||
const InformationBanner = lazy(() => import('./InformationBanner'));
|
||||
|
||||
const DiscussionsHome = () => {
|
||||
const location = useLocation();
|
||||
@@ -46,6 +47,7 @@ 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;
|
||||
@@ -92,7 +94,8 @@ const DiscussionsHome = () => {
|
||||
)}
|
||||
<PostActionsBar />
|
||||
</div>
|
||||
<DiscussionsRestrictionBanner />
|
||||
{isFeedbackBannerVisible && <InformationBanner />}
|
||||
<BlackoutInformationBanner />
|
||||
</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();
|
||||
waitFor(() => expect(screen.queryByRole('banner')).toBeInTheDocument());
|
||||
expect(screen.queryByRole('banner')).toBeInTheDocument();
|
||||
expect(document.getElementById('courseTabsNavigation')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('contentinfo')).toBeInTheDocument();
|
||||
});
|
||||
@@ -172,8 +172,7 @@ 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,
|
||||
@@ -194,8 +193,7 @@ 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, {
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -23,9 +22,9 @@ export default function useFeedbackWrapper() {
|
||||
|
||||
useEffect(() => {
|
||||
if (configStatus === RequestStatus.SUCCESSFUL) {
|
||||
let url = getConfig().LEARNER_FEEDBACK_URL;
|
||||
let url = '//w.usabilla.com/9e6036348fa1.js';
|
||||
if (isStaff || isUserGroupTA || isCourseAdmin || isCourseStaff) {
|
||||
url = getConfig().STAFF_FEEDBACK_URL;
|
||||
url = '//w.usabilla.com/767740a06856.js';
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
63
src/discussions/discussions-home/InformationBanner.jsx
Normal file
63
src/discussions/discussions-home/InformationBanner.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
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;
|
||||
136
src/discussions/discussions-home/InformationBanner.test.jsx
Normal file
136
src/discussions/discussions-home/InformationBanner.test.jsx
Normal file
@@ -0,0 +1,136 @@
|
||||
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,7 +8,6 @@ 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,7 +9,6 @@ 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,5 +1,4 @@
|
||||
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,6 +92,7 @@ describe('InContext Topic Posts View', () => {
|
||||
enableInContext: true,
|
||||
provider: 'openedx',
|
||||
hasModerationPrivileges: true,
|
||||
blackouts: [],
|
||||
},
|
||||
});
|
||||
Factory.resetAll();
|
||||
@@ -205,8 +206,7 @@ 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,8 +226,7 @@ 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();
|
||||
@@ -254,8 +253,7 @@ 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' });
|
||||
@@ -263,8 +261,7 @@ 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';
|
||||
|
||||
const BackButton = ({
|
||||
function BackButton({
|
||||
intl, path, title, loading,
|
||||
}) => {
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
@@ -32,7 +32,7 @@ const BackButton = ({
|
||||
<div className="border-bottom border-light-400" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
BackButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -20,21 +20,12 @@ 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) => {
|
||||
@@ -60,21 +51,12 @@ 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';
|
||||
|
||||
const TopicSearchResultBar = ({ intl }) => {
|
||||
function TopicSearchResultBar({ intl }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
@@ -21,7 +21,7 @@ const TopicSearchResultBar = ({ intl }) => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
TopicSearchResultBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable no-unused-vars, react/forbid-prop-types */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -110,8 +110,7 @@ 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();
|
||||
|
||||
@@ -123,8 +122,7 @@ 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,7 +66,6 @@ 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,4 +1,3 @@
|
||||
/* eslint-disable default-param-last */
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
@@ -202,8 +201,7 @@ 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,
|
||||
}) => {
|
||||
@@ -229,8 +227,7 @@ 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();
|
||||
@@ -260,8 +257,7 @@ 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);
|
||||
@@ -278,8 +274,7 @@ 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,8 +29,7 @@ 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();
|
||||
@@ -39,23 +38,20 @@ 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,8 +37,7 @@ 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();
|
||||
|
||||
@@ -56,8 +55,7 @@ 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];
|
||||
@@ -67,11 +65,9 @@ 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();
|
||||
|
||||
@@ -79,8 +75,7 @@ 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();
|
||||
@@ -111,8 +106,7 @@ 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();
|
||||
|
||||
@@ -122,6 +116,5 @@ 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';
|
||||
|
||||
const LearnerPostFilterBar = () => {
|
||||
function LearnerPostFilterBar() {
|
||||
const dispatch = useDispatch();
|
||||
const { courseId } = useParams();
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
@@ -98,6 +98,6 @@ const LearnerPostFilterBar = () => {
|
||||
showCohortsFilter={userHasModerationPrivileges || userIsGroupTa}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default LearnerPostFilterBar;
|
||||
|
||||
@@ -168,9 +168,24 @@ 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 disabled by the course team',
|
||||
defaultMessage: 'Posting in discussions is temporarily disabled by the course team',
|
||||
description: 'Informative text when discussion posting is disabled',
|
||||
},
|
||||
imageWarningMessage: {
|
||||
|
||||
@@ -74,7 +74,6 @@ const PostCommentsView = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||
<PostCommentsContext.Provider value={{
|
||||
isClosed: closed,
|
||||
postType: type,
|
||||
|
||||
@@ -10,7 +10,6 @@ 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';
|
||||
@@ -21,13 +20,12 @@ 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, removeComment } from './data/thunks';
|
||||
import { fetchCommentResponses, fetchThreadComments, removeComment } from './data/thunks';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
import './data/__factories__';
|
||||
@@ -41,21 +39,44 @@ const discussionPostId = 'thread-1';
|
||||
const questionPostId = 'thread-2';
|
||||
const closedPostId = 'thread-2';
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const reverseOrder = true;
|
||||
const enableInContextSidebar = false;
|
||||
let store;
|
||||
let axiosMock;
|
||||
let testLocation;
|
||||
let container;
|
||||
let unmount;
|
||||
|
||||
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 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() {
|
||||
@@ -69,16 +90,13 @@ 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);
|
||||
});
|
||||
@@ -89,30 +107,12 @@ async function getThreadAPIResponse(attr = null) {
|
||||
await executeThunk(fetchThread(discussionPostId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
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) {
|
||||
function renderComponent(postId) {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{ courseId, postId, isClosed }}
|
||||
value={{ courseId, postId }}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/posts/${postId}`]}>
|
||||
<DiscussionContent />
|
||||
@@ -132,45 +132,6 @@ function renderComponent(postId, isClosed = false) {
|
||||
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({
|
||||
@@ -204,12 +165,10 @@ 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();
|
||||
});
|
||||
|
||||
@@ -222,16 +181,6 @@ 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'));
|
||||
@@ -312,7 +261,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-2')).toBeInTheDocument());
|
||||
await waitFor(async () => expect(await screen.findByTestId('reply-comment-7')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should allow editing an existing comment', async () => {
|
||||
@@ -330,6 +279,23 @@ 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));
|
||||
@@ -359,39 +325,6 @@ 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));
|
||||
@@ -465,20 +398,6 @@ 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');
|
||||
@@ -564,10 +483,8 @@ 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 () => {
|
||||
await waitFor(() => renderComponent('unloaded-id'));
|
||||
renderComponent('unloaded-id');
|
||||
expect(await screen.findByText('Thread not found', { exact: true }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
@@ -580,70 +497,6 @@ 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', () => {
|
||||
@@ -652,8 +505,8 @@ describe('ThreadView', () => {
|
||||
it('initially loads only the first page', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
await waitFor(() => screen.findByTestId('reply-comment-2'));
|
||||
expect(screen.queryByTestId('reply-comment-3')).not.toBeInTheDocument();
|
||||
await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
expect(screen.queryByTestId('reply-comment-8')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of responses', async () => {
|
||||
@@ -663,7 +516,8 @@ describe('ThreadView', () => {
|
||||
await act(async () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
await screen.findByTestId('reply-comment-3');
|
||||
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
});
|
||||
|
||||
it('newly loaded responses are appended to the old ones', async () => {
|
||||
@@ -674,9 +528,9 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('reply-comment-3');
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
// check that comments from the first page are also displayed
|
||||
expect(screen.queryByTestId('reply-comment-2')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('reply-comment-7')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('load more button is hidden when no more responses pages to load', async () => {
|
||||
@@ -687,7 +541,8 @@ describe('ThreadView', () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
|
||||
await screen.findByTestId('reply-comment-3');
|
||||
await screen.findByTestId('reply-comment-8');
|
||||
await expect(findLoadMoreCommentsResponsesButton()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -721,21 +576,21 @@ describe('ThreadView', () => {
|
||||
it('shows action dropdown for replies', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-2'));
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
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-2'));
|
||||
expect(within(reply).queryByTestId('comment-2')).toBeInTheDocument();
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
expect(within(reply).queryByTestId('comment-7')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows delete confirmation modal', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-2'));
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
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 { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
|
||||
import { isLastElementOfList } from '../../utils';
|
||||
import { usePostComments } from '../data/hooks';
|
||||
import messages from '../messages';
|
||||
@@ -16,8 +16,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
const intl = useIntl();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const { isClosed } = useContext(PostCommentsContext);
|
||||
const isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const {
|
||||
endorsedCommentsIds,
|
||||
unEndorsedCommentsIds,
|
||||
@@ -36,7 +35,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
|
||||
const handleDefinition = useCallback((message, commentsLength) => (
|
||||
<div
|
||||
className="comment-line mx-4 my-14px text-gray-700 font-style"
|
||||
className="mx-4 my-14px text-gray-700 font-style"
|
||||
role="heading"
|
||||
aria-level="2"
|
||||
>
|
||||
@@ -73,7 +72,6 @@ const CommentsView = ({ endorsed }) => {
|
||||
), [hasMorePages, isLoading, handleLoadMoreResponses]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{((hasMorePages && isLoading) || !isLoading) && (
|
||||
<>
|
||||
@@ -90,7 +88,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
{handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)}
|
||||
{unEndorsedCommentsIds.length === 0 && <br />}
|
||||
{handleComments(unEndorsedCommentsIds, false)}
|
||||
{(isUserPrivilagedInPostingRestriction && !!unEndorsedCommentsIds.length && !isClosed) && (
|
||||
{(userCanAddThreadInBlackoutDate && !!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 { useUserPostingEnabled } from '../../../data/hooks';
|
||||
import { useUserCanAddThreadInBlackoutDate } 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 isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
|
||||
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 (isUserPrivilagedInPostingRestriction) {
|
||||
if (userCanAddThreadInBlackoutDate) {
|
||||
setReplying(true);
|
||||
}
|
||||
}, [isUserPrivilagedInPostingRestriction]);
|
||||
}, [userCanAddThreadInBlackoutDate]);
|
||||
|
||||
const handleCommentLike = useCallback(async () => {
|
||||
await dispatch(editComment(id, { voted: !voted }));
|
||||
@@ -265,7 +265,7 @@ const Comment = ({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
!isClosed && isUserPrivilagedInPostingRestriction && (inlineReplies.length >= 5) && (
|
||||
!isClosed && userCanAddThreadInBlackoutDate && (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';
|
||||
|
||||
const CommentEditor = ({
|
||||
function CommentEditor({
|
||||
comment,
|
||||
edit,
|
||||
formClasses,
|
||||
onCloseEditor,
|
||||
}) => {
|
||||
}) {
|
||||
const {
|
||||
id, threadId, parentId, rawBody, author, lastEdit,
|
||||
} = comment;
|
||||
@@ -60,8 +60,7 @@ const CommentEditor = ({
|
||||
|
||||
const initialValues = {
|
||||
comment: rawBody,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
editReasonCode: lastEdit?.reasonCode || (userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined),
|
||||
editReasonCode: lastEdit?.reasonCode || (userIsStaff ? 'violates-guidelines' : ''),
|
||||
};
|
||||
|
||||
const handleCloseEditor = useCallback((resetForm) => {
|
||||
@@ -177,13 +176,13 @@ const CommentEditor = ({
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
CommentEditor.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
author: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
lastEdit: PropTypes.shape({}),
|
||||
lastEdit: PropTypes.object,
|
||||
parentId: PropTypes.string,
|
||||
rawBody: PropTypes.string,
|
||||
threadId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -129,9 +129,9 @@ const Reply = ({ responseId }) => {
|
||||
</div>
|
||||
<div
|
||||
className="bg-light-300 pl-4 pt-2.5 pr-2.5 pb-10px flex-fill"
|
||||
style={{ borderRadius: '0rem 0.375rem 0.375rem', maxWidth: 'calc(100% - 50px)' }}
|
||||
style={{ borderRadius: '0rem 0.375rem 0.375rem' }}
|
||||
>
|
||||
<div className="d-flex flex-row justify-content-between">
|
||||
<div className="d-flex flex-row justify-content-between" style={{ height: '24px' }}>
|
||||
<AuthorLabel
|
||||
author={author}
|
||||
authorLabel={authorLabel}
|
||||
|
||||
@@ -59,7 +59,6 @@ Factory.define('commentsResult')
|
||||
return Factory.buildList('comment', len, {
|
||||
thread_id: threadId,
|
||||
parent_id: parentId,
|
||||
endorsed,
|
||||
}, {
|
||||
endorsedBy: endorsed ? 'staff' : null,
|
||||
childCount,
|
||||
|
||||
@@ -20,14 +20,16 @@ export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussi
|
||||
* @param enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const getThreadComments = async (threadId, {
|
||||
endorsed,
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
enableInContextSidebar = false,
|
||||
signal,
|
||||
} = {}) => {
|
||||
export async function getThreadComments(
|
||||
threadId, {
|
||||
endorsed,
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
enableInContextSidebar = false,
|
||||
signal,
|
||||
} = {},
|
||||
) {
|
||||
const params = snakeCaseObject({
|
||||
threadId,
|
||||
endorsed: EndorsementValue[endorsed],
|
||||
@@ -40,7 +42,7 @@ export const getThreadComments = async (threadId, {
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient().get(getCommentsApiUrl(), { params: { ...params, signal } });
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a responses to a comment.
|
||||
@@ -49,11 +51,13 @@ export const getThreadComments = async (threadId, {
|
||||
* @param {number=} pageSize
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const getCommentResponses = async (commentId, {
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
} = {}) => {
|
||||
export async function getCommentResponses(
|
||||
commentId, {
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
} = {},
|
||||
) {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const params = snakeCaseObject({
|
||||
page,
|
||||
@@ -64,7 +68,7 @@ export const getCommentResponses = async (commentId, {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(url, { params });
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts a comment.
|
||||
@@ -74,13 +78,13 @@ export const getCommentResponses = async (commentId, {
|
||||
* @param {boolean} enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const postComment = async (comment, threadId, parentId = null, enableInContextSidebar = false) => {
|
||||
export async function postComment(comment, threadId, parentId = null, enableInContextSidebar = false) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getCommentsApiUrl(), snakeCaseObject({
|
||||
threadId, raw_body: comment, parentId, enableInContextSidebar,
|
||||
}));
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates existing comment.
|
||||
@@ -92,13 +96,13 @@ export const postComment = async (comment, threadId, parentId = null, enableInCo
|
||||
* @param {string=} editReasonCode The moderation reason code for editing.
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const updateComment = async (commentId, {
|
||||
export async function updateComment(commentId, {
|
||||
comment,
|
||||
voted,
|
||||
flagged,
|
||||
endorsed,
|
||||
editReasonCode,
|
||||
}) => {
|
||||
}) {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
const postData = snakeCaseObject({
|
||||
raw_body: comment,
|
||||
@@ -111,14 +115,14 @@ export const updateComment = async (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 const deleteComment = async (commentId) => {
|
||||
export async function deleteComment(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,7 +26,6 @@ Factory.define('thread')
|
||||
'type',
|
||||
'voted',
|
||||
'pinned',
|
||||
'copy_link',
|
||||
],
|
||||
author: 'test_user',
|
||||
author_label: 'Staff',
|
||||
|
||||
@@ -28,20 +28,22 @@ export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussio
|
||||
* @param {number} cohort
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const getThreads = async (courseId, {
|
||||
topicIds,
|
||||
page,
|
||||
pageSize,
|
||||
textSearch,
|
||||
orderBy,
|
||||
following,
|
||||
view,
|
||||
author,
|
||||
flagged,
|
||||
threadType,
|
||||
countFlagged,
|
||||
cohort,
|
||||
} = {}) => {
|
||||
export async function getThreads(
|
||||
courseId, {
|
||||
topicIds,
|
||||
page,
|
||||
pageSize,
|
||||
textSearch,
|
||||
orderBy,
|
||||
following,
|
||||
view,
|
||||
author,
|
||||
flagged,
|
||||
threadType,
|
||||
countFlagged,
|
||||
cohort,
|
||||
} = {},
|
||||
) {
|
||||
const params = snakeCaseObject({
|
||||
courseId,
|
||||
page,
|
||||
@@ -60,19 +62,19 @@ export const getThreads = async (courseId, {
|
||||
});
|
||||
const { data } = await getAuthenticatedHttpClient().get(getThreadsApiUrl(), { params });
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single thread.
|
||||
* @param {string} threadId
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const getThread = async (threadId, courseId) => {
|
||||
export async function getThread(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.
|
||||
@@ -88,7 +90,7 @@ export const getThread = async (threadId, courseId) => {
|
||||
* @param {boolean} enableInContextSidebar
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const postThread = async (
|
||||
export async function postThread(
|
||||
courseId,
|
||||
topicId,
|
||||
type,
|
||||
@@ -101,7 +103,7 @@ export const postThread = async (
|
||||
anonymousToPeers,
|
||||
} = {},
|
||||
enableInContextSidebar = false,
|
||||
) => {
|
||||
) {
|
||||
const postData = snakeCaseObject({
|
||||
courseId,
|
||||
topicId,
|
||||
@@ -117,7 +119,7 @@ export const postThread = async (
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getThreadsApiUrl(), postData);
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing thread.
|
||||
@@ -136,7 +138,7 @@ export const postThread = async (
|
||||
* @param {string} closeReasonCode
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const updateThread = async (threadId, {
|
||||
export async function updateThread(threadId, {
|
||||
flagged,
|
||||
voted,
|
||||
read,
|
||||
@@ -149,7 +151,7 @@ export const updateThread = async (threadId, {
|
||||
pinned,
|
||||
editReasonCode,
|
||||
closeReasonCode,
|
||||
} = {}) => {
|
||||
} = {}) {
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
const patchData = snakeCaseObject({
|
||||
topicId,
|
||||
@@ -168,17 +170,17 @@ export const updateThread = async (threadId, {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(url, patchData, { headers: { 'Content-Type': 'application/merge-patch+json' } });
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a thread.
|
||||
* @param {string} threadId
|
||||
*/
|
||||
export const deleteThread = async (threadId) => {
|
||||
export async function deleteThread(threadId) {
|
||||
const url = `${getThreadsApiUrl()}${threadId}/`;
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file.
|
||||
@@ -188,7 +190,7 @@ export const deleteThread = async (threadId) => {
|
||||
* @param {string} threadKey
|
||||
* @returns {Promise<{ location: string }>}
|
||||
*/
|
||||
export const uploadFile = async (blob, filename, courseId, threadKey) => {
|
||||
export async function uploadFile(blob, filename, courseId, threadKey) {
|
||||
const uploadUrl = `${getCoursesApiUrl()}${courseId}/upload`;
|
||||
const formData = new FormData();
|
||||
formData.append('thread_key', threadKey);
|
||||
@@ -198,4 +200,4 @@ export const uploadFile = async (blob, filename, courseId, threadKey) => {
|
||||
throw new Error(data.developer_message);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
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 { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { useUserCanAddThreadInBlackoutDate } 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 isUserPrivilagedInPostingRestriction = useUserPostingEnabled();
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const { enableInContextSidebar, page } = useContext(DiscussionContext);
|
||||
|
||||
const handleCloseInContext = useCallback(() => {
|
||||
@@ -49,15 +49,13 @@ const PostActionsBar = () => {
|
||||
{intl.formatMessage(messages.title)}
|
||||
</h4>
|
||||
)}
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && isUserPrivilagedInPostingRestriction && (
|
||||
{loadingStatus === RequestStatus.SUCCESSFUL && userCanAddThreadInBlackoutDate && (
|
||||
<>
|
||||
{!enableInContextSidebar && <div className="border-right border-light-400 mx-3" />}
|
||||
<Button
|
||||
variant={enableInContextSidebar ? 'plain' : 'brand'}
|
||||
className={classNames(
|
||||
'my-0 font-style border-0 line-height-24',
|
||||
{ 'px-3 py-10px border-0': enableInContextSidebar },
|
||||
)}
|
||||
className={classNames('my-0 font-style border-0 line-height-24',
|
||||
{ 'px-3 py-10px border-0': enableInContextSidebar })}
|
||||
onClick={handleAddPost}
|
||||
size={enableInContextSidebar ? 'md' : 'sm'}
|
||||
>
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
selectUserIsGroupTa,
|
||||
selectUserIsStaff,
|
||||
} from '../../data/selectors';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { EmptyPage } from '../../empty-posts';
|
||||
import {
|
||||
selectArchivedTopics,
|
||||
@@ -132,11 +131,9 @@ 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) {
|
||||
@@ -291,18 +288,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)}>
|
||||
|
||||
@@ -19,9 +19,9 @@ const PostTypeCard = ({
|
||||
<label htmlFor={`post-type-${value}`} className="d-flex p-0 my-0 mr-3">
|
||||
<Form.Radio value={value} id={`post-type-${value}`} className="sr-only">{type}</Form.Radio>
|
||||
<Card
|
||||
className={classNames('shadow-none', {
|
||||
'border-primary-500-2': selected,
|
||||
'border-light-400-2': !selected,
|
||||
className={classNames('border-2 shadow-none', {
|
||||
'border-primary': selected,
|
||||
'border-light-400': !selected,
|
||||
})}
|
||||
style={{ cursor: 'pointer', width: `${enableInContextSidebar ? '10.021rem' : '14.25rem'}` }}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, useToggle } from '@edx/paragon';
|
||||
|
||||
import HTMLLoader from '../../../components/HTMLLoader';
|
||||
import { ContentActions, getFullUrl } from '../../../data/constants';
|
||||
import { ContentActions } 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 } = useContext(DiscussionContext);
|
||||
const courseId = useSelector((state) => state.config.id);
|
||||
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(getFullUrl(`${courseId}/posts/${postId}`));
|
||||
}, [window.location.origin, postId, courseId]);
|
||||
const handlePostCopyLink = useCallback(() => navigator.clipboard.writeText(
|
||||
`${window.location.origin}/${courseId}/posts/${postId}`,
|
||||
), [window.location.origin, postId, courseId]);
|
||||
|
||||
const handlePostPin = useCallback(() => dispatch(
|
||||
updateExistingThread(postId, { pinned: !pinned }),
|
||||
), [postId, pinned]);
|
||||
const handlePostPin = useCallback(() => dispatch(updateExistingThread(
|
||||
postId, { pinned: !pinned },
|
||||
)), [postId, pinned]);
|
||||
|
||||
const handlePostReport = useCallback(() => {
|
||||
if (abuseFlagged) {
|
||||
@@ -188,10 +188,8 @@ 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,3 +1,4 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -6,7 +7,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Badge, Icon } from '@edx/paragon';
|
||||
import { Badge, Icon, Truncate } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
|
||||
import { PushPin } from '../../../components/icons';
|
||||
@@ -50,107 +51,105 @@ 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',
|
||||
{ 'bg-light-300': isPostRead },
|
||||
{ 'post-summary-card-selected': id === selectedPostId },
|
||||
)
|
||||
}
|
||||
to={linkUrl}
|
||||
aria-current={checkIsSelected ? 'page' : undefined}
|
||||
role="option"
|
||||
tabIndex={(checkIsSelected || idx === 0) ? 0 : -1}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<AuthorLabel
|
||||
author={author || intl.formatMessage(messages.anonymous)}
|
||||
<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 })
|
||||
}
|
||||
>
|
||||
<PostAvatar
|
||||
postType={type}
|
||||
author={author}
|
||||
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}
|
||||
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'}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AuthorLabel
|
||||
author={author || intl.formatMessage(messages.anonymous)}
|
||||
authorLabel={authorLabel}
|
||||
labelColor={authorLabelColor && `text-${authorLabelColor}`}
|
||||
/>
|
||||
<PostSummaryFooter
|
||||
postId={id}
|
||||
voted={voted}
|
||||
voteCount={voteCount}
|
||||
following={following}
|
||||
commentCount={commentCount}
|
||||
unreadCommentCount={unreadCommentCount}
|
||||
groupId={groupId}
|
||||
groupName={groupName}
|
||||
createdAt={createdAt}
|
||||
preview
|
||||
showNewCountLabel={isPostRead}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!showDivider && pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
{!showDivider && pinned && <div className="pt-1 bg-light-500 border-top border-light-700" />}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ const TopicGroupBase = ({
|
||||
|
||||
const renderFilteredTopics = useMemo(() => {
|
||||
if (!hasFilteredSubtopics) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* 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 ?? 0;
|
||||
count += nonCoursewareTopicsList?.length;
|
||||
// Counting legacy topics
|
||||
if (provider === DiscussionProvider.LEGACY) {
|
||||
const categories = topicsSelector?.categoryIds;
|
||||
|
||||
@@ -17,7 +17,6 @@ const DiscussionsProductTour = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{!isEmpty(config) && (
|
||||
<ProductTour
|
||||
|
||||
@@ -267,6 +267,28 @@ 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; }
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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,
|
||||
];
|
||||
26
src/i18n/index.jsx
Normal file
26
src/i18n/index.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
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": "العودة إلى قائمة المواضيع",
|
||||
"discussions.topics.backAlt": "Back to topics list",
|
||||
"discussions.topics.discussions": "{count، plural, =0 {لا مناقشات} one {مناقشة واحدة} two {مناقشتان} few {# مناقشات} many {# مناقشة} other {# مناقشات}",
|
||||
"discussions.topics.questions": "{count، plural, =0 {لا مناقشات} one {سؤال واحد} two {سؤالان} few {# اسئلة} many {# سؤالًا} other {# أسئلة}",
|
||||
"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.unnamed.section.label": "Unnamed Section",
|
||||
"discussions.topics.unnamed.subsection.label": "Unnamed Subsection",
|
||||
"discussions.subtopics.unnamed.topic.label": "Unnamed Topic",
|
||||
"discussions.topics.title": "No topic exists",
|
||||
"discussions.topics.createTopic": "Please contact you admin to create a topic",
|
||||
"discussions.topics.nothing": "لا شيء هنا حتى الان",
|
||||
"discussions.topics.nothing": "Nothing here yet",
|
||||
"discussions.topics.archived.label": "مؤرشف",
|
||||
"discussions.learner.reported": "تم الإبلاغ عن {reported}",
|
||||
"discussions.learner.previouslyReported": "تم الإبلاغ عن {previouslyReported} من قبل",
|
||||
@@ -59,7 +59,10 @@
|
||||
"discussions.authors.label.ta": "أستاذ مساعد",
|
||||
"discussions.learner.loadMostPosts": "تواريخ تعطيل نشطة حاليا. لا يمكن النشر في المناقشات خلال هذه الفترة.",
|
||||
"discussions.post.anonymous.author": "مجهول",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is disabled by the course team",
|
||||
"discussion.banner.welcomeMessage": "🎉 مرحبًا بك في تجربة المناقشات الجديدة والمحسّنة!",
|
||||
"discussion.banner.learnMore": "معرفة المزيد",
|
||||
"discussion.banner.shareFeedback": "شاركنا رأيك",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is temporarily disabled by the course team",
|
||||
"discussions.editor.image.warning.message": "لن تظهر الصور التي يزيد عرضها أو ارتفاعها عن 999 بكسل عند عرض المنشور أو الرد او التعليق باستخدام مناقشات المساق المضمّنة",
|
||||
"discussions.editor.image.warning.title": "تحذير!",
|
||||
"discussions.editor.image.warning.dismiss": "حسنًا",
|
||||
@@ -139,7 +142,7 @@
|
||||
"discussions.post.editor.anonymousPost": "النشر كمجهول",
|
||||
"discussions.post.editor.anonymousToPeersPost": "انشر ﻷقرانك كمجهول",
|
||||
"discussions.editor.posts.editReasonCode": "سبب التعديل",
|
||||
"discussions.editor.posts.showPreview.button": "عرض المعاينة",
|
||||
"discussions.editor.posts.showPreview.button": "Show preview",
|
||||
"discussions.topic.noName.label": "تصنيف دون اسم",
|
||||
"discussions.subtopic.noName.label": "تصنيف فرعي دون اسم",
|
||||
"discussions.posts.filter.showALl": "عرض الكل",
|
||||
@@ -161,22 +164,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": "أضف الرد",
|
||||
"discussions.post.addResponse": "Add response",
|
||||
"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.followed": "Followed",
|
||||
"discussions.post.notFollowed": "Not Followed",
|
||||
"discussions.post.answered": "تمّت الإجابة",
|
||||
"discussions.post.unFollow": "إلغاء المتابعة",
|
||||
"discussions.post.like": "أعجبني",
|
||||
"discussions.post.removeLike": "إلغاء الإغجاب",
|
||||
"discussions.post.liked": "اعحبني",
|
||||
"discussions.post.likes": "الإعجابات",
|
||||
"discussions.post.liked": "liked",
|
||||
"discussions.post.likes": "likes",
|
||||
"discussions.post.viewActivity": "عرض النشاط",
|
||||
"discussions.post.activity": "النشاط",
|
||||
"discussions.post.activity": "Activity",
|
||||
"discussions.post.closed": "منشور مقفل أمام الردود والتعليقات",
|
||||
"discussions.post.relatedTo": "متعلق بـ",
|
||||
"discussions.editor.delete.post.title": "حذف المنشور",
|
||||
@@ -193,7 +196,7 @@
|
||||
"discussions.post.editedBy": "عدّله",
|
||||
"discussions.post.editReason": "السبب",
|
||||
"discussions.post.postWithoutPreview": "المعاينة غير متاحة",
|
||||
"discussions.post.follow.description": "أنت تتابع هذا المنشور/الرد",
|
||||
"discussions.post.follow.description": "you are following this post",
|
||||
"discussions.post.unfollow.description": "you are not following this post",
|
||||
"discussions.topics.sort.message": "مرتبة حسب {sortBy}",
|
||||
"discussions.topics.sort.lastActivity": "الأحدث نشاطًا",
|
||||
@@ -201,11 +204,11 @@
|
||||
"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.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.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "فرز الردود!"
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
{
|
||||
"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,7 +59,10 @@
|
||||
"discussions.authors.label.ta": "TA",
|
||||
"discussions.learner.loadMostPosts": "Mehr Beiträge laden",
|
||||
"discussions.post.anonymous.author": "Anonym",
|
||||
"discussion.blackoutBanner.information": "Posting in discussions is disabled by the course team",
|
||||
"discussion.banner.welcomeMessage": "🎉 Willkommen beim neuen und verbesserten Diskussionserlebnis!",
|
||||
"discussion.banner.learnMore": "Lernen Sie mehr",
|
||||
"discussion.banner.shareFeedback": "Feedback teilen",
|
||||
"discussion.blackoutBanner.information": "Das Posten in Diskussionen wird vom Kursteam vorübergehend deaktiviert",
|
||||
"discussions.editor.image.warning.message": "Bilder mit einer Breite oder Höhe von mehr als 999 Pixel sind nicht sichtbar, wenn der Beitrag, die Antwort oder der Kommentar über Inline-Kursdiskussionen angezeigt werden",
|
||||
"discussions.editor.image.warning.title": "Warnung!",
|
||||
"discussions.editor.image.warning.dismiss": "Ok",
|
||||
|
||||
@@ -59,7 +59,10 @@
|
||||
"discussions.authors.label.ta": "ejército de reserva",
|
||||
"discussions.learner.loadMostPosts": "Cargar más mensajes\n",
|
||||
"discussions.post.anonymous.author": "anónimo",
|
||||
"discussion.blackoutBanner.information": "El equipo del curso ha desactivado la publicación en los debates.",
|
||||
"discussion.banner.welcomeMessage": "🎉 ¡Bienvenido a la nueva y mejorada experiencia de debates!",
|
||||
"discussion.banner.learnMore": "Aprender más",
|
||||
"discussion.banner.shareFeedback": "Compartir comentarios",
|
||||
"discussion.blackoutBanner.information": "El equipo del curso deshabilita temporalmente la publicación en discusiones",
|
||||
"discussions.editor.image.warning.message": "Las imágenes que tengan un ancho o alto superior a 999 px no serán visibles cuando la publicación, la respuesta o el comentario se vean mediante debates en línea del curso.",
|
||||
"discussions.editor.image.warning.title": "¡Advertencia!",
|
||||
"discussions.editor.image.warning.dismiss": "Aceptar",
|
||||
@@ -206,6 +209,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": "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!"
|
||||
"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!"
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
{
|
||||
"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!"
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
{
|
||||
"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!"
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
{
|
||||
"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": "مرتب سازی پاسخ ها!"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user