Compare commits

..

1 Commits

Author SHA1 Message Date
adeel.tajamul
ddb7a2c13b fix: switch to use PUBLIC_PATH for routes 2023-05-23 12:10:48 +05:00
114 changed files with 29295 additions and 7232 deletions

5
.env
View File

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

View File

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

View File

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

View File

@@ -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',
},
},
}
}
);

View File

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

View File

@@ -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
View File

@@ -0,0 +1,2 @@
process.env.TA_FEEDBACK_FORM= 'https://learner-form.test';
process.env.STAFF_FEEDBACK_FORM= 'https://staff-form.test';

2
.nvmrc
View File

@@ -1 +1 @@
18
16

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

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

View File

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

View File

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

View File

@@ -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');

View File

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

View File

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

View File

@@ -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();

View File

@@ -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);

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function InsertLink() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function Issue() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function People() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function PushPin() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function Question() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function QuestionAnswer() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function QuestionAnswerOutline() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function StarFilled() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function StarOutline() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function ThumbUpFilled() {
return (
<svg

View File

@@ -1,6 +1,5 @@
import React from 'react';
// eslint-disable-next-line react/function-component-definition
export default function ThumbUpOutline() {
return (
<svg

View File

@@ -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: {

View File

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

View File

@@ -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) => {

View File

@@ -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 = {

View File

@@ -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 });
}
},
);
});
});
});

View File

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

View File

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

View File

@@ -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([
{

View File

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

View File

@@ -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();
});

View File

@@ -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),

View File

@@ -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();
});

View File

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

View File

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

View File

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

View File

@@ -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');

View File

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

View File

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

View File

@@ -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, {

View File

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

View 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;

View 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);
});
});

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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();

View File

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

View File

@@ -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');

View File

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

View File

@@ -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';

View File

@@ -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();

View File

@@ -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]);

View File

@@ -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();

View File

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

View File

@@ -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');
},
);
});
});

View File

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

View File

@@ -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: {

View File

@@ -74,7 +74,6 @@ const PostCommentsView = () => {
}
return (
// eslint-disable-next-line react/jsx-no-constructed-context-values
<PostCommentsContext.Provider value={{
isClosed: closed,
postType: type,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,6 @@ Factory.define('commentsResult')
return Factory.buildList('comment', len, {
thread_id: threadId,
parent_id: parentId,
endorsed,
}, {
endorsedBy: endorsed ? 'staff' : null,
childCount,

View File

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

View File

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

View File

@@ -26,7 +26,6 @@ Factory.define('thread')
'type',
'voted',
'pinned',
'copy_link',
],
author: 'test_user',
author_label: 'Staff',

View File

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

View File

@@ -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';

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
</>
);
};

View File

@@ -39,7 +39,6 @@ const TopicGroupBase = ({
const renderFilteredTopics = useMemo(() => {
if (!hasFilteredSubtopics) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}

View File

@@ -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';

View File

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

View File

@@ -17,7 +17,6 @@ const DiscussionsProductTour = () => {
}, []);
return (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{!isEmpty(config) && (
<ProductTour

View File

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

View File

@@ -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
View 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;

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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