Compare commits

...

30 Commits

Author SHA1 Message Date
Brian Smith
79a2fa404b feat(deps): update header to 5.6.0 (#741) 2024-10-22 19:19:10 -04:00
Brian Smith
472bbe2d96 Revert "test: Remove support for Node 18 (#736)" (#740)
This reverts commit dc5f097736. Node 18 removal PRs should be merged after Sumac is cut.
2024-10-22 13:55:44 -04:00
Bilal Qamar
dc5f097736 test: Remove support for Node 18 (#736) 2024-09-10 14:38:24 +05:00
Bilal Qamar
5e8c8254b4 build: Upgrade to Node 20 (#734)
* feat: updated node to v20

* refactor: updated package-lock along with ci & lockfile version workflows

* refactor: updated lockfile version workflow

* refactor: updated package-lock
2024-09-03 12:21:05 -04:00
Bilal Qamar
0d6692cf8c test: Add Node 20 to CI matrix (#735) 2024-08-22 14:37:56 -04:00
sundasnoreen12
3391e966f3 feat: added help section for post documentation (#733)
* feat: added help section for post documentation

* refactor: refactor code
2024-08-08 18:13:08 +05:00
Bilal Qamar
4297a96102 feat: updated frontend-build & frontend-platform major versions (#626)
* chore: bumped jest to v29

* refactor: updated frontend-build

* refactor: updated package-lock

* feat: updated build and platform major versions, along with edx packages

* refactor: updated package-lock

* refactor: updated package-lock
2024-08-02 16:32:34 +05:00
sundasnoreen12
db883ca7cd feat: added draft functionality for comment and responses (#727)
* feat: added draft functionality for comment and responses

* fix: fixed comment update issue:

* test: added draft test case

* test: added mock conditions for tinymce

* refactor: refactor code

* test: added test cases

* refactor: refactor hook file

* refactor: fixed review issues

* refactor: memoize function

* refactor: refactor code

* test: added update comment test case

* refactor: refactor remove hook method

* test: fixed test cases issue
2024-07-24 17:24:23 +05:00
ayesha waris
422fbf6173 fix: fixed author liking its own post (#720) 2024-06-26 17:13:52 +05:00
Ahtisham Shahid
e862ee6fb1 fix: post editor breaking for moderator (#717)
fix: updated unit tests

fix: updated unit tests
2024-06-21 12:29:27 +05:00
Adolfo R. Brandes
eeae6d45ce build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 12:02:50 -03:00
ayesha waris
71b88bcea3 fix: fixed sidebar inconsistent font size (#716) 2024-06-11 18:25:33 +05:00
Ahtisham Shahid
c808069fe1 Revert "feat: updated course config api version (#702)" (#715)
This reverts commit 8d86e6dcc0.
2024-06-11 15:25:41 +05:00
ayesha waris
b9543c6d9c fix: fixed font-size of load more buttons and add a post heading (#708)
* fix: fixed font-size of load more buttons and add a post heading

* fix: fixed font-size of time stamps

* fix: fixed width of actions dropdown modal
2024-05-28 18:13:03 +05:00
ayesha waris
a545d0b9f6 fix: responsiveness of MFE discussions (#697)
* fix: fixed sidebar xl screen width

* fix: fixed failing test runs

* refactor: removed unused css classes
2024-05-27 16:48:53 +05:00
Ahtisham Shahid
8d86e6dcc0 feat: updated course config api version (#702) 2024-05-24 05:38:30 -04:00
sundasnoreen12
37781566f5 fix: fix redirection to new tab issue (#704) 2024-05-22 12:59:45 +05:00
Brian Smith
50948acfeb feat: import FooterSlot from frontend-slot-footer package 2024-05-17 09:39:00 -03:00
Awais Ansari
4de1011780 Revert "feat: use frontend-plugin-framework to provide a FooterSlot" (#700)
This reverts commit d7474782b4.
2024-05-13 18:16:56 +05:00
Brian Smith
d7474782b4 feat: use frontend-plugin-framework to provide a FooterSlot 2024-05-10 11:21:26 -03:00
ayesha waris
e1c78dda6e fix: fixed limited content visibility due to oversized sidebar (#692)
* fix: fixed limited content visibility due to oversized sidebar

* refactor: changed names of constants

---------

Co-authored-by: Awais Ansari <79941147+awais-ansari@users.noreply.github.com>
2024-04-08 23:43:24 +05:00
ayesha waris
f282da52c1 fix: fixed commentsInThreads[s] is not iterable (#693) 2024-04-08 22:48:27 +05:00
sundasnoreen12
d7fcc86847 fix: fixed api calling issues for admin (#691)
* fix: fixed api calling issues for admin

* test: fixed test case

* refactor: fixed review issue

---------

Co-authored-by: Awais Ansari <79941147+awais-ansari@users.noreply.github.com>
2024-04-05 16:48:36 +05:00
Danyal Faheem
c0873df575 fix: render favicon and siteName in title (#668)
* fix: render favicon and siteName in title

* fix: remove translation files
2024-04-05 16:09:53 +05:00
Stanislav
12fbe7eebd fix: Discussions UI fixes on mobile resolutions (#689) 2024-04-04 17:46:01 +05:00
sundasnoreen12
7db4fde252 feat: restricted unnecessary api calls (#683)
* feat: restricted unnecessary api calls

* fix: fixed content unavailable issue for user admin

* refactor: refactor code for course status
2024-04-04 12:42:37 +05:00
sundasnoreen12
4914f51b6e fix: fixed crash issue for post in learner tab (#684) 2024-03-28 13:30:50 +05:00
ayesha waris
80073e3f83 feat: updated redux structure using updated comments api (#670)
* feat: updated redux structure and commentsview component

* test: fixed test cases

* fix: fixed lint error
2024-03-25 15:09:22 +05:00
sundasnoreen12
3aacdda7a1 feat: remove Transifex calls for OEP-58 for discussion MFE (#682) 2024-03-21 00:19:56 +05:00
Brian Smith
1a2068d52f chore(deps): update paragon and frontend-build to openedx scope 2024-02-28 12:55:13 -03:00
120 changed files with 10687 additions and 15542 deletions

View File

@@ -2,3 +2,4 @@ coverage/*
dist/
node_modules/
jest.config.js
src/i18n/messages/

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig(
'eslint',

View File

@@ -9,17 +9,19 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm ci
- name: Validate package-lock.json changes
@@ -33,4 +35,7 @@ jobs:
- name: i18n_extract
run: npm run i18n_extract
- name: Coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

2
.nvmrc
View File

@@ -1 +1 @@
18
20

View File

@@ -1,8 +0,0 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-discussions]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -1,7 +1,3 @@
export TRANSIFEX_RESOURCE = frontend-app-discussions
transifex_resource = frontend-app-discussions
transifex_langs = "ar,cs,de_DE,es_419,es_AR,es_ES,fa_IR,fr,fr_CA,fr_FR,hi,it_IT,pl,pt_PT,tr_TR,uk,ru,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
@@ -56,12 +52,6 @@ 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
@@ -74,9 +64,9 @@ pull_translations:
translations/frontend-app-discussions/src/i18n/messages:frontend-app-discussions
$(intl_imports) frontend-component-header frontend-component-footer frontend-platform paragon frontend-app-discussions
endif
# endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...
git diff --exit-code package-lock.json
git diff --exit-code package-lock.json

View File

@@ -52,6 +52,12 @@ Cloning and Startup
The dev server is running at `http://localhost:2002 <http://localhost:2002>`_.
Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Getting Help
============
Please tag **@openedx/edx-infinity ** on any PRs or issues. Thanks.

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('jest', {
// setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want.

20734
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,11 +34,11 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "12.6.1",
"@edx/frontend-component-header": "4.10.1",
"@edx/frontend-platform": "5.6.1",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.0.0",
"@edx/openedx-atlas": "^0.6.0",
"@edx/paragon": "20.46.3",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.1.1",
"@reduxjs/toolkit": "1.9.7",
"@tinymce/tinymce-react": "3.13.1",
"babel-polyfill": "6.26.0",
@@ -51,6 +51,7 @@
"raw-loader": "4.0.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.18.0",
"react-router-dom": "6.18.0",
@@ -62,17 +63,18 @@
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-build": "13.0.14",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "14.0.3",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "13.5.0",
"axios": "^0.28.0",
"axios-mock-adapter": "1.22.0",
"babel-plugin-react-intl": "8.2.25",
"eslint-plugin-simple-import-sort": "7.0.0",
"glob": "7.2.0",
"husky": "7.0.4",
"jest": "27.5.1",
"jest": "29.7.0",
"rosie": "2.1.1"
}
}

View File

@@ -1,14 +1,14 @@
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import {
Collapsible, Form, Icon, Spinner,
} from '@openedx/paragon';
import { Tune } from '@openedx/paragon/icons';
import { capitalize, toString } from 'lodash';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Collapsible, Form, Icon, Spinner,
} from '@edx/paragon';
import { Tune } from '@edx/paragon/icons';
import {
PostsStatusFilter, RequestStatus,

View File

@@ -1,10 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form, TransitionReplace } from '@openedx/paragon';
import { getIn, useFormikContext } from 'formik';
import { Form, TransitionReplace } from '@edx/paragon';
const FormikErrorFeedback = ({ name }) => {
const { touched, errors } = useFormikContext();
const fieldTouched = getIn(touched, name);

View File

@@ -9,7 +9,7 @@ import { useDebounce } from '../discussions/data/hooks';
const defaultSanitizeOptions = {
USE_PROFILES: { html: true },
ADD_ATTR: ['columnalign'],
ADD_ATTR: ['columnalign', 'target'],
};
const HTMLLoader = ({

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
const Head = ({ intl }) => (
<Helmet>
<title>
{intl.formatMessage(messages['discussions.page.title'], { siteName: getConfig().SITE_NAME })}
</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
);
Head.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(Head);

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { render } from '@testing-library/react';
import { Helmet } from 'react-helmet';
import { getConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import Head from './Head';
describe('Head', () => {
const props = {};
it('should match render title tag and favicon with the site configuration values', () => {
render(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
const helmet = Helmet.peek();
expect(helmet.title).toEqual(`Discussions | ${getConfig().SITE_NAME}`);
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
});
});

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'discussions.page.title': {
id: 'discussions.page.title',
defaultMessage: 'Discussions | {siteName}',
description: 'Title tag',
},
});
export default messages;

View File

@@ -1,6 +1,6 @@
@import "~@edx/brand/paragon/fonts.scss";
@import "~@edx/brand/paragon/variables.scss";
@import "~@edx/paragon/scss/core/core.scss";
@import "~@openedx/paragon/scss/core/core.scss";
@import "~@edx/brand/paragon/overrides.scss";
$fa-font-path: "~font-awesome/fonts";

View File

@@ -1,10 +1,10 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Dropdown } from '@openedx/paragon';
import classNames from 'classnames';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon';
import useIndexOfLastVisibleChild from './useIndexOfLastVisibleChild';

View File

@@ -1,6 +1,6 @@
import { useLayoutEffect, useRef, useState } from 'react';
import { useWindowSize } from '@edx/paragon';
import { useWindowSize } from '@openedx/paragon';
const invisibleStyle = {
position: 'absolute',

View File

@@ -0,0 +1,74 @@
import React, { useState } from 'react';
import {
Hyperlink, Icon, IconButton, IconButtonWithTooltip,
} from '@openedx/paragon';
import { Close, HelpOutline } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../discussions/posts/post-editor/messages';
const PostHelpPanel = () => {
const intl = useIntl();
const [showHelpPane, setShowHelpPane] = useState(false);
return (
<>
<div className="d-flex justify-content-end">
<IconButtonWithTooltip
onClick={() => setShowHelpPane(true)}
alt={intl.formatMessage(messages.showHelpIcon)}
tooltipContent={<div>{intl.formatMessage(messages.discussionHelpTooltip)}</div>}
src={HelpOutline}
iconAs={Icon}
size="inline"
className="float-right p-3 help-icon"
iconClassNames="help-icon-size"
data-testid="help-button"
invertColors
isActive
/>
</div>
{showHelpPane && (
<div
className="w-100 p-2 bg-light-200 rounded box-shadow-down-1 post-preview overflow-auto my-3"
style={{ minHeight: '200px', wordBreak: 'break-word' }}
>
<IconButton
onClick={() => setShowHelpPane(false)}
alt={intl.formatMessage(messages.actionsAlt)}
src={Close}
iconAs={Icon}
size="inline"
className="float-right p-3"
iconClassNames="icon-size"
data-testid="hide-help-button"
/>
<div className="pt-2 px-3">
<h4 className="font-weight-bold">{intl.formatMessage(messages.discussionHelpHeader)}</h4>
<p className="pt-2">{intl.formatMessage(messages.discussionHelpDescription)}</p>
<Hyperlink
target="_blank"
className="w-100"
destination="https://support.edx.org/hc/en-us/sections/115004169687-Participating-in-Course-Discussions"
showLaunchIcon={false}
>
{intl.formatMessage(messages.discussionHelpCourseParticipation)}
</Hyperlink>
<Hyperlink
target="_blank"
className="w-100"
destination="https://support.edx.org/hc/en-us/articles/360000035267-Entering-math-expressions-in-course-discussions"
showLaunchIcon={false}
>
{intl.formatMessage(messages.discussionHelpMathExpressions)}
</Hyperlink>
</div>
</div>
)}
</>
);
};
export default React.memo(PostHelpPanel);

View File

@@ -1,9 +1,10 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Icon, IconButton } from '@openedx/paragon';
import { Close } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Icon, IconButton } from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import messages from '../discussions/posts/post-editor/messages';
import HTMLLoader from './HTMLLoader';

View File

@@ -2,12 +2,12 @@ import React, {
useCallback, useContext, useEffect, useRef, useState,
} from 'react';
import { Icon, SearchField } from '@openedx/paragon';
import { Search as SearchIcon } from '@openedx/paragon/icons';
import camelCase from 'lodash/camelCase';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, SearchField } from '@edx/paragon';
import { Search as SearchIcon } from '@edx/paragon/icons';
import DiscussionContext from '../discussions/common/context';
import { setUsernameSearch } from '../discussions/learners/data';

View File

@@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Icon } from '@openedx/paragon';
import { Search } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Icon } from '@edx/paragon';
import { Search } from '@edx/paragon/icons';
import { RequestStatus } from '../data/constants';
import messages from '../discussions/posts/post-actions-bar/messages';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Spinner as ParagonSpinner } from '@edx/paragon';
import { Spinner as ParagonSpinner } from '@openedx/paragon';
const Spinner = () => (
<div className="spinner-container" data-testid="spinner">

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { ActionRow, AlertModal, Button } from '@openedx/paragon';
import { Editor } from '@tinymce/tinymce-react';
import { useLocation, useParams } from 'react-router-dom';
// TinyMCE so the global var exists
@@ -7,7 +8,6 @@ import { useLocation, useParams } from 'react-router-dom';
import tinymce from 'tinymce/tinymce';
import { useIntl } from '@edx/frontend-platform/i18n';
import { ActionRow, AlertModal, Button } from '@edx/paragon';
import { MAX_UPLOAD_FILE_SIZE } from '../data/constants';
import messages from '../discussions/messages';

View File

@@ -2,11 +2,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Icon, OverlayTrigger, Tooltip } from '@openedx/paragon';
import { HelpOutline, PostOutline, Report } from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons';
import {
selectUserHasModerationPrivileges,

View File

@@ -3,14 +3,14 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import {
Button, Dropdown, Icon, IconButton, ModalPopup, useToggle,
} from '@openedx/paragon';
import { MoreHoriz } from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';
import {
Button, Dropdown, Icon, IconButton, ModalPopup, useToggle,
} from '@edx/paragon';
import { MoreHoriz } from '@edx/paragon/icons';
import { ContentActions } from '../../data/constants';
import { selectIsPostingEnabled } from '../data/selectors';
@@ -79,7 +79,7 @@ const ActionsDropdown = ({
placement="bottom-end"
>
<div
className="bg-white shadow d-flex flex-column"
className="bg-white shadow d-flex flex-column mt-1"
data-testid="actions-dropdown-modal-popup"
>
{actions.map(action => (
@@ -94,12 +94,13 @@ const ActionsDropdown = ({
handleActions(action.action);
}}
className="d-flex justify-content-start actions-dropdown-item"
data-testId={action.id}
>
<Icon
src={action.icon}
className="icon-size-24"
/>
<span className="font-weight-normal font-xl ml-2">
<span className="font-weight-normal ml-2">
{intl.formatMessage(action.label)}
</span>
</Dropdown.Item>

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import { Report } from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { Report } from '@edx/paragon/icons';
import { AvatarOutlineAndLabelColors } from '../../data/constants';
import {

View File

@@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import messages from '../post-comments/messages';
import AuthorLabel from './AuthorLabel';

View File

@@ -1,12 +1,12 @@
import React, { useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Icon, OverlayTrigger, Tooltip } from '@openedx/paragon';
import classNames from 'classnames';
import { generatePath, Link } from 'react-router-dom';
import * as timeago from 'timeago.js';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
import { Routes } from '../../data/constants';
import messages from '../messages';
@@ -38,7 +38,7 @@ const AuthorLabel = ({
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-style font-weight-500 author-name', {
'text-gray-700': isRetiredUser,
'text-primary-500': !authorLabelMessage && !isRetiredUser,
})}
@@ -71,7 +71,7 @@ const AuthorLabel = ({
/>
{authorLabelMessage && (
<span
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
className={classNames('mr-1.5 font-style font-weight-500', {
'text-primary-500': showTextPrimary,
'text-gray-700': isRetiredUser,
})}
@@ -85,7 +85,7 @@ const AuthorLabel = ({
{postCreatedAt && (
<span
title={postCreatedAt}
className={classNames('align-content-center', {
className={classNames('align-content-center post-summary-timestamp', {
'text-white': alert,
'text-gray-500': !alert,
})}
@@ -99,7 +99,7 @@ const AuthorLabel = ({
return showUserNameAsLink
? (
<div className={className}>
<div className={`${className} flex-wrap`}>
<Link
data-testid="learner-posts-link"
id="learner-posts-link"
@@ -112,7 +112,7 @@ const AuthorLabel = ({
{labelContents}
</div>
)
: <div className={className}>{authorName}{labelContents}</div>;
: <div className={`${className} flex-wrap`}>{authorName}{labelContents}</div>;
};
AuthorLabel.propTypes = {

View File

@@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ActionRow, Button, ModalDialog } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { ActionRow, Button, ModalDialog } from '@edx/paragon';
import messages from '../messages';

View File

@@ -1,11 +1,11 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { Alert, Icon } from '@openedx/paragon';
import { CheckCircle, Verified } from '@openedx/paragon/icons';
import * as timeago from 'timeago.js';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert, Icon } from '@edx/paragon';
import { CheckCircle, Verified } from '@edx/paragon/icons';
import { ThreadType } from '../../data/constants';
import messages from '../post-comments/messages';

View File

@@ -1,18 +1,18 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import {
Button, Icon, IconButton, OverlayTrigger, Tooltip,
} from '@openedx/paragon';
import {
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
} from '@openedx/paragon/icons';
import classNames from 'classnames';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button, Icon, IconButton, OverlayTrigger, Tooltip,
} from '@edx/paragon';
import {
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
} from '@edx/paragon/icons';
import { ThreadType } from '../../data/constants';
import { useUserPostingEnabled } from '../data/hooks';
import { useHasLikePermission, useUserPostingEnabled } from '../data/hooks';
import PostCommentsContext from '../post-comments/postCommentsContext';
import ActionsDropdown from './ActionsDropdown';
import DiscussionContext from './context';
@@ -33,6 +33,7 @@ const HoverCard = ({
const { enableInContextSidebar } = useContext(DiscussionContext);
const { isClosed } = useContext(PostCommentsContext);
const isUserPrivilegedInPostingRestriction = useUserPostingEnabled();
const userHasLikePermission = useHasLikePermission(contentType, id);
return (
<div
@@ -45,7 +46,7 @@ const HoverCard = ({
<Button
variant="tertiary"
className={classNames(
'px-2.5 py-2 border-0 font-style text-gray-700 font-size-12',
'px-2.5 py-2 border-0 font-style text-gray-700',
{ 'w-100': enableInContextSidebar },
)}
onClick={() => handleResponseCommentButton()}
@@ -86,6 +87,7 @@ const HoverCard = ({
iconAs={Icon}
size="sm"
alt="Like"
disabled={!userHasLikePermission}
iconClassNames="like-icon-dimensions"
onClick={(e) => {
e.preventDefault();

View File

@@ -1,21 +1,21 @@
import React, { useCallback } from 'react';
import propTypes from 'prop-types';
import { Button } from '@openedx/paragon';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import ContentUnavailableIcon from '../../assets/ContentUnavailable';
import selectCourseTabs from '../../components/NavigationBar/data/selectors';
import { useIsOnDesktop, useIsOnXLDesktop } from '../data/hooks';
import { useIsOnTablet, useIsOnXLDesktop } from '../data/hooks';
import messages from '../messages';
const ContentUnavailable = ({ subTitleMessage }) => {
const intl = useIntl();
const isOnDesktop = useIsOnDesktop();
const isOnTabletorDesktop = useIsOnTablet();
const isOnXLDesktop = useIsOnXLDesktop();
const { courseId } = useSelector(selectCourseTabs);
@@ -26,8 +26,8 @@ const ContentUnavailable = ({ subTitleMessage }) => {
return (
<div className="min-content-height justify-content-center align-items-center d-flex w-100 flex-column bg-white">
<div className={classNames('d-flex flex-column align-items-center', {
'content-unavailable-desktop': isOnDesktop || isOnXLDesktop,
'py-0 px-3': !isOnDesktop && !isOnXLDesktop,
'content-unavailable-desktop': isOnTabletorDesktop || isOnXLDesktop,
'py-0 px-3': !isOnTabletorDesktop && !isOnXLDesktop,
})}
>
<ContentUnavailableIcon />
@@ -35,7 +35,7 @@ const ContentUnavailable = ({ subTitleMessage }) => {
{intl.formatMessage(messages.contentUnavailableTitle)}
</h3>
<p className="pb-2 text-gray-500 text-center">{intl.formatMessage(subTitleMessage)}</p>
<Button onClick={redirectToDashboard} variant="outline-dark" className="font-size-14 py-2 px-2.5">
<Button onClick={redirectToDashboard} variant="outline-dark" className="py-2 px-2.5">
{intl.formatMessage(messages.contentUnavailableAction)}
</Button>
</div>

View File

@@ -5,8 +5,10 @@ ensureConfig([
'LMS_BASE_URL',
], 'Posts API service');
export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v2/courses/`;
export const getCourseSettingsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
export const getDiscussionsConfigUrl = (courseId) => `${getCourseConfigApiUrl()}${courseId}/`;
export const getDiscussionsSettingsUrl = (courseId) => `${getCourseSettingsApiUrl()}${courseId}/settings`;
/**
* Get discussions course config
* @param {string} courseId
@@ -21,7 +23,7 @@ export async function getDiscussionsConfig(courseId) {
* @param {string} courseId
*/
export async function getDiscussionsSettings(courseId) {
const url = `${getDiscussionsConfigUrl(courseId)}settings`;
const url = `${getDiscussionsSettingsUrl(courseId)}`;
const { data } = await getAuthenticatedHttpClient().get(url);
return data;
}

View File

@@ -3,6 +3,7 @@ import {
useContext, useEffect, useMemo, useRef, useState,
} from 'react';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux';
import {
matchPath, useLocation, useMatch, useNavigate,
@@ -11,13 +12,15 @@ import {
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { breakpoints, useWindowSize } from '@edx/paragon';
import selectCourseTabs from '../../components/NavigationBar/data/selectors';
import { LOADED } from '../../components/NavigationBar/data/slice';
import fetchTab from '../../components/NavigationBar/data/thunks';
import { RequestStatus, Routes } from '../../data/constants';
import { ContentActions, RequestStatus, Routes } from '../../data/constants';
import { selectTopicsUnderCategory } from '../../data/selectors';
import fetchCourseBlocks from '../../data/thunks';
import DiscussionContext from '../common/context';
import PostCommentsContext from '../post-comments/postCommentsContext';
import { clearRedirect } from '../posts/data';
import { threadsLoadingStatus } from '../posts/data/selectors';
import { selectTopics } from '../topics/data/selectors';
@@ -25,13 +28,15 @@ 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 { checkPermissions, discussionsPath } from '../utils';
import { ContentSelectors } from './constants';
import {
selectAreThreadsFiltered,
selectEnableInContext,
selectIsCourseAdmin,
selectIsCourseStaff,
selectIsPostingEnabled,
selectIsUserLearner,
selectPostThreadCount,
selectUserHasModerationPrivileges,
selectUserIsGroupTa,
@@ -74,12 +79,10 @@ export const useSidebarVisible = () => {
export function useCourseDiscussionData(courseId) {
const dispatch = useDispatch();
const { authenticatedUser } = useContext(AppContext);
useEffect(() => {
async function fetchBaseData() {
await dispatch(fetchCourseConfig(courseId));
await dispatch(fetchCourseBlocks(courseId, authenticatedUser.username));
await dispatch(fetchTab(courseId));
}
@@ -87,6 +90,23 @@ export function useCourseDiscussionData(courseId) {
}, [courseId]);
}
export function useCourseBlockData(courseId) {
const dispatch = useDispatch();
const { authenticatedUser } = useContext(AppContext);
const { isEnrolled, courseStatus } = useSelector(selectCourseTabs);
const isUserLearner = useSelector(selectIsUserLearner);
useEffect(() => {
async function fetchBaseData() {
if (courseStatus === LOADED && (!isUserLearner || isEnrolled)) {
await dispatch(fetchCourseBlocks(courseId, authenticatedUser.username));
}
}
fetchBaseData();
}, [courseId, isEnrolled, courseStatus, isUserLearner]);
}
export function useRedirectToThread(courseId, enableInContextSidebar) {
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -113,12 +133,17 @@ export function useRedirectToThread(courseId, enableInContextSidebar) {
export function useIsOnDesktop() {
const windowSize = useWindowSize();
return windowSize.width >= breakpoints.medium.minWidth;
return windowSize.width >= breakpoints.medium.maxWidth;
}
export function useIsOnTablet() {
const windowSize = useWindowSize();
return windowSize.width >= breakpoints.small.maxWidth;
}
export function useIsOnXLDesktop() {
const windowSize = useWindowSize();
return windowSize.width >= breakpoints.extraLarge.minWidth;
return windowSize.width >= breakpoints.extraLarge.maxWidth;
}
/**
@@ -261,3 +286,10 @@ export const useDebounce = (value, delay) => {
);
return debouncedValue;
};
export const useHasLikePermission = (contentType, id) => {
const { postType } = useContext(PostCommentsContext);
const content = { ...useSelector(ContentSelectors[contentType](id)), postType };
return checkPermissions(content, ContentActions.VOTE);
};

View File

@@ -1,6 +1,8 @@
import { createSelector } from '@reduxjs/toolkit';
import selectCourseTabs from '../../components/NavigationBar/data/selectors';
import { PostsStatusFilter, ThreadType } from '../../data/constants';
import { isCourseStatusValid } from '../utils';
export const selectAnonymousPostingConfig = state => ({
allowAnonymous: state.config.allowAnonymous,
@@ -69,12 +71,14 @@ export const selectIsUserLearner = createSelector(
selectUserIsStaff,
selectIsCourseAdmin,
selectIsCourseStaff,
selectCourseTabs,
(
userHasModerationPrivileges,
userIsGroupTa,
userIsStaff,
userIsCourseAdmin,
userIsCourseStaff,
{ courseStatus },
) => (
(
!userHasModerationPrivileges
@@ -82,6 +86,7 @@ export const selectIsUserLearner = createSelector(
&& !userIsStaff
&& !userIsCourseAdmin
&& !userIsCourseStaff
&& isCourseStatusValid(courseStatus)
) || false
),
);

View File

@@ -3,18 +3,19 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import { useWindowSize } from '@openedx/paragon';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import {
Navigate, Route, Routes,
} from 'react-router-dom';
import { useWindowSize } from '@edx/paragon';
import Spinner from '../../components/Spinner';
import { RequestStatus, Routes as ROUTES } from '../../data/constants';
import DiscussionContext from '../common/context';
import { useContainerSize, useIsOnDesktop, useIsOnXLDesktop } from '../data/hooks';
import {
useContainerSize, useIsOnDesktop, useIsOnTablet, useIsOnXLDesktop,
} from '../data/hooks';
import { selectConfigLoadingStatus, selectEnableInContext } from '../data/selectors';
const TopicPostsView = lazy(() => import('../in-context-topics/TopicPostsView'));
@@ -27,6 +28,7 @@ const LegacyTopicsView = lazy(() => import('../topics/TopicsView'));
const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => {
const isOnDesktop = useIsOnDesktop();
const isOnXLDesktop = useIsOnXLDesktop();
const isOnTablet = useIsOnTablet();
const { enableInContextSidebar } = useContext(DiscussionContext);
const enableInContext = useSelector(selectEnableInContext);
const configStatus = useSelector(selectConfigLoadingStatus);
@@ -52,6 +54,7 @@ const DiscussionSidebar = ({ displaySidebar, postActionBarRef }) => {
'd-flex overflow-auto box-shadow-centered-1': displaySidebar,
'w-100': !isOnDesktop,
'sidebar-desktop-width': isOnDesktop && !isOnXLDesktop,
'sidebar-tablet-width': isOnTablet && !isOnDesktop,
'w-25 sidebar-XL-width': isOnXLDesktop,
'min-content-height': !enableInContextSidebar,
})}

View File

@@ -12,12 +12,11 @@ import { LearningHeader as Header } from '@edx/frontend-component-header';
import { Spinner } from '../../components';
import selectCourseTabs from '../../components/NavigationBar/data/selectors';
import { LOADED } from '../../components/NavigationBar/data/slice';
import { ALL_ROUTES, DiscussionProvider, Routes as ROUTES } from '../../data/constants';
import DiscussionContext from '../common/context';
import ContentUnavailable from '../content-unavailable/ContentUnavailable';
import {
useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useSidebarVisible,
useCourseBlockData, useCourseDiscussionData, useIsOnTablet, useRedirectToThread, useSidebarVisible,
} from '../data/hooks';
import { selectDiscussionProvider, selectEnableInContext, selectIsUserLearner } from '../data/selectors';
import { EmptyLearners, EmptyTopics } from '../empty-posts';
@@ -25,9 +24,10 @@ import EmptyPosts from '../empty-posts/EmptyPosts';
import { EmptyTopic as InContextEmptyTopics } from '../in-context-topics/components';
import messages from '../messages';
import { selectPostEditorVisible } from '../posts/data/selectors';
import { isCourseStatusValid } from '../utils';
import useFeedbackWrapper from './FeedbackWrapper';
const Footer = lazy(() => import('@edx/frontend-component-footer'));
const FooterSlot = lazy(() => import('@openedx/frontend-slot-footer'));
const PostActionsBar = lazy(() => import('../posts/post-actions-bar/PostActionsBar'));
const CourseTabsNavigation = lazy(() => import('../../components/NavigationBar/CourseTabsNavigation'));
const LegacyBreadcrumbMenu = lazy(() => import('../navigation/breadcrumb-menu/LegacyBreadcrumbMenu'));
@@ -51,7 +51,7 @@ const DiscussionsHome = () => {
const page = pageParams?.page || null;
const matchPattern = ALL_ROUTES.find((route) => matchPath({ path: route }, location.pathname));
const { params } = useMatch(matchPattern);
const isOnDesktop = useIsOnDesktop();
const isOnTabletorDesktop = useIsOnTablet();
let displaySidebar = useSidebarVisible();
const enableInContextSidebar = Boolean(new URLSearchParams(location.search).get('inContextSidebar') !== null);
const {
@@ -60,12 +60,13 @@ const DiscussionsHome = () => {
useCourseDiscussionData(courseId);
useRedirectToThread(courseId, enableInContextSidebar);
useCourseBlockData(courseId);
useFeedbackWrapper();
/* Display the content area if we are currently viewing/editing a post or creating one.
If the window is larger than a particular size, show the sidebar for navigating between posts/topics.
However, for smaller screens or embeds, only show the sidebar if the content area isn't displayed. */
const displayContentArea = (postId || postEditorVisible || (learnerUsername && postId));
if (displayContentArea) { displaySidebar = isOnDesktop; }
if (displayContentArea) { displaySidebar = isOnTabletorDesktop; }
const discussionContextValue = useMemo(() => ({
page,
@@ -81,7 +82,7 @@ const DiscussionsHome = () => {
<Suspense fallback={(<Spinner />)}>
<DiscussionContext.Provider value={discussionContextValue}>
{!enableInContextSidebar && (<Header courseOrg={org} courseNumber={courseNumber} courseTitle={courseTitle} />)}
<main className="container-fluid d-flex flex-column p-0 w-100" id="main" tabIndex="-1">
<main className="container-fluid d-flex flex-column p-0 w-100 font-size" id="main" tabIndex="-1">
{!enableInContextSidebar && <CourseTabsNavigation />}
{(isEnrolled || !isUserLearner) && (
<div
@@ -120,7 +121,7 @@ const DiscussionsHome = () => {
</Routes>
</Suspense>
)}
{(courseStatus === LOADED) && (
{isCourseStatusValid(courseStatus) && (
!isEnrolled && isUserLearner ? (
<Suspense fallback={(<Spinner />)}>
<Routes>
@@ -172,9 +173,9 @@ const DiscussionsHome = () => {
</div>
)
)}
{!enableInContextSidebar && (<DiscussionsProductTour />)}
{!enableInContextSidebar && isEnrolled && (<DiscussionsProductTour />)}
</main>
{!enableInContextSidebar && <Footer />}
{!enableInContextSidebar && <FooterSlot />}
</DiscussionContext.Provider>
</Suspense>
);

View File

@@ -230,7 +230,7 @@ describe('DiscussionsHome', () => {
it('should display post editor form when click on add a post button in legacy topics view', async () => {
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
enable_in_context: false,
enable_in_context: false, hasModerationPrivileges: true,
});
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
await renderComponent(`/${courseId}/topics`);

View File

@@ -1,9 +1,9 @@
import React, { useCallback, useState } from 'react';
import { PageBanner } from '@openedx/paragon';
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';

View File

@@ -2,15 +2,15 @@ import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useIsOnDesktop } from '../data/hooks';
import { useIsOnTablet } from '../data/hooks';
import messages from '../messages';
import EmptyPage from './EmptyPage';
const EmptyLearners = () => {
const intl = useIntl();
const isOnDesktop = useIsOnDesktop();
const isOnTabletorDesktop = useIsOnTablet();
if (!isOnDesktop) {
if (!isOnTabletorDesktop) {
return null;
}

View File

@@ -1,10 +1,9 @@
import React from 'react';
import propTypes from 'prop-types';
import { Button } from '@openedx/paragon';
import classNames from 'classnames';
import { Button } from '@edx/paragon';
import EmptyIcon from '../../assets/Empty';
const EmptyPage = ({

View File

@@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useIsOnDesktop } from '../data/hooks';
import { useIsOnTablet } from '../data/hooks';
import { selectAreThreadsFiltered, selectPostThreadCount } from '../data/selectors';
import messages from '../messages';
import { showPostEditor } from '../posts/data';
@@ -15,7 +15,7 @@ import EmptyPage from './EmptyPage';
const EmptyPosts = ({ subTitleMessage }) => {
const intl = useIntl();
const dispatch = useDispatch();
const isOnDesktop = useIsOnDesktop();
const isOnTabletorDesktop = useIsOnTablet();
const isFiltered = useSelector(selectAreThreadsFiltered);
const totalThreads = useSelector(selectPostThreadCount);
@@ -31,7 +31,7 @@ const EmptyPosts = ({ subTitleMessage }) => {
const isEmpty = [0, null].includes(totalThreads) && !isFiltered;
if (!(isOnDesktop || isEmpty)) {
if (!(isOnTabletorDesktop || isEmpty)) {
return null;
} if (isEmpty) {
subTitle = subTitleMessage;

View File

@@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useIsOnDesktop, useTotalTopicThreadCount } from '../data/hooks';
import { useIsOnTablet, useTotalTopicThreadCount } from '../data/hooks';
import { selectTopicThreadCount } from '../data/selectors';
import messages from '../messages';
import { showPostEditor } from '../posts/data';
@@ -16,7 +16,7 @@ const EmptyTopics = () => {
const intl = useIntl();
const { topicId } = useParams();
const dispatch = useDispatch();
const isOnDesktop = useIsOnDesktop();
const isOnTabletorDesktop = useIsOnTablet();
const hasGlobalThreads = useTotalTopicThreadCount() > 0;
const topicThreadCount = useSelector(selectTopicThreadCount(topicId));
@@ -30,7 +30,7 @@ const EmptyTopics = () => {
let action;
let actionText;
if (!isOnDesktop) {
if (!isOnTabletorDesktop) {
return null;
}

View File

@@ -2,11 +2,11 @@ import React, {
useCallback, useContext, useEffect, useMemo,
} from 'react';
import { Spinner } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Spinner } from '@edx/paragon';
import { RequestStatus, Routes } from '../../data/constants';
import DiscussionContext from '../common/context';

View File

@@ -2,12 +2,11 @@ import React, {
useCallback, useContext, useEffect, useMemo,
} from 'react';
import { Spinner } from '@openedx/paragon';
import classNames from 'classnames';
import isEmpty from 'lodash/isEmpty';
import { useDispatch, useSelector } from 'react-redux';
import { Spinner } from '@edx/paragon';
import SearchInfo from '../../components/SearchInfo';
import { RequestStatus } from '../../data/constants';
import DiscussionContext from '../common/context';

View File

@@ -151,12 +151,14 @@ describe('InContext Topics View', () => {
const sectionGroups = await screen.getAllByTestId('section-group');
coursewareTopics.forEach(async (topic, index) => {
const stats = await sectionGroups[index].querySelectorAll('.icon-size:not([data-testid="subsection-group"].icon-size)');
const subsectionGroups = await within(sectionGroups[index]).getAllByTestId('subsection-group');
await waitFor(async () => {
const stats = await sectionGroups[index].querySelectorAll('.icon-size:not([data-testid="subsection-group"].icon-size)');
const subsectionGroups = await within(sectionGroups[index]).getAllByTestId('subsection-group');
expect(within(sectionGroups[index]).queryByText(topic.displayName)).toBeInTheDocument();
expect(stats).toHaveLength(0);
expect(subsectionGroups).toHaveLength(2);
expect(within(sectionGroups[index]).queryByText(topic.displayName)).toBeInTheDocument();
expect(stats).toHaveLength(0);
expect(subsectionGroups).toHaveLength(2);
});
});
});

View File

@@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Icon, IconButton, Spinner } from '@openedx/paragon';
import { ArrowBack } from '@openedx/paragon/icons';
import { useNavigate } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon, IconButton, Spinner } from '@edx/paragon';
import { ArrowBack } from '@edx/paragon/icons';
import messages from '../messages';

View File

@@ -6,7 +6,7 @@ import { useParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import DiscussionContext from '../../common/context';
import { useIsOnDesktop } from '../../data/hooks';
import { useIsOnTablet } from '../../data/hooks';
import { selectPostThreadCount } from '../../data/selectors';
import EmptyPage from '../../empty-posts/EmptyPage';
import messages from '../../messages';
@@ -17,7 +17,7 @@ const EmptyTopics = () => {
const intl = useIntl();
const { category, topicId } = useParams();
const dispatch = useDispatch();
const isOnDesktop = useIsOnDesktop();
const isOnTabletorDesktop = useIsOnTablet();
const { enableInContextSidebar } = useContext(DiscussionContext);
const courseWareThreadsCount = useSelector(selectCourseWareThreadsCount(category));
const topicThreadsCount = useSelector(selectPostThreadCount);
@@ -34,7 +34,7 @@ const EmptyTopics = () => {
let action;
let actionText;
if (!isOnDesktop) {
if (!isOnTabletorDesktop) {
return null;
}

View File

@@ -1,10 +1,10 @@
import React, { useCallback, useContext, useEffect } from 'react';
import { Icon, SearchField } from '@openedx/paragon';
import { Search as SearchIcon } from '@openedx/paragon/icons';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, SearchField } from '@edx/paragon';
import { Search as SearchIcon } from '@edx/paragon/icons';
import DiscussionContext from '../../common/context';
import postsMessages from '../../posts/post-actions-bar/messages';

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { SearchField } from '@openedx/paragon';
import { useDispatch } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { SearchField } from '@edx/paragon';
import { setFilter } from '../data';
import messages from '../messages';

View File

@@ -2,15 +2,15 @@ import React, {
useCallback, useContext, useEffect, useMemo,
} from 'react';
import {
Button, Icon, IconButton, Spinner,
} from '@openedx/paragon';
import { ArrowBack } from '@openedx/paragon/icons';
import capitalize from 'lodash/capitalize';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useNavigate } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button, Icon, IconButton, Spinner,
} from '@edx/paragon';
import { ArrowBack } from '@edx/paragon/icons';
import {
RequestStatus,

View File

@@ -1,10 +1,10 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import { Button, Spinner } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Spinner } from '@edx/paragon';
import SearchInfo from '../../components/SearchInfo';
import { RequestStatus } from '../../data/constants';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Avatar } from '@edx/paragon';
import { Avatar } from '@openedx/paragon';
const LearnerAvatar = ({ username }) => (
<div className="mr-3 mt-1">

View File

@@ -37,7 +37,7 @@ const LearnerCard = ({ learner }) => {
<div className="d-flex flex-column justify-content-start mw-100 flex-fill">
<div className="d-flex align-items-center flex-fill">
<div
className="text-truncate font-weight-500 font-size-14 text-primary-500 font-style"
className="text-truncate font-weight-500 text-primary-500 font-style"
>
{username}
</div>

View File

@@ -1,13 +1,13 @@
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { Collapsible, Form, Icon } from '@openedx/paragon';
import { Check, Tune } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Collapsible, Form, Icon } from '@edx/paragon';
import { Check, Tune } from '@edx/paragon/icons';
import { LearnersOrdering } from '../../../data/constants';
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';

View File

@@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Icon, OverlayTrigger, Tooltip } from '@openedx/paragon';
import {
Edit, QuestionAnswerOutline, Report, ReportGmailerrorred,
} from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
import {
Edit, QuestionAnswerOutline, Report, ReportGmailerrorred,
} from '@edx/paragon/icons';
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
import messages from '../messages';

View File

@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownButton } from '@openedx/paragon';
import { Link } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, DropdownButton } from '@edx/paragon';
import messages from './messages';

View File

@@ -1,9 +1,9 @@
import React, { useContext, useMemo } from 'react';
import { Nav } from '@openedx/paragon';
import { matchPath, NavLink, useLocation } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Nav } from '@edx/paragon';
import { Routes } from '../../../data/constants';
import DiscussionContext from '../../common/context';

View File

@@ -2,19 +2,17 @@ import React, {
Suspense, useCallback, useContext, useEffect, useMemo, useState,
} from 'react';
import { Button, Icon, IconButton } from '@openedx/paragon';
import { ArrowBack } from '@openedx/paragon/icons';
import { useLocation, useNavigate } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Icon, IconButton } from '@edx/paragon';
import { ArrowBack } from '@edx/paragon/icons';
import Spinner from '../../components/Spinner';
import {
EndorsementStatus, PostsPages, ThreadType,
} from '../../data/constants';
import { PostsPages } from '../../data/constants';
import useDispatchWithState from '../../data/hooks';
import DiscussionContext from '../common/context';
import { useIsOnDesktop } from '../data/hooks';
import { useIsOnTablet } from '../data/hooks';
import { EmptyPage } from '../empty-posts';
import { Post } from '../posts';
import { fetchThread } from '../posts/data/thunks';
@@ -31,7 +29,7 @@ const PostCommentsView = () => {
const intl = useIntl();
const navigate = useNavigate();
const location = useLocation();
const isOnDesktop = useIsOnDesktop();
const isOnTabletorDesktop = useIsOnTablet();
const [addingResponse, setAddingResponse] = useState(false);
const [isLoading, submitDispatch] = useDispatchWithState();
const {
@@ -86,7 +84,7 @@ const PostCommentsView = () => {
return (
<PostCommentsContext.Provider value={postCommentsContextValue}>
{!isOnDesktop && (
{!isOnTabletorDesktop && (
enableInContextSidebar ? (
<>
<div className="px-4 py-1.5 bg-white">
@@ -127,15 +125,7 @@ const PostCommentsView = () => {
</div>
<Suspense fallback={(<Spinner />)}>
{!!commentsCount && <CommentsSort />}
{type === ThreadType.DISCUSSION && (
<CommentsView endorsed={EndorsementStatus.DISCUSSION} />
)}
{type === ThreadType.QUESTION && (
<>
<CommentsView endorsed={EndorsementStatus.ENDORSED} />
<CommentsView endorsed={EndorsementStatus.UNENDORSED} />
</>
)}
<CommentsView threadType={type} />
</Suspense>
</PostCommentsContext.Provider>
);

View File

@@ -12,13 +12,13 @@ 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 { getApiBaseUrl, ThreadType } from '../../data/constants';
import { initializeStore } from '../../store';
import executeThunk from '../../test-utils';
import { getCohortsApiUrl } from '../cohorts/data/api';
import fetchCourseCohorts from '../cohorts/data/thunks';
import DiscussionContext from '../common/context';
import { getCourseConfigApiUrl } from '../data/api';
import { getCourseConfigApiUrl, getCourseSettingsApiUrl } from '../data/api';
import fetchCourseConfig from '../data/thunks';
import DiscussionContent from '../discussions-home/DiscussionContent';
import { getThreadsApiUrl } from '../posts/data/api';
@@ -37,6 +37,7 @@ import '../topics/data/__factories__';
import '../cohorts/data/__factories__';
const courseConfigApiUrl = getCourseConfigApiUrl();
const courseSettingsApiUrl = getCourseSettingsApiUrl();
const commentsApiUrl = getCommentsApiUrl();
const threadsApiUrl = getThreadsApiUrl();
const discussionPostId = 'thread-1';
@@ -50,10 +51,10 @@ let testLocation;
let container;
let unmount;
async function mockAxiosReturnPagedComments(threadId, endorsed = false, page = 1, count = 2) {
async function mockAxiosReturnPagedComments(threadId, threadType = ThreadType.DISCUSSION, page = 1, count = 2) {
axiosMock.onGet(commentsApiUrl).reply(200, Factory.build('commentsResult', { can_delete: true }, {
threadId,
endorsed,
threadType,
pageSize: 1,
count,
childCount: page === 1 ? 2 : 0,
@@ -76,6 +77,7 @@ async function mockAxiosReturnPagedCommentsResponses() {
Factory.build('commentsResult', null, {
threadId: discussionPostId,
parentId,
endorsed: false,
page,
pageSize: 1,
count: 2,
@@ -104,7 +106,7 @@ async function setupCourseConfig() {
{ code: 'reason-2', label: 'reason 2' },
],
});
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
axiosMock.onGet(`${courseSettingsApiUrl}${courseId}/settings`).reply(200, {});
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
}
@@ -201,6 +203,7 @@ describe('ThreadView', () => {
id: commentId,
rendered_body: rawBody,
raw_body: rawBody,
endorsed: false,
})];
});
axiosMock.onPost(commentsApiUrl).reply(({ data }) => {
@@ -209,6 +212,7 @@ describe('ThreadView', () => {
rendered_body: rawBody,
raw_body: rawBody,
thread_id: threadId,
endorsed: false,
})];
});
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { isPostingEnabled: true });
@@ -230,9 +234,9 @@ 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 () => {
it('should not allow posting a reply on a closed post', async () => {
axiosMock.reset();
await mockAxiosReturnPagedComments(closedPostId, true);
await mockAxiosReturnPagedComments(closedPostId, ThreadType.QUESTION);
await waitFor(() => renderComponent(closedPostId, true));
const comments = await waitFor(() => screen.findAllByTestId('comment-comment-4'));
const hoverCard = within(comments[0]).getByTestId('hover-card-comment-4');
@@ -288,7 +292,7 @@ describe('ThreadView', () => {
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
});
it('should allow posting a response', async () => {
it('should allow posting a comment', async () => {
await waitFor(() => renderComponent(discussionPostId));
const post = await screen.findByTestId('post-thread-1');
@@ -540,8 +544,11 @@ describe('ThreadView', () => {
// Wait for the content to load
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
const endorseButton = await waitFor(() => within(hoverCard).getByRole('button', { name: /Endorse/i }));
await act(async () => {
fireEvent.click(within(hoverCard).getByRole('button', { name: /Endorse/i }));
fireEvent.click(endorseButton);
});
expect(axiosMock.history.patch).toHaveLength(2);
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
@@ -591,7 +598,7 @@ describe('ThreadView', () => {
it('pressing load more button will load next page of comments', async () => {
await waitFor(() => renderComponent(discussionPostId));
await mockAxiosReturnPagedComments(discussionPostId, false, 2);
await mockAxiosReturnPagedComments(discussionPostId, ThreadType.DISCUSSION, 2);
const loadMoreButton = await findLoadMoreCommentsButton();
await act(async () => {
@@ -604,7 +611,7 @@ describe('ThreadView', () => {
it('newly loaded comments are appended to the old ones', async () => {
await waitFor(() => renderComponent(discussionPostId));
await mockAxiosReturnPagedComments(discussionPostId, false, 2);
await mockAxiosReturnPagedComments(discussionPostId, ThreadType.DISCUSSION, 2);
const loadMoreButton = await findLoadMoreCommentsButton();
await act(async () => {
@@ -622,7 +629,7 @@ describe('ThreadView', () => {
const findLoadMoreCommentsButtons = () => screen.findByTestId('load-more-comments');
it('initially loads only the first page', async () => {
await mockAxiosReturnPagedComments(questionPostId);
await mockAxiosReturnPagedComments(questionPostId, ThreadType.QUESTION);
act(() => renderComponent(questionPostId));
expect(await screen.findByTestId('comment-comment-4'))
@@ -633,7 +640,7 @@ describe('ThreadView', () => {
});
it('pressing load more button will load next page of comments', async () => {
await mockAxiosReturnPagedComments(questionPostId);
await mockAxiosReturnPagedComments(questionPostId, ThreadType.QUESTION);
await waitFor(() => renderComponent(questionPostId));
const loadMoreButton = await findLoadMoreCommentsButtons();
@@ -644,7 +651,7 @@ describe('ThreadView', () => {
expect(await screen.queryByTestId('comment-comment-5'))
.not
.toBeInTheDocument();
await mockAxiosReturnPagedComments(questionPostId, false, 2, 1);
await mockAxiosReturnPagedComments(questionPostId, ThreadType.QUESTION, 2, 1);
await act(async () => {
fireEvent.click(loadMoreButton);
});
@@ -664,7 +671,152 @@ describe('ThreadView', () => {
expect(screen.queryByTestId('reply-comment-3')).not.toBeInTheDocument();
});
it('pressing load more button will load next page of responses', async () => {
it('successfully added comment in the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));
await act(async () => {
fireEvent.click(screen.queryByText('Add comment'));
});
await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment!' } });
});
await act(async () => {
fireEvent.click(screen.queryByText('Cancel'));
});
await act(async () => {
fireEvent.click(screen.queryByText('Add comment'));
});
expect(screen.queryByText('Draft comment!')).toBeInTheDocument();
});
it('successfully updated comment in the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));
const comment = screen.queryByTestId('reply-comment-2');
const actionBtn = comment.querySelector('button[aria-label="Actions menu"]');
await act(async () => {
fireEvent.click(actionBtn);
});
await act(async () => {
fireEvent.click(screen.queryByTestId('edit'));
});
await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment!' } });
});
await act(async () => {
fireEvent.click(screen.queryByText('Cancel'));
});
await act(async () => {
fireEvent.click(actionBtn);
});
await act(async () => {
fireEvent.click(screen.queryByTestId('edit'));
});
await act(async () => {
fireEvent.click(screen.queryByText('Submit'));
});
await waitFor(() => expect(screen.queryByText('Draft comment!')).toBeInTheDocument());
});
it('successfully removed comment from the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));
await act(async () => {
fireEvent.click(screen.queryByText('Add comment'));
});
await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment 123!' } });
});
await act(async () => {
fireEvent.click(screen.queryByText('Submit'));
});
await act(async () => {
fireEvent.click(screen.queryAllByText('Add comment')[0]);
});
expect(screen.queryByTestId('tinymce-editor').value).toBe('');
});
it('successfully added response in the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));
await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});
await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft Response!' } });
});
await act(async () => {
fireEvent.click(screen.queryByText('Cancel'));
});
await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});
expect(screen.queryByText('Draft Response!')).toBeInTheDocument();
});
it('successfully removed response from the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));
await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});
await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft Response!' } });
});
await act(async () => {
fireEvent.click(screen.queryByText('Submit'));
});
await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});
expect(screen.queryByTestId('tinymce-editor').value).toBe('');
});
it('successfully maintain response for the specific post in the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));
await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});
await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Hello, world!' } });
});
await waitFor(() => renderComponent('thread-2'));
await act(async () => {
fireEvent.click(screen.queryAllByText('Add response')[0]);
});
expect(screen.queryByText('Hello, world!')).toBeInTheDocument();
});
it('pressing load more button will load next page of replies', async () => {
await waitFor(() => renderComponent(discussionPostId));
const loadMoreButton = await findLoadMoreCommentsResponsesButton();
@@ -674,7 +826,7 @@ describe('ThreadView', () => {
await screen.findByTestId('reply-comment-3');
});
it('newly loaded responses are appended to the old ones', async () => {
it('newly loaded replies are appended to the old ones', async () => {
await waitFor(() => renderComponent(discussionPostId));
const loadMoreButton = await findLoadMoreCommentsResponsesButton();
@@ -687,7 +839,7 @@ describe('ThreadView', () => {
expect(screen.queryByTestId('reply-comment-2')).toBeInTheDocument();
});
it('load more button is hidden when no more responses pages to load', async () => {
it('load more button is hidden when no more replies pages to load', async () => {
await waitFor(() => renderComponent(discussionPostId));
const loadMoreButton = await findLoadMoreCommentsResponsesButton();

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useEffect, useState } from 'react';
import {
Button, Dropdown, ModalPopup, useToggle,
} from '@openedx/paragon';
import { ExpandLess, ExpandMore } from '@openedx/paragon/icons';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button, Dropdown, ModalPopup, useToggle,
} from '@edx/paragon';
import { ExpandLess, ExpandMore } from '@edx/paragon/icons';
import { updateUserDiscussionsTourByName } from '../../tours/data';
import { selectCommentSortOrder } from '../data/selectors';

View File

@@ -1,10 +1,11 @@
import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Spinner } from '@edx/paragon';
import { Button, Spinner } from '@openedx/paragon';
import { EndorsementStatus } from '../../../data/constants';
import { useIntl } from '@edx/frontend-platform/i18n';
import { ThreadType } from '../../../data/constants';
import { useUserPostingEnabled } from '../../data/hooks';
import { isLastElementOfList } from '../../utils';
import { usePostComments } from '../data/hooks';
@@ -12,7 +13,7 @@ import messages from '../messages';
import PostCommentsContext from '../postCommentsContext';
import { Comment, ResponseEditor } from './comment';
const CommentsView = ({ endorsed }) => {
const CommentsView = ({ threadType }) => {
const intl = useIntl();
const [addingResponse, setAddingResponse] = useState(false);
const { isClosed } = useContext(PostCommentsContext);
@@ -24,7 +25,7 @@ const CommentsView = ({ endorsed }) => {
hasMorePages,
isLoading,
handleLoadMoreResponses,
} = usePostComments(endorsed);
} = usePostComments(threadType);
const handleAddResponse = useCallback(() => {
setAddingResponse(true);
@@ -44,7 +45,7 @@ const CommentsView = ({ endorsed }) => {
</div>
), []);
const handleComments = useCallback((postCommentsIds, showLoadMoreResponses = false) => (
const handleComments = useCallback((postCommentsIds) => (
<div className="mx-4" role="list">
{postCommentsIds.map((commentId) => (
<Comment
@@ -53,72 +54,66 @@ const CommentsView = ({ endorsed }) => {
marginBottom={isLastElementOfList(postCommentsIds, commentId)}
/>
))}
{hasMorePages && !isLoading && !showLoadMoreResponses && (
<Button
onClick={handleLoadMoreResponses}
variant="link"
block="true"
className="px-4 mt-3 border-0 line-height-24 py-0 mb-2 font-style font-weight-500 font-size-14"
data-testid="load-more-comments"
>
{intl.formatMessage(messages.loadMoreResponses)}
</Button>
)}
{isLoading && !showLoadMoreResponses && (
<div className="mb-2 mt-3 d-flex justify-content-center">
<Spinner animation="border" variant="primary" className="spinner-dimensions" />
</div>
)}
</div>
), [hasMorePages, isLoading, handleLoadMoreResponses]);
return (
((hasMorePages && isLoading) || !isLoading) && (
<>
{endorsedCommentsIds.length > 0 && (
<>
{endorsedCommentsIds.length > 0 && (
<>
{handleDefinition(messages.endorsedResponseCount, endorsedCommentsIds.length)}
{endorsed === EndorsementStatus.DISCUSSION
? handleComments(endorsedCommentsIds, true)
: handleComments(endorsedCommentsIds, false)}
</>
)}
{endorsed !== EndorsementStatus.ENDORSED && (
<>
{handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)}
{unEndorsedCommentsIds.length === 0 && <br />}
{handleComments(unEndorsedCommentsIds, false)}
{(isUserPrivilegedInPostingRestriction && !!unEndorsedCommentsIds.length && !isClosed) && (
<div className="mx-4">
{!addingResponse && (
<Button
variant="plain"
block="true"
className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
line-height-24 font-size-14 text-primary-500"
onClick={handleAddResponse}
data-testid="add-response"
>
{intl.formatMessage(messages.addResponse)}
</Button>
)}
<ResponseEditor
addWrappingDiv
addingResponse={addingResponse}
handleCloseEditor={handleCloseResponseEditor}
/>
</div>
)}
</>
)}
{handleDefinition(messages.endorsedResponseCount, endorsedCommentsIds.length)}
{handleComments(endorsedCommentsIds)}
</>
)}
{handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)}
{unEndorsedCommentsIds.length > 0 && handleComments(unEndorsedCommentsIds)}
{hasMorePages && !isLoading && (!!unEndorsedCommentsIds.length || !!endorsedCommentsIds.length) && (
<Button
onClick={handleLoadMoreResponses}
variant="link"
block="true"
className="px-4 mt-3 border-0 line-height-24 py-0 mb-2 font-style font-weight-500"
data-testid="load-more-comments"
>
{intl.formatMessage(messages.loadMoreResponses)}
</Button>
)}
{isLoading && (
<div className="mb-2 mt-3 d-flex justify-content-center">
<Spinner animation="border" variant="primary" className="spinner-dimensions" />
</div>
)}
{(isUserPrivilegedInPostingRestriction && (!!unEndorsedCommentsIds.length || !!endorsedCommentsIds.length)
&& !isClosed) && (
<div className="mx-4">
{!addingResponse && (
<Button
variant="plain"
block="true"
className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
line-height-24 text-primary-500"
onClick={handleAddResponse}
data-testid="add-response"
>
{intl.formatMessage(messages.addResponse)}
</Button>
)}
<ResponseEditor
addWrappingDiv
addingResponse={addingResponse}
handleCloseEditor={handleCloseResponseEditor}
/>
</div>
)}
</>
)
);
};
CommentsView.propTypes = {
endorsed: PropTypes.oneOf([
EndorsementStatus.ENDORSED, EndorsementStatus.UNENDORSED, EndorsementStatus.DISCUSSION,
threadType: PropTypes.oneOf([
ThreadType.DISCUSSION, ThreadType.QUESTION,
]).isRequired,
};

View File

@@ -3,11 +3,11 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import { Button, useToggle } from '@openedx/paragon';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../../components/HTMLLoader';
import { ContentActions, EndorsementStatus } from '../../../../data/constants';
@@ -82,7 +82,7 @@ const Comment = ({
}, []);
const handleCommentEndorse = useCallback(async () => {
await dispatch(editComment(id, { endorsed: !endorsed }, ContentActions.ENDORSE));
await dispatch(editComment(id, { endorsed: !endorsed }));
await dispatch(fetchThread(threadId, courseId));
}, [id, endorsed, threadId]);
@@ -104,6 +104,10 @@ const Comment = ({
hideReportConfirmation();
}, [abuseFlagged, id, hideReportConfirmation]);
const handleCommentLike = useCallback(async () => {
await dispatch(editComment(id, { voted: !voted }));
}, [id, voted]);
const actionHandlers = useMemo(() => ({
[ContentActions.EDIT_CONTENT]: handleEditContent,
[ContentActions.ENDORSE]: handleCommentEndorse,
@@ -124,10 +128,6 @@ const Comment = ({
}
}, [isUserPrivilegedInPostingRestriction]);
const handleCommentLike = useCallback(async () => {
await dispatch(editComment(id, { voted: !voted }));
}, [id, voted]);
const handleCloseEditor = useCallback(() => {
setEditing(false);
}, []);
@@ -249,7 +249,7 @@ const Comment = ({
onClick={handleLoadMoreComments}
variant="link"
block="true"
className="font-size-14 line-height-24 font-style pt-10px border-0 font-weight-500 pb-0"
className="line-height-24 font-style pt-10px border-0 font-weight-500 pb-0"
data-testid="load-more-comments-responses"
>
{intl.formatMessage(messages.loadMoreComments)}
@@ -267,7 +267,7 @@ const Comment = ({
) : (
!isClosed && isUserPrivilegedInPostingRestriction && (inlineReplies.length >= 5) && (
<Button
className="d-flex flex-grow mt-2 font-size-14 font-style font-weight-500 text-primary-500 add-comment-btn rounded-0"
className="d-flex flex-grow mt-2 font-style font-weight-500 text-primary-500 add-comment-btn rounded-0"
variant="plain"
style={{ height: '36px' }}
onClick={handleAddCommentReply}

View File

@@ -1,15 +1,15 @@
import React, {
useCallback, useContext, useEffect, useRef,
useCallback, useContext, useEffect, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';
import { Button, Form, StatefulButton } from '@openedx/paragon';
import { Formik } from 'formik';
import { useSelector } from 'react-redux';
import * as Yup from 'yup';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button, Form, StatefulButton } from '@edx/paragon';
import { TinyMCEEditor } from '../../../../components';
import FormikErrorFeedback from '../../../../components/FormikErrorFeedback';
@@ -22,7 +22,9 @@ import {
selectUserIsGroupTa,
selectUserIsStaff,
} from '../../../data/selectors';
import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
import { extractContent, formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
import { useDraftContent } from '../../data/hooks';
import { setDraftComments, setDraftResponses } from '../../data/slices';
import { addComment, editComment } from '../../data/thunks';
import messages from '../../messages';
@@ -45,6 +47,8 @@ const CommentEditor = ({
const userIsStaff = useSelector(selectUserIsStaff);
const { editReasons } = useSelector(selectModerationSettings);
const [submitting, dispatch] = useDispatchWithState();
const [editorContent, setEditorContent] = useState();
const { addDraftContent, getDraftContent, removeDraftContent } = useDraftContent();
const canDisplayEditReason = (edit
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
@@ -62,7 +66,7 @@ const CommentEditor = ({
});
const initialValues = {
comment: rawBody,
comment: editorContent,
editReasonCode: lastEdit?.reasonCode || (userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined),
};
@@ -71,6 +75,15 @@ const CommentEditor = ({
onCloseEditor();
}, [onCloseEditor, initialValues]);
const deleteEditorContent = useCallback(async () => {
const { updatedResponses, updatedComments } = removeDraftContent(parentId, id, threadId);
if (parentId) {
await dispatch(setDraftComments(updatedComments));
} else {
await dispatch(setDraftResponses(updatedResponses));
}
}, [parentId, id, threadId, setDraftComments, setDraftResponses]);
const saveUpdatedComment = useCallback(async (values, { resetForm }) => {
if (id) {
const payload = {
@@ -86,6 +99,7 @@ const CommentEditor = ({
editorRef.current.plugins.autosave.removeDraft();
}
handleCloseEditor(resetForm);
deleteEditorContent();
}, [id, threadId, parentId, enableInContextSidebar, handleCloseEditor]);
// The editorId is used to autosave contents to localstorage. This format means that the autosave is scoped to
// the current comment id, or the current comment parent or the curren thread.
@@ -97,11 +111,33 @@ const CommentEditor = ({
}
}, [formRef]);
useEffect(() => {
const draftHtml = getDraftContent(parentId, threadId, id) || rawBody;
setEditorContent(draftHtml);
}, [parentId, threadId, id]);
const saveDraftContent = async (content) => {
const draftDataContent = extractContent(content);
const { updatedResponses, updatedComments } = addDraftContent(
draftDataContent,
parentId,
id,
threadId,
);
if (parentId) {
await dispatch(setDraftComments(updatedComments));
} else {
await dispatch(setDraftResponses(updatedResponses));
}
};
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={saveUpdatedComment}
enableReinitialize
>
{({
values,
@@ -151,7 +187,10 @@ const CommentEditor = ({
id={editorId}
value={values.comment}
onEditorChange={formikCompatibleHandler(handleChange, 'comment')}
onBlur={formikCompatibleHandler(handleBlur, 'comment')}
onBlur={(content) => {
formikCompatibleHandler(handleChange, 'comment');
saveDraftContent(content);
}}
/>
{isFormikFieldInvalid('comment', {
errors,

View File

@@ -1,10 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Avatar } from '@openedx/paragon';
import classNames from 'classnames';
import { Avatar } from '@edx/paragon';
import { AvatarOutlineAndLabelColors } from '../../../../data/constants';
import { AuthorLabel } from '../../../common';
import { useAlertBannerVisible } from '../../../data/hooks';

View File

@@ -1,11 +1,11 @@
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Avatar, useToggle } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux';
import * as timeago from 'timeago.js';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Avatar, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../../components/HTMLLoader';
import { AvatarOutlineAndLabelColors, ContentActions } from '../../../../data/constants';
@@ -54,7 +54,7 @@ const Reply = ({ responseId }) => {
}, []);
const handleReplyEndorse = useCallback(() => {
dispatch(editComment(id, { endorsed: !endorsed }, ContentActions.ENDORSE));
dispatch(editComment(id, { endorsed: !endorsed }));
}, [endorsed, id]);
const handleAbusedFlag = useCallback(() => {
@@ -131,7 +131,7 @@ const Reply = ({ responseId }) => {
className="bg-light-300 pl-4 pt-2.5 pr-2.5 pb-10px flex-fill"
style={{ borderRadius: '0rem 0.375rem 0.375rem' }}
>
<div className="d-flex flex-row justify-content-between" style={{ height: '24px' }}>
<div className="d-flex flex-row justify-content-between">
<AuthorLabel
author={author}
authorLabel={authorLabel}

View File

@@ -6,7 +6,7 @@ Factory.define('comment')
.sequence('rendered_body', ['endorsed'], (idx, endorsed) => `Some contents for <b>${endorsed ? 'endorsed ' : 'unendorsed '}comment number ${idx}</b>.`)
.attr('thread_id', null, 'test-thread')
.option('endorsedBy', null, null)
.attr('endorsed', ['endorsedBy'], (endorsedBy) => !!endorsedBy)
.attr('endorsed', ['endorsed'], (endorsed) => endorsed)
.attr('endorsed_by', ['endorsedBy'], (endorsedBy) => endorsedBy)
.attr('endorsed_by_label', ['endorsedBy'], (endorsedBy) => (endorsedBy ? 'Staff' : null))
.attr('endorsed_at', ['endorsedBy'], (endorsedBy) => (endorsedBy ? (new Date()).toISOString() : null))
@@ -38,7 +38,7 @@ Factory.define('commentsResult')
.option('pageSize', null, 5)
.option('threadId', null, 'test-thread')
.option('parentId', null, null)
.option('endorsed', null, null)
.option('endorsed', false, false)
.option('childCount', null, 0)
.attr('pagination', ['threadId', 'count', 'page', 'pageSize'], (threadId, count, page, pageSize) => {
const numPages = Math.ceil(count / pageSize);

View File

@@ -1,7 +1,7 @@
import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { EndorsementValue } from '../../../data/constants';
import { ThreadType } from '../../../data/constants';
ensureConfig([
'LMS_BASE_URL',
@@ -20,7 +20,7 @@ export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussi
* @returns {Promise<{}>}
*/
export const getThreadComments = async (threadId, {
endorsed,
threadType,
page,
pageSize,
reverseOrder,
@@ -29,12 +29,12 @@ export const getThreadComments = async (threadId, {
} = {}) => {
const params = snakeCaseObject({
threadId,
endorsed: EndorsementValue[endorsed],
page,
pageSize,
reverseOrder,
requestedFields: 'profile_image',
enableInContextSidebar,
mergeQuestionTypeResponses: threadType === ThreadType.QUESTION ? true : null,
});
const { data } = await getAuthenticatedHttpClient().get(getCommentsApiUrl(), { params: { ...params, signal } });

View File

@@ -3,6 +3,7 @@ import {
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { v4 as uuidv4 } from 'uuid';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -13,7 +14,8 @@ import { selectThread } from '../../posts/data/selectors';
import { markThreadAsRead } from '../../posts/data/thunks';
import { filterPosts } from '../../utils';
import {
selectCommentSortOrder, selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages,
selectCommentSortOrder, selectDraftComments, selectDraftResponses,
selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages,
} from './selectors';
import { fetchThreadComments } from './thunks';
@@ -40,13 +42,13 @@ export function usePost(postId) {
return thread || {};
}
export function usePostComments(endorsed = null) {
export function usePostComments(threadType) {
const { enableInContextSidebar, postId } = useContext(DiscussionContext);
const [isLoading, dispatch] = useDispatchWithState();
const comments = useSelector(selectThreadComments(postId, endorsed));
const comments = useSelector(selectThreadComments(postId));
const reverseOrder = useSelector(selectCommentSortOrder);
const hasMorePages = useSelector(selectThreadHasMorePages(postId, endorsed));
const currentPage = useSelector(selectThreadCurrentPage(postId, endorsed));
const hasMorePages = useSelector(selectThreadHasMorePages(postId));
const currentPage = useSelector(selectThreadCurrentPage(postId));
const endorsedCommentsIds = useMemo(() => (
[...filterPosts(comments, 'endorsed')].map(comment => comment.id)
@@ -58,19 +60,19 @@ export function usePostComments(endorsed = null) {
const handleLoadMoreResponses = useCallback(async () => {
const params = {
endorsed,
threadType,
page: currentPage + 1,
reverseOrder,
};
await dispatch(fetchThreadComments(postId, params));
trackLoadMoreEvent(postId, params);
}, [currentPage, endorsed, postId, reverseOrder]);
}, [currentPage, threadType, postId, reverseOrder]);
useEffect(() => {
const abortController = new AbortController();
dispatch(fetchThreadComments(postId, {
endorsed,
threadType,
page: 1,
reverseOrder,
enableInContextSidebar,
@@ -80,7 +82,7 @@ export function usePostComments(endorsed = null) {
return () => {
abortController.abort();
};
}, [postId, endorsed, reverseOrder, enableInContextSidebar]);
}, [postId, threadType, reverseOrder, enableInContextSidebar]);
return {
endorsedCommentsIds,
@@ -102,3 +104,73 @@ export function useCommentsCount(postId) {
return commentsLength;
}
export const useDraftContent = () => {
const comments = useSelector(selectDraftComments);
const responses = useSelector(selectDraftResponses);
const getObjectByParentId = (data, parentId, isComment, id) => Object.values(data)
.find(draft => (isComment ? draft.parentId === parentId && (id ? draft.id === id : draft.isNewContent === true)
: draft.threadId === parentId && (id ? draft.id === id : draft.isNewContent === true)));
const updateDraftData = (draftData, newDraftObject) => ({
...draftData,
[newDraftObject.id]: newDraftObject,
});
const addDraftContent = (content, parentId, id, threadId) => {
const data = parentId ? comments : responses;
const draftParentId = parentId || threadId;
const isComment = !!parentId;
const existingObj = getObjectByParentId(data, draftParentId, isComment, id);
const newObject = existingObj
? { ...existingObj, content }
: {
threadId,
content,
parentId,
id: id || uuidv4(),
isNewContent: !id,
};
const updatedComments = parentId ? updateDraftData(comments, newObject) : comments;
const updatedResponses = !parentId ? updateDraftData(responses, newObject) : responses;
return { updatedComments, updatedResponses };
};
const getDraftContent = (parentId, threadId, id) => {
if (id) {
return parentId ? comments?.[id]?.content : responses?.[id]?.content;
}
const data = parentId ? comments : responses;
const draftParentId = parentId || threadId;
const isComment = !!parentId;
return getObjectByParentId(data, draftParentId, isComment, id)?.content;
};
const removeItem = (draftData, objId) => {
const { [objId]: _, ...newDraftData } = draftData;
return newDraftData;
};
const updateContent = (items, itemId, parentId, isComment) => {
const itemObj = itemId ? items[itemId] : getObjectByParentId(items, parentId, isComment, itemId);
return itemObj ? removeItem(items, itemObj.id) : items;
};
const removeDraftContent = (parentId, id, threadId) => {
const updatedResponses = !parentId ? updateContent(responses, id, threadId, false) : responses;
const updatedComments = parentId ? updateContent(comments, id, parentId, true) : comments;
return { updatedResponses, updatedComments };
};
return {
addDraftContent,
getDraftContent,
removeDraftContent,
};
};

View File

@@ -4,7 +4,7 @@ import { Factory } from 'rosie';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeMockApp } from '@edx/frontend-platform/testing';
import { EndorsementStatus } from '../../../data/constants';
import { ThreadType } from '../../../data/constants';
import { initializeStore } from '../../../store';
import executeThunk from '../../../test-utils';
import { getCommentsApiUrl } from './api';
@@ -39,37 +39,23 @@ describe('Comments/Responses data layer tests', () => {
});
test.each([
{
threadType: 'discussion',
endorsed: EndorsementStatus.DISCUSSION,
},
{
threadType: 'question',
endorsed: EndorsementStatus.UNENDORSED,
},
{
threadType: 'question',
endorsed: EndorsementStatus.ENDORSED,
},
])('successfully processes comments for \'$threadType\' thread with endorsed=$endorsed', async ({
endorsed,
}) => {
ThreadType.DISCUSSION,
ThreadType.QUESTION,
])('successfully processes comments for %s type thread', async (threadType) => {
const threadId = 'test-thread';
axiosMock.onGet(commentsApiUrl)
.reply(200, Factory.build('commentsResult'));
await executeThunk(fetchThreadComments(threadId, { endorsed }), store.dispatch, store.getState);
await executeThunk(fetchThreadComments(threadId, { threadType }), store.dispatch, store.getState);
expect(store.getState().comments.commentsInThreads)
.toEqual({ 'test-thread': { [endorsed]: ['comment-1', 'comment-2', 'comment-3'] } });
.toEqual({ 'test-thread': ['comment-1', 'comment-2', 'comment-3'] });
expect(store.getState().comments.pagination)
.toEqual({
'test-thread': {
[endorsed]: {
currentPage: 1,
totalPages: 1,
hasMorePages: false,
},
currentPage: 1,
totalPages: 1,
hasMorePages: false,
},
});
expect(Object.keys(store.getState().comments.commentsById))
@@ -82,7 +68,7 @@ describe('Comments/Responses data layer tests', () => {
.toEqual('test-thread');
});
test('successfully processes comment responses', async () => {
test('successfully processes comment replies', async () => {
const threadId = 'test-thread';
const commentId = 'comment-1';
axiosMock.onGet(commentsApiUrl)
@@ -101,7 +87,7 @@ describe('Comments/Responses data layer tests', () => {
.toEqual({ 'comment-1': ['comment-4', 'comment-5', 'comment-6'] });
});
test('successfully handles response creation for discussion type threads', async () => {
test('successfully handles comment creation for threads', async () => {
const threadId = 'test-thread';
const content = 'Test comment';
axiosMock.onGet(commentsApiUrl)
@@ -119,21 +105,19 @@ describe('Comments/Responses data layer tests', () => {
await executeThunk(addComment(content, threadId, null), store.dispatch, store.getState);
expect(store.getState().comments.commentsInThreads[threadId])
.toEqual({
[EndorsementStatus.DISCUSSION]: [
'comment-1',
'comment-2',
'comment-3',
'comment-4',
],
});
.toEqual([
'comment-1',
'comment-2',
'comment-3',
'comment-4',
]);
expect(Object.keys(store.getState().comments.commentsById))
.toEqual(['comment-1', 'comment-2', 'comment-3', 'comment-4']);
expect(store.getState().comments.commentsById['comment-4'].threadId)
.toEqual(threadId);
});
test('successfully handles reply creation for discussion type threads', async () => {
test('successfully handles reply creation for threads', async () => {
const threadId = 'test-thread';
const parentId = 'comment-1';
const content = 'Test comment';
@@ -156,13 +140,11 @@ describe('Comments/Responses data layer tests', () => {
await executeThunk(addComment(content, threadId, null), store.dispatch, store.getState);
expect(store.getState().comments.commentsInThreads[threadId])
.toEqual({
[EndorsementStatus.DISCUSSION]: [
'comment-1',
'comment-2',
'comment-3',
],
});
.toEqual([
'comment-1',
'comment-2',
'comment-3',
]);
expect(Object.keys(store.getState().comments.commentsById))
.toEqual(['comment-1', 'comment-2', 'comment-3', 'comment-4']);
expect(store.getState().comments.commentsInComments[parentId])
@@ -173,54 +155,6 @@ describe('Comments/Responses data layer tests', () => {
.toEqual(parentId);
});
test('successfully handles comment creation for question type threads', async () => {
const threadId = 'test-thread';
const content = 'Test comment';
axiosMock.onGet(commentsApiUrl)
.reply(200, Factory.build('commentsResult', null, { endorsed: false }));
await executeThunk(
fetchThreadComments(threadId, { endorsed: EndorsementStatus.UNENDORSED }),
store.dispatch,
store.getState,
);
axiosMock.onGet(commentsApiUrl)
.reply(200, Factory.build('commentsResult', null, { endorsed: true }));
await executeThunk(
fetchThreadComments(threadId, { endorsed: EndorsementStatus.ENDORSED }),
store.dispatch,
store.getState,
);
axiosMock.onPost(commentsApiUrl)
.reply(200, Factory.build('comment', {
thread_id: threadId,
raw_body: content,
rendered_body: content,
}));
await executeThunk(addComment(content, threadId, null), store.dispatch, store.getState);
expect(store.getState().comments.commentsInThreads[threadId])
.toEqual({
[EndorsementStatus.UNENDORSED]: [
'comment-1',
'comment-2',
'comment-3',
// Newly-added comment
'comment-7',
],
[EndorsementStatus.ENDORSED]: [
'comment-4',
'comment-5',
'comment-6',
],
});
expect(Object.keys(store.getState().comments.commentsById))
.toEqual(['comment-1', 'comment-2', 'comment-3', 'comment-4', 'comment-5', 'comment-6', 'comment-7']);
expect(store.getState().comments.commentsById['comment-7'].threadId)
.toEqual(threadId);
});
test('successfully handles comment edits', async () => {
const threadId = 'test-thread';
const commentId = 'comment-1';
@@ -271,7 +205,7 @@ describe('Comments/Responses data layer tests', () => {
.toContain(commentId);
});
test('correctly handles comment responses pagination after posting a new response', async () => {
test('correctly handles comment replies pagination after posting a new reply', async () => {
const threadId = 'test-thread';
const commentId = 'comment-1';
@@ -327,15 +261,9 @@ describe('Comments/Responses data layer tests', () => {
});
test.each([
{
threadType: 'discussion',
endorsed: EndorsementStatus.DISCUSSION,
},
{
threadType: 'unendorsed',
endorsed: EndorsementStatus.UNENDORSED,
},
])('correctly handles `$threadType` thread comments pagination after posting a new comment', async ({ endorsed }) => {
ThreadType.DISCUSSION,
ThreadType.QUESTION,
])('correctly handles %s thread comments pagination after posting a new comment', async (threadType) => {
const threadId = 'test-thread';
// Build all comments first, so we can paginate over them and they
@@ -348,7 +276,7 @@ describe('Comments/Responses data layer tests', () => {
results: allComments.slice(0, 3),
pagination: { count: 4, numPages: 2 },
});
await executeThunk(fetchThreadComments(threadId, { endorsed }), store.dispatch, store.getState);
await executeThunk(fetchThreadComments(threadId, { threadType }), store.dispatch, store.getState);
// Post new comment
const comment = Factory.build('comment', { thread_id: threadId });
@@ -365,10 +293,10 @@ describe('Comments/Responses data layer tests', () => {
results: allComments.slice(3, 6),
pagination: { count: 6, numPages: 2 },
});
await executeThunk(fetchThreadComments(threadId, { page: 2, endorsed }), store.dispatch, store.getState);
await executeThunk(fetchThreadComments(threadId, { page: 2, threadType }), store.dispatch, store.getState);
// sorting is implemented on backend
expect(store.getState().comments.commentsInThreads[threadId][endorsed])
expect(store.getState().comments.commentsInThreads[threadId])
.toEqual([
'comment-1',
'comment-2',

View File

@@ -8,9 +8,9 @@ export const selectCommentOrResponseById = commentOrResponseId => createSelector
comments => comments[commentOrResponseId],
);
export const selectThreadComments = (threadId, endorsed = null) => createSelector(
export const selectThreadComments = (threadId) => createSelector(
[
state => state.comments.commentsInThreads[threadId]?.[endorsed] || [],
state => state.comments.commentsInThreads[threadId] || [],
selectCommentsById,
],
mapIdToComment,
@@ -28,12 +28,12 @@ export const selectCommentResponses = commentId => createSelector(
mapIdToComment,
);
export const selectThreadHasMorePages = (threadId, endorsed = null) => (
state => state.comments.pagination[threadId]?.[endorsed]?.hasMorePages || false
export const selectThreadHasMorePages = (threadId) => (
state => state.comments.pagination[threadId]?.hasMorePages || false
);
export const selectThreadCurrentPage = (threadId, endorsed = null) => (
state => state.comments.pagination[threadId]?.[endorsed]?.currentPage || null
export const selectThreadCurrentPage = (threadId) => (
state => state.comments.pagination[threadId]?.currentPage || null
);
export const selectCommentHasMorePages = commentId => (
@@ -47,3 +47,7 @@ export const selectCommentCurrentPage = commentId => (
export const selectCommentsStatus = state => state.comments.status;
export const selectCommentSortOrder = state => state.comments.sortOrder;
export const selectDraftComments = state => state.comments.draftComments;
export const selectDraftResponses = state => state.comments.draftResponses;

View File

@@ -1,6 +1,6 @@
import { createSlice } from '@reduxjs/toolkit';
import { EndorsementStatus, RequestStatus } from '../../../data/constants';
import { RequestStatus } from '../../../data/constants';
const commentsSlice = createSlice({
name: 'comments',
@@ -22,6 +22,8 @@ const commentsSlice = createSlice({
pagination: {},
responsesPagination: {},
sortOrder: true,
draftResponses: {},
draftComments: {},
},
reducers: {
fetchCommentsRequest: (state) => (
@@ -31,17 +33,12 @@ const commentsSlice = createSlice({
}
),
fetchCommentsSuccess: (state, { payload }) => {
const { threadId, page, endorsed } = payload;
const { threadId, page } = payload;
const newState = { ...state };
newState.status = RequestStatus.SUCCESSFUL;
newState.commentsInThreads = {
...newState.commentsInThreads,
[threadId]: newState.commentsInThreads[threadId] || {},
};
newState.pagination = {
...newState.pagination,
[threadId]: newState.pagination[threadId] || {},
@@ -50,23 +47,16 @@ const commentsSlice = createSlice({
if (page === 1) {
newState.commentsInThreads = {
...newState.commentsInThreads,
[threadId]: {
...newState.commentsInThreads[threadId],
[endorsed]: payload.commentsInThreads[threadId] || [],
},
[threadId]: payload.commentsInThreads[threadId] ? [...payload.commentsInThreads[threadId]] : [],
};
} else {
newState.commentsInThreads = {
...newState.commentsInThreads,
[threadId]: {
...newState.commentsInThreads[threadId],
[endorsed]: [
...new Set([
...(newState.commentsInThreads[threadId][endorsed] || []),
...(payload.commentsInThreads[threadId] || []),
]),
],
},
[threadId]: [
...new Set([
...(newState.commentsInThreads[threadId] || []),
...(payload.commentsInThreads[threadId] || []),
]),
],
};
}
@@ -74,11 +64,9 @@ const commentsSlice = createSlice({
...newState.pagination,
[threadId]: {
...newState.pagination[threadId],
[endorsed]: {
currentPage: payload.page,
totalPages: payload.pagination.numPages,
hasMorePages: Boolean(payload.pagination.next),
},
currentPage: payload.page,
totalPages: payload.pagination.numPages,
hasMorePages: Boolean(payload.pagination.next),
},
};
@@ -181,21 +169,10 @@ const commentsSlice = createSlice({
],
};
} else {
const threadComments = newState.commentsInThreads[payload.threadId] || {};
const endorsementStatus = threadComments[EndorsementStatus.DISCUSSION]
? EndorsementStatus.DISCUSSION
: EndorsementStatus.UNENDORSED;
const updatedThreadComments = {
...threadComments,
[endorsementStatus]: [
...(threadComments[endorsementStatus] || []),
payload.id,
],
};
const threadComments = newState.commentsInThreads[payload.threadId] || [];
newState.commentsInThreads = {
...newState.commentsInThreads,
[payload.threadId]: updatedThreadComments,
[payload.threadId]: [...threadComments, payload.id],
};
}
@@ -231,30 +208,7 @@ const commentsSlice = createSlice({
[payload.id]: payload,
},
commentDraft: null,
}
),
updateCommentsList: (state, { payload }) => {
const { id: commentId, threadId, endorsed } = payload;
const commentAddListtype = endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED;
const commentRemoveListType = !endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED;
const updatedThread = { ...state.commentsInThreads[threadId] };
updatedThread[commentRemoveListType] = updatedThread[commentRemoveListType]
?.filter(item => item !== commentId)
?? [];
updatedThread[commentAddListtype] = [
...(updatedThread[commentAddListtype] || []), commentId,
];
return {
...state,
commentsInThreads: {
...state.commentsInThreads,
[threadId]: updatedThread,
},
};
},
}),
deleteCommentRequest: (state) => (
{
...state,
@@ -285,12 +239,9 @@ const commentsSlice = createSlice({
commentsById: { ...state.commentsById },
};
[EndorsementStatus.DISCUSSION, EndorsementStatus.UNENDORSED, EndorsementStatus.ENDORSED].forEach((endorsed) => {
newState.commentsInThreads[threadId] = {
...newState.commentsInThreads[threadId],
[endorsed]: newState.commentsInThreads[threadId]?.[endorsed]?.filter(item => item !== commentId),
};
});
newState.commentsInThreads[threadId] = [
...newState.commentsInThreads[threadId]?.filter(item => item !== commentId) || [],
];
if (parentId) {
newState.commentsInComments[parentId] = newState.commentsInComments[parentId].filter(
@@ -308,6 +259,18 @@ const commentsSlice = createSlice({
sortOrder: payload,
}
),
setDraftComments: (state, { payload }) => (
{
...state,
draftComments: payload,
}
),
setDraftResponses: (state, { payload }) => (
{
...state,
draftResponses: payload,
}
),
},
});
@@ -328,12 +291,13 @@ export const {
updateCommentFailed,
updateCommentRequest,
updateCommentSuccess,
updateCommentsList,
deleteCommentDenied,
deleteCommentFailed,
deleteCommentRequest,
deleteCommentSuccess,
setCommentSortOrder,
setDraftComments,
setDraftResponses,
} = commentsSlice.actions;
export const commentsReducer = commentsSlice.reducer;

View File

@@ -1,7 +1,6 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { ContentActions, EndorsementStatus } from '../../../data/constants';
import { getHttpErrorStatus } from '../../utils';
import {
deleteComment, getCommentResponses, getThreadComments, postComment, updateComment,
@@ -26,7 +25,6 @@ import {
updateCommentDenied,
updateCommentFailed,
updateCommentRequest,
updateCommentsList,
updateCommentSuccess,
} from './slices';
@@ -78,7 +76,7 @@ export function fetchThreadComments(
{
page = 1,
reverseOrder,
endorsed = EndorsementStatus.DISCUSSION,
threadType,
enableInContextSidebar,
signal,
} = {},
@@ -87,11 +85,10 @@ export function fetchThreadComments(
try {
dispatch(fetchCommentsRequest());
const data = await getThreadComments(threadId, {
page, reverseOrder, endorsed, enableInContextSidebar, signal,
page, reverseOrder, threadType, enableInContextSidebar, signal,
});
dispatch(fetchCommentsSuccess({
...normaliseComments(camelCaseObject(data)),
endorsed,
page,
threadId,
}));
@@ -127,15 +124,12 @@ export function fetchCommentResponses(commentId, { page = 1, reverseOrder = true
};
}
export function editComment(commentId, comment, action = null) {
export function editComment(commentId, comment) {
return async (dispatch) => {
try {
dispatch(updateCommentRequest({ commentId }));
const data = await updateComment(commentId, comment);
dispatch(updateCommentSuccess(camelCaseObject(data)));
if (action === ContentActions.ENDORSE) {
dispatch(updateCommentsList(camelCaseObject(data)));
}
} catch (error) {
if (getHttpErrorStatus(error) === 403) {
dispatch(updateCommentDenied());

View File

@@ -3,11 +3,11 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import { Button, Spinner } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button, Spinner } from '@edx/paragon';
import { RequestStatus } from '../../data/constants';
import DiscussionContext from '../common/context';

View File

@@ -185,7 +185,7 @@ const threadsSlice = createSlice({
},
pages: !payload.anonymousToPeers
? [
...[payload.id].concat(state.pages[0]) || [],
...(state.pages[0] ? [payload.id].concat(state.pages[0]) : []),
...state.pages.slice(1),
]
: [...state.pages],

View File

@@ -1,13 +1,13 @@
import React, { useCallback, useContext } from 'react';
import {
Button, Icon, IconButton,
} from '@openedx/paragon';
import { Close } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button, Icon, IconButton,
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import Search from '../../../components/Search';
import { RequestStatus } from '../../../data/constants';

View File

@@ -3,6 +3,10 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import {
Button, Form, Spinner, StatefulButton,
} from '@openedx/paragon';
import { Help, Post } from '@openedx/paragon/icons';
import { Formik } from 'formik';
import { isEmpty } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
@@ -11,13 +15,10 @@ import * as Yup from 'yup';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import {
Button, Form, Spinner, StatefulButton,
} from '@edx/paragon';
import { Help, Post } from '@edx/paragon/icons';
import { TinyMCEEditor } from '../../../components';
import FormikErrorFeedback from '../../../components/FormikErrorFeedback';
import PostHelpPanel from '../../../components/PostHelpPanel';
import PostPreviewPanel from '../../../components/PostPreviewPanel';
import useDispatchWithState from '../../../data/hooks';
import selectCourseCohorts from '../../cohorts/data/selectors';
@@ -241,7 +242,7 @@ const PostEditor = ({
resetForm,
}) => (
<Form className="m-4 card p-4 post-form" onSubmit={handleSubmit}>
<h4 className="mb-4 font-style font-size-16" style={{ lineHeight: '16px' }}>
<h4 className="mb-4 font-style" style={{ lineHeight: '16px' }}>
{editExisting
? intl.formatMessage(messages.editPostHeading)
: intl.formatMessage(messages.addPostHeading)}
@@ -409,6 +410,7 @@ const PostEditor = ({
onEditorChange={formikCompatibleHandler(handleChange, 'comment')}
onBlur={formikCompatibleHandler(handleBlur, 'comment')}
/>
<PostHelpPanel />
<FormikErrorFeedback name="comment" />
</div>
<PostPreviewPanel htmlNode={values.comment} isPost editExisting={editExisting} />
@@ -423,7 +425,7 @@ const PostEditor = ({
onBlur={handleBlur}
className="mr-4.5"
>
<span className="font-size-14">
<span>
{intl.formatMessage(messages.followPost)}
</span>
</Form.Checkbox>
@@ -436,7 +438,7 @@ const PostEditor = ({
onChange={handleChange}
onBlur={handleBlur}
>
<span className="font-size-14">
<span>
{intl.formatMessage(messages.anonymousToPeersPost)}
</span>
</Form.Checkbox>

View File

@@ -368,5 +368,34 @@ describe('PostEditor', () => {
expect(container.querySelector('[data-testid="hide-preview-button"]')).not.toBeInTheDocument();
});
});
it('should show Help Panel', async () => {
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
await act(async () => {
fireEvent.click(container.querySelector('[data-testid="help-button"]'));
});
await waitFor(() => {
expect(container.querySelector('[data-testid="hide-help-button"]')).toBeInTheDocument();
});
});
it('should hide Help Panel', async () => {
await renderComponent(true, `/${courseId}/posts/${threadId}/edit`);
await act(async () => {
fireEvent.click(container.querySelector('[data-testid="help-button"]'));
});
await act(async () => {
fireEvent.click(container.querySelector('[data-testid="hide-help-button"]'));
});
await waitFor(() => {
expect(container.querySelector('[data-testid="help-button"]')).toBeInTheDocument();
expect(container.querySelector('[data-testid="hide-help-button"]')).not.toBeInTheDocument();
});
});
});
});

View File

@@ -1,10 +1,9 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { Card, Form } from '@openedx/paragon';
import classNames from 'classnames';
import { Card, Form } from '@edx/paragon';
import DiscussionContext from '../../common/context';
const PostTypeCard = ({

View File

@@ -116,6 +116,36 @@ const messages = defineMessages({
defaultMessage: 'Show preview',
description: 'show preview button text to allow user to see their post content.',
},
showHelpIcon: {
id: 'discussions.editor.posts.showHelp.icon',
defaultMessage: 'Show Help',
description: 'show help icon to allow user to see important documentation.',
},
discussionHelpHeader: {
id: 'discussions.editor.posts.discussionHelpHeader',
defaultMessage: 'Discussions help',
description: 'header text for post help section.',
},
discussionHelpDescription: {
id: 'discussions.editor.posts.discussionHelpDescription',
defaultMessage: 'Course discussions give you the opportunity to start conversations, ask questions, and interact with other learners. See the links below to learn more:',
description: 'description message for post help section.',
},
discussionHelpCourseParticipation: {
id: 'discussions.editor.posts.discussionHelpCourseParticipation',
defaultMessage: 'Participating in course discussions',
description: 'Documentation link title for participating in course discussions.',
},
discussionHelpMathExpressions: {
id: 'discussions.editor.posts.discussionHelpMathExpressions',
defaultMessage: 'Entering math expressions in course discussions',
description: 'Documentation link title for entering math expressions in course discussions.',
},
discussionHelpTooltip: {
id: 'discussions.editor.posts.discussionHelpTooltip',
defaultMessage: 'Learn more about MathJax & LaTeX',
description: 'Tooltip help message for documentation help.',
},
actionsAlt: {
id: 'discussions.actions.label',
defaultMessage: 'Actions menu',

View File

@@ -3,6 +3,10 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import {
Collapsible, Form, Icon, Spinner,
} from '@openedx/paragon';
import { Check, Tune } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { capitalize, isEmpty, toString } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
@@ -10,10 +14,6 @@ import { useParams } from 'react-router-dom';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Collapsible, Form, Icon, Spinner,
} from '@edx/paragon';
import { Check, Tune } from '@edx/paragon/icons';
import {
PostsStatusFilter, RequestStatus,
@@ -44,7 +44,7 @@ export const ActionItem = React.memo(({
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={value === selected ? '0' : '-1'}
>
<Icon src={Check} className={classNames('text-success mr-2', { invisible: value !== selected })} />
<Icon src={Check} className={classNames('text-success dropdown-icon-dimensions', { invisible: value !== selected })} />
<Form.Radio id={id} className="sr-only sr-only-focusable" value={value} tabIndex="0">
{label}
</Form.Radio>

View File

@@ -3,15 +3,15 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Button,
Form,
ModalDialog,
} from '@edx/paragon';
} from '@openedx/paragon';
import { useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { selectModerationSettings } from '../../data/selectors';
import messages from './messages';

View File

@@ -1,11 +1,12 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon, IconButton, OverlayTrigger, Tooltip,
} from '@edx/paragon';
import { ThumbUpFilled, ThumbUpOutline } from '@edx/paragon/icons';
} from '@openedx/paragon';
import { ThumbUpFilled, ThumbUpOutline } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Hyperlink, useToggle } from '@openedx/paragon';
import classNames from 'classnames';
import { toString } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
@@ -8,7 +9,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../components/HTMLLoader';
import { ContentActions, getFullUrl } from '../../../data/constants';
@@ -85,6 +85,10 @@ const Post = ({ handleAddResponseButton }) => {
updateExistingThread(postId, { pinned: !pinned }),
), [postId, pinned]);
const handlePostLike = useCallback(() => {
dispatch(updateExistingThread(postId, { voted: !voted }));
}, [postId, voted]);
const handlePostReport = useCallback(() => {
if (abuseFlagged) {
dispatch(updateExistingThread(postId, { flagged: !abuseFlagged }));
@@ -109,10 +113,6 @@ const Post = ({ handleAddResponseButton }) => {
hideClosePostModal();
}, [postId, hideClosePostModal]);
const handlePostLike = useCallback(() => {
dispatch(updateExistingThread(postId, { voted: !voted }));
}, [postId, voted]);
const handlePostFollow = useCallback(() => {
dispatch(updateExistingThread(postId, { following: !following }));
}, [postId, following]);
@@ -188,10 +188,7 @@ 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', { 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter })}
style={{ lineHeight: '20px' }}
>
<span className="text-gray-500" style={{ lineHeight: '20px' }}>
@@ -206,13 +203,9 @@ const Post = ({ handleAddResponseButton }) => {
)}
>
{(topicContext && !topic) ? (
<>
<span className="w-auto">{topicContext.chapterName}</span>
<span className="mx-1">/</span>
<span className="w-auto">{topicContext.verticalName}</span>
<span className="mx-1">/</span>
<span className="w-auto">{topicContext.unitName}</span>
</>
<span>
{topicContext.chapterName} / {topicContext.verticalName} / {topicContext.unitName}
</span>
) : (
getTopicInfo(topic)
)}

View File

@@ -1,15 +1,15 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import {
Icon, IconButton, OverlayTrigger, Tooltip,
} from '@openedx/paragon';
import {
Locked, People, StarFilled, StarOutline,
} from '@openedx/paragon/icons';
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon, IconButton, OverlayTrigger, Tooltip,
} from '@edx/paragon';
import {
Locked, People, StarFilled, StarOutline,
} from '@edx/paragon/icons';
import { updateExistingThread } from '../data/thunks';
import LikeButton from './LikeButton';

View File

@@ -1,11 +1,11 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Avatar, Badge, Icon } from '@openedx/paragon';
import { Question } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Avatar, Badge, Icon } from '@edx/paragon';
import { Question } from '@edx/paragon/icons';
import { AvatarOutlineAndLabelColors, ThreadType } from '../../../data/constants';
import { AuthorLabel } from '../../common';

View File

@@ -1,13 +1,13 @@
import React, { useContext, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Badge, Icon } from '@openedx/paragon';
import { CheckCircle, PushPin } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Badge, Icon } from '@edx/paragon';
import { CheckCircle, PushPin } from '@edx/paragon/icons';
import { AvatarOutlineAndLabelColors, Routes, ThreadType } from '../../../data/constants';
import AuthorLabel from '../../common/AuthorLabel';
@@ -89,13 +89,13 @@ const PostLink = ({
<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-500 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">
<span className="text-gray-700 font-weight-normal font-style align-bottom">
{isPostPreviewAvailable(previewBody) ? previewBody : intl.formatMessage(messages.postWithoutPreview)}
</span>
</div>

View File

@@ -1,17 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Badge, Icon, OverlayTrigger, Tooltip,
} from '@openedx/paragon';
import {
People, QuestionAnswer, QuestionAnswerOutline,
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
} from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import * as timeago from 'timeago.js';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Badge, Icon, OverlayTrigger, Tooltip,
} from '@edx/paragon';
import {
People, QuestionAnswer, QuestionAnswerOutline,
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
} from '@edx/paragon/icons';
import timeLocale from '../../common/time-locale';
import { selectUserHasModerationPrivileges } from '../../data/selectors';
@@ -68,7 +68,7 @@ const PostSummaryFooter = ({
</OverlayTrigger>
{preview && commentCount > 1 && (
<div className="d-flex align-items-center ml-4.5 text-gray-700 font-style font-size-12">
<div className="d-flex align-items-center ml-4.5 text-gray-700 font-style">
<OverlayTrigger
overlay={(
<Tooltip id={`follow-${postId}-tooltip`}>

View File

@@ -1,13 +1,13 @@
import React, { useCallback, useContext } from 'react';
import PropTypes from 'prop-types';
import { Icon, OverlayTrigger, Tooltip } from '@openedx/paragon';
import { HelpOutline, PostOutline, Report } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { Link, useLocation, useParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
import { HelpOutline, PostOutline, Report } from '@edx/paragon/icons';
import { Routes } from '../../../../data/constants';
import DiscussionContext from '../../../common/context';

View File

@@ -1,10 +1,9 @@
import React, { useEffect } from 'react';
import { ProductTour } from '@openedx/paragon';
import isEmpty from 'lodash/isEmpty';
import { useDispatch } from 'react-redux';
import { ProductTour } from '@edx/paragon';
import { useTourConfiguration } from '../data/hooks';
import { fetchDiscussionTours } from './data/thunks';

View File

@@ -1,5 +1,10 @@
import { useCallback, useContext, useMemo } from 'react';
import {
CheckCircle, CheckCircleOutline, Delete, Edit, InsertLink,
Institution, Lock, LockOpen, Pin, Report, School,
Verified, VerifiedOutline,
} from '@openedx/paragon/icons';
import { getIn } from 'formik';
import { uniqBy } from 'lodash';
import { useSelector } from 'react-redux';
@@ -8,12 +13,8 @@ import {
} from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import {
CheckCircle, CheckCircleOutline, Delete, Edit, InsertLink,
Institution, Lock, LockOpen, Pin, Report, School,
Verified, VerifiedOutline,
} from '@edx/paragon/icons';
import { DENIED, LOADED } from '../components/NavigationBar/data/slice';
import {
ContentActions, Routes, ThreadType,
} from '../data/constants';
@@ -313,3 +314,12 @@ export function getAuthorLabel(intl, authorLabel) {
return authorLabelMappings[authorLabel] || {};
}
export const isCourseStatusValid = (courseStatus) => [DENIED, LOADED].includes(courseStatus);
export const extractContent = (content) => {
if (typeof content === 'object') {
return content.target.getContent();
}
return content;
};

View File

@@ -1,51 +1 @@
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';
import csMessages from './messages/cs.json';
import deDEMessages 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';
// no need to import en messages-- they are in the defaultMessage field
const appMessages = {
ar: arMessages,
cs: csMessages,
'de-de': deDEMessages,
'es-419': es419Messages,
'es-ar': esARMessages,
'es-es': esESMessages,
'fa-ir': faIRMessages,
fr: frMessages,
'fr-ca': frCAMessages,
'fr-fr': frFRMessages,
hi: hiMessages,
'it-it': itITMessages,
pl: plMessages,
'pt-pt': ptPTMessages,
'tr-tr': trTRMessages,
uk: ukMessages,
ru: ruMessages,
'zh-cn': zhCNMessages,
};
export default [
headerMessages,
footerMessages,
paragonMessages,
appMessages,
];
export default [];

View File

@@ -1,209 +0,0 @@
{
"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": "حسنًا",
"navigation.course.tabs.label": "مواد المساق",
"discussions.topics.backAlt": "العودة إلى قائمة المواضيع",
"discussions.topics.discussions": "{العدد، الجمع، =0 { المحادثة } واحد {# المحادثة } آخر {# المحادثات } }",
"discussions.topics.questions": "{عد، جمع، =0 {سؤال} واحد {# سؤال} آخر {# أسئلة} }",
"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, flagled {reportedactivity} النشاط {الأكثر نشاطًا} Other { {sort} } }",
"discussion.learner.allActivity": "كل النشاط",
"discussion.learner.posts": "المنشورات",
"discussions.comments.comment.addComment": "إضافة تعليق",
"discussions.comments.comment.addResponse": "إضافة رد",
"discussions.comments.comment.abuseFlaggedMessage": "تم إبلاغ الطاقم عن هذا المحتوى لمراجعته.",
"discussions.actions.back.alt": "العودة إلى القائمة",
"discussions.comments.comment.responseCount": "{num, plural, =0 {لا يوجد ردود} واحد {يظهر # رد} آخر {يظهر # ردود} }",
"discussions.comments.comment.endorsedResponseCount": "{num, plural, =0 {لا توجد ردود معتمدة} واحدة {يتم عرض # ردود معتمدة} أخرى {يتم عرض # ردود معتمدة} }",
"discussions.comments.comment.loadMoreComments": "تحميل المزيد من التعليقات",
"discussions.comments.comment.loadMoreResponses": "تحميل المزيد من الردود",
"discussions.comments.comment.visibility": "هذا المنشور مرئي لـ {group, Select, null {Everyone}other { {group} } }.",
"discussions.comments.comment.postedTime": "{postType, Select, المحادثة {المحادثة} سؤال {سؤال} آخر { {postType} } } تم النشر بواسطة {relativeTime}",
"discussions.comments.comment.commentTime": "تم النشر {relativeTime}",
"discussions.comments.comment.answer": "الإجابة",
"discussions.comments.comment.answeredlabel": "تم تعليمها كمُجابة من طرف",
"discussions.comments.comment.endorsed": "معتمد",
"discussions.comments.comment.endorsedlabel": "اعتمده",
"discussions.actions.label": "قائمة الإجراءات",
"discussions.editor.submit": "إرسال",
"discussions.editor.submitting": "الإرسال جارٍ",
"discussions.editor.cancel": "إلغاء",
"discussions.editor.error.empty": "لا يمكن أن يكون محتوى المنشور فارغًا.",
"discussions.editor.delete.response.title": "حذف الرد",
"discussions.editor.delete.response.description": "هل أنت متأكد من رغبتك في حذف هذا الردّ نهائيًا؟",
"discussions.editor.delete.comment.title": "حذف التعليق",
"discussions.editor.delete.comment.description": "هل أنت متأكد من رغبتك في حذف هذا التعليق نهائيا؟",
"discussions.delete.confirmation.button.delete": "حذف",
"discussions.editor.response.response.title": "أتريد الإبلاغ عن محتوى غير لائق؟",
"discussions.editor.response.description": "سيراجع فريق الإشراف على المناقشة هذا المحتوى و يتخذ الإجراء المناسب.",
"discussions.editor.report.comment.title": "أتريد الإبلاغ عن محتوى غير لائق؟",
"discussions.editor.report.comment.description": "سيراجع فريق الإشراف على المناقشة هذا المحتوى ويتخذ الإجراء المناسب.",
"discussions.editor.comments.editReasonCode": "سبب التعديل",
"discussions.editor.posts.editReasonCode.error": "حدد سبب التعديل",
"discussions.comment.comments.editedBy": "عدّله",
"discussions.comment.comments.fullStop": "•",
"discussions.comment.comments.reason": "السبب",
"discussions.post.closedBy": "تم إقفال المنشور من طرف",
"discussion.comment.time": "منذ {time}",
"discussion.thread.notFound": "المناقشة غير موجودة",
"discussions.comment.sortFilterStatus": "{فرز، تحديد، خطأ {الأقدم أولاً} صحيح {الأحدث أولاً} أخرى { {sort} } }",
"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.example.title": "جولة المثال",
"tour.example.body": "هذه جولة نموذجية",
"learn.course.tabs.navigation.overflow.menu": "المزيد...",
"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.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.app.title": "المناقشات",
"discussions.posts.actionBar.searchAllPosts": "البحث في كافّة المنشورات",
"discussions.posts.actionBar.search": "{صفحة، حدد المواضيع {بحث في المواضيع} مشاركات {بحث في كل المشاركات} الطلّاب {بحث الطلّاب } myPosts {بحث في كل المشاركات} أخرى { {page} } }",
"discussions.actionBar.searchInfo": "{num، plural, =0 {لا توجد نتائج} one {تم إظهار نتيجة واحدة} two {تم إظهار نتيجتين} few {تم إظهار # نتائج} many {تم إظهار # نتيجة} other {تم إظهار # نتائج}} لـ\"{text}\"",
"discussions.actionBar.searchRewriteInfo": "لم يعثر على نتائج لـ \"{searchString}\". {num، plural, =0 {لا توجد نتائج} one {تم إظهار نتيجة واحدة} two {تم إظهار نتيجتين} few {تم إظهار # نتائج} many {تم إظهار # نتيجة} other {تم إظهار # نتائج}} لـ\"{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} } } {الحالة، حدد، الحالة الكل {} الحالة غير مقروءة {غير مقروءة} الحالة التالية {followed} الحالة المُبلغ عنها {المُبلغ عنها} الحالة غير المُجيبة {unanswered} الحالة غير المُستجيبة {unresponded} أخرى { {status} } } {نوع، حدد، المحادثة { المحادثات } سؤال {أسئلة} جميع {المشاركات} أخرى { {type} } } {cohortType، حدد، الكل {} المجموعة {in {فوج} } أخرى { {cohortType} } } مرتبة حسب {فرز، تحديد، LastActivityAt { النشاط الأخير} تعليق عدد {أكثر الأنشطة} عدد الأصوات {أكثر الإعجابات} أخرى { {sort} } }"
}

View File

@@ -1,209 +0,0 @@
{
"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": "Zasílání příspěvků v diskuzích je zakázáno týmem kurzu",
"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",
"navigation.course.tabs.label": "Materiály ke kurzu",
"discussions.topics.backAlt": "Zpět na seznam témat",
"discussions.topics.discussions": "",
"discussions.topics.questions": "",
"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": "",
"discussion.learner.allActivity": "Všechna aktivita",
"discussion.learner.posts": "Příspěvky",
"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": "",
"discussions.comments.comment.endorsedResponseCount": "",
"discussions.comments.comment.loadMoreComments": "Načíst další komentáře",
"discussions.comments.comment.loadMoreResponses": "Načíst další odpovědi",
"discussions.comments.comment.visibility": "",
"discussions.comments.comment.postedTime": "",
"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": "",
"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.example.title": "",
"tour.example.body": "",
"learn.course.tabs.navigation.overflow.menu": "Více...",
"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.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.app.title": "Diskuze",
"discussions.posts.actionBar.searchAllPosts": "Prohledejte příspěvky",
"discussions.posts.actionBar.search": "",
"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": ""
}

View File

@@ -1,209 +0,0 @@
{
"discussions.actions.button.alt": "Aktionsmenü",
"discussions.actions.copylink": "Link kopieren",
"discussions.actions.edit": "Bearbeiten",
"discussions.actions.pin": "Veröffentlichen",
"discussions.actions.unpin": "Ablösen",
"discussions.actions.delete": "Löschen",
"discussions.confirmation.button.confirm": "Bestätigen",
"discussions.actions.close": "Schließen",
"discussions.actions.reopen": "Wieder öffnen",
"discussions.actions.report": "Melden",
"discussions.actions.unreport": "Meldung aufheben",
"discussions.actions.endorse": "Befürworten",
"discussions.actions.unendorse": "Nicht Befürworten",
"discussions.actions.markAnswered": "Als beantwortet markieren",
"discussions.actions.unMarkAnswered": "Markierung als beantwortet aufheben",
"discussions.modal.confirmation.button.cancel": "Löschen",
"discussions.empty.allTopics": "Alle Diskussionsaktivitäten zu diesen Themen werden hier angezeigt.",
"discussions.empty.allPosts": "Alle Diskussionsaktivitäten für Ihren Kurs werden hier angezeigt.",
"discussions.empty.myPosts": "Beiträge, mit denen Sie interagiert haben, werden hier angezeigt.",
"discussions.empty.topic": "Alle Diskussionsaktivitäten zu diesem Thema werden hier angezeigt.",
"discussions.empty.title": "Hier noch nichts",
"discussions.empty.noPostSelected": "Kein Beitrag ausgewählt",
"discussions.empty.noTopicSelected": "Kein Thema ausgewählt",
"discussions.sidebar.noResultsFound": "Keine Ergebnisse gefunden",
"discussions.sidebar.differentKeywords": "Versuchen Sie, nach anderen Schlüsselwörtern zu suchen",
"discussions.sidebar.removeKeywords": "Versuchen Sie, nach anderen Schlüsselwörtern zu suchen oder einige Filter zu entfernen",
"discussions.sidebar.removeKeywordsOnly": "Versuchen Sie, nach anderen Schlüsselwörtern zu suchen",
"discussions.sidebar.removeFilters": "Versuchen Sie, einige Filter zu entfernen",
"discussions.empty.iconAlt": "Leer",
"discussions.authors.label.staff": "Betreuung",
"discussions.authors.label.ta": "TA",
"discussions.learner.loadMostPosts": "Mehr Beiträge laden",
"discussions.post.anonymous.author": "Anonym",
"discussion.blackoutBanner.information": "Das Posten in Diskussionen ist vom Kursteam 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",
"navigation.course.tabs.label": "Kursmaterial",
"discussions.topics.backAlt": "Zurück zur Themenliste",
"discussions.topics.discussions": "{count, plural, =0 { Diskussion } one {# Diskussion } other {# Diskussionen } }",
"discussions.topics.questions": "{count, plural, =0 { Frage } one {# Frage } other {# Questions} }",
"discussions.topics.reported": "{reported} gemeldet",
"discussions.topics.previouslyReported": "{previouslyReported} zuvor gemeldet",
"discussions.topics.find.label": "Themen suchen",
"discussions.topics.unnamed.section.label": "Unbenannter Abschnitt",
"discussions.topics.unnamed.subsection.label": "Unbenannter Unterabschnitt",
"discussions.subtopics.unnamed.topic.label": "Unbenanntes Thema",
"discussions.topics.title": "Kein Thema vorhanden",
"discussions.topics.createTopic": "Bitte kontaktieren Sie Ihren Administrator, um ein Thema zu erstellen",
"discussions.topics.nothing": "Hier noch nichts",
"discussions.topics.archived.label": "Archiviert",
"discussions.learner.reported": "{reported} gemeldet",
"discussions.learner.previouslyReported": "{previouslyReported} zuvor gemeldet",
"discussions.learner.lastLogin": "Zuletzt aktiv {lastActiveTime}",
"discussions.learner.loadMostLearners": "Lade weiteres",
"discussions.learner.back": "Zurück",
"discussions.learner.activityForLearner": "Aktivität für {username}",
"discussions.learner.mostActivity": "Die meisten Aktivitäten",
"discussions.learner.reportedActivity": "Gemeldete Aktivität",
"discussions.learner.recentActivity": "Letzte Aktivität",
"discussions.learner.sortFilterStatus": "Alle Lernenden sortiert nach {sortieren, auswählen, markiert {gemeldete Aktivität} Aktivität {größte Aktivität} andere { {sort} } }",
"discussion.learner.allActivity": "Alle Aktivitäten",
"discussion.learner.posts": "Beiträge",
"discussions.comments.comment.addComment": "Kommentar hinzufügen",
"discussions.comments.comment.addResponse": "Fügen Sie eine Antwort hinzu",
"discussions.comments.comment.abuseFlaggedMessage": "Inhalte, die den Kursmitarbeitern zur Überprüfung gemeldet wurden",
"discussions.actions.back.alt": "Zurück zur Liste",
"discussions.comments.comment.responseCount": "{num, plural, =0 {Keine Antworten} one {# Antwort wird angezeigt} other {# Antworten werden angezeigt} }",
"discussions.comments.comment.endorsedResponseCount": "{num, plural, =0 {Keine befürworteten Antworten} one {# befürwortete Antwort wird angezeigt} other {# befürwortete Antworten werden angezeigt} }",
"discussions.comments.comment.loadMoreComments": "Weitere Kommentare laden",
"discussions.comments.comment.loadMoreResponses": "Weitere Antworten laden",
"discussions.comments.comment.visibility": "Dieser Beitrag ist für {group, select, null {Everyone} other { {group} } } sichtbar.",
"discussions.comments.comment.postedTime": "{postType, select, Diskussion {Diskussion} Frage {Frage} other { {postType} } } gepostet {relativeTime} von",
"discussions.comments.comment.commentTime": "Gepostet {relativeTime}",
"discussions.comments.comment.answer": "Antwort",
"discussions.comments.comment.answeredlabel": "Als beantwortet von markiert",
"discussions.comments.comment.endorsed": "Bestätigt",
"discussions.comments.comment.endorsedlabel": "Bestätigt von",
"discussions.actions.label": "Aktionsmenü",
"discussions.editor.submit": "Einreichen",
"discussions.editor.submitting": "Übermitteln, einreichen",
"discussions.editor.cancel": "Löschen",
"discussions.editor.error.empty": "Der Beitragsinhalt darf nicht leer sein.",
"discussions.editor.delete.response.title": "Antwort löschen",
"discussions.editor.delete.response.description": "Möchten Sie diese Antwort wirklich dauerhaft löschen?",
"discussions.editor.delete.comment.title": "Kommentar löschen",
"discussions.editor.delete.comment.description": "Möchten Sie diesen Kommentar wirklich dauerhaft löschen?",
"discussions.delete.confirmation.button.delete": "Löschen",
"discussions.editor.response.response.title": "Unangemessene Inhalte melden?",
"discussions.editor.response.description": "Das Diskussionsmoderationsteam überprüft diesen Inhalt und ergreift entsprechende Maßnahmen.",
"discussions.editor.report.comment.title": "Unangemessene Inhalte melden?",
"discussions.editor.report.comment.description": "Das Diskussionsmoderationsteam überprüft diesen Inhalt und ergreift entsprechende Maßnahmen.",
"discussions.editor.comments.editReasonCode": "Grund für die Bearbeitung",
"discussions.editor.posts.editReasonCode.error": "Grund für die Bearbeitung auswählen",
"discussions.comment.comments.editedBy": "Bearbeitet von",
"discussions.comment.comments.fullStop": "•",
"discussions.comment.comments.reason": "Grund",
"discussions.post.closedBy": "Post geschlossen von",
"discussion.comment.time": "{time} vor",
"discussion.thread.notFound": "Thema nicht gefunden",
"discussions.comment.sortFilterStatus": "{sort, select, falsch {Älteste zuerst} Wahr {Neueste zuerst} other { {sort} } }",
"discussions.topics.sort.message": "Sortiert nach {sortBy}",
"discussions.topics.sort.lastActivity": "Letzte Aktivität",
"discussions.topics.sort.commentCount": "Die meisten Aktivitäten",
"discussions.topics.sort.courseStructure": "Kursstruktur",
"discussions.topics.unnamed.label": "Unbenannte Kategorie",
"discussions.subtopics.unnamed.label": "Unbenannte Unterkategorie",
"tour.action.advance": "Weiter",
"tour.action.dismiss": "Abgewiesen",
"tour.action.end": "okay",
"tour.example.title": "Beispiel",
"tour.example.body": "Dies ist eine Beispiel",
"learn.course.tabs.navigation.overflow.menu": "Mehr...",
"discussions.navigation.breadcrumbMenu.allTopics": "Themen",
"discussions.navigation.breadcrumbMenu.showAll": "Alles anzeigen",
"discussions.navigation.navigationBar.allPosts": "Alle Artikel",
"discussions.navigation.navigationBar.allTopics": "Themen",
"discussions.navigation.navigationBar.myPosts": "Meine Posts",
"discussions.navigation.navigationBar.learners": "Lernende",
"discussions.post.author.anonymous": "Anonym",
"discussions.post.addResponse": "Antwort hinzufügen",
"discussions.post.lastResponse": "Letzte Antwort {time}",
"discussions.post.postedOn": "Gepostet {time} von {author} {authorLabel}",
"discussions.post.contentReported": "Gemeldet",
"discussions.post.following": "Folge",
"discussions.post.follow": "Folgen",
"discussions.post.followed": "Gefolgt",
"discussions.post.notFollowed": "Nicht gefolgt",
"discussions.post.answered": "Beantwortet",
"discussions.post.unFollow": "Verlassen",
"discussions.post.like": "Wie",
"discussions.post.removeLike": "nicht wie",
"discussions.post.liked": "gefallen",
"discussions.post.likes": "Likes",
"discussions.post.viewActivity": "Aktivität anzeigen",
"discussions.post.activity": "Aktivität",
"discussions.post.closed": "Beitrag für Antworten und Kommentare geschlossen",
"discussions.post.relatedTo": "Im Zusammenhang mit",
"discussions.editor.delete.post.title": "Beitrag entfernen",
"discussions.editor.delete.post.description": "Möchten Sie diesen Beitrag wirklich dauerhaft löschen?",
"discussions.post.delete.confirmation.button.delete": "Löschen",
"discussions.editor.report.post.title": "Unangemessene Inhalte melden?",
"discussions.editor.report.post.description": "Das Diskussionsmoderationsteam überprüft diesen Inhalt und ergreift entsprechende Maßnahmen.",
"discussions.post.closePostModal.title": "Beitrag schließen",
"discussions.post.closePostModal.text": "Geben Sie einen Grund für das Schließen dieses Beitrags ein. Dies wird nur anderen Moderatoren angezeigt.",
"discussions.post.closePostModal.reasonCodeInput": "Grund",
"discussions.post.closePostModal.cancel": "Löschen",
"discussions.post.closePostModal.confirm": "Beitrag schließen",
"discussions.post.label.new": "{count} Neu",
"discussions.post.editedBy": "Bearbeitet von",
"discussions.post.editReason": "Grund",
"discussions.post.postWithoutPreview": "Keine Vorschau vorhanden",
"discussions.post.follow.description": "Sie folgen diesem Beitrag",
"discussions.post.unfollow.description": "Sie folgen diesem Beitrag nicht",
"discussions.app.title": "Diskussionen",
"discussions.posts.actionBar.searchAllPosts": "Einträge durchsuchen",
"discussions.posts.actionBar.search": "{Seite, Auswahl, Themen {Themen durchsuchen} Beiträge {Alle Beiträge durchsuchen} Lernende {Lernende suchen} myPosts {Alle Beiträge durchsuchen} andere { {page} } }",
"discussions.actionBar.searchInfo": "{count} Ergebnisse für &quot;{text}&quot; werden angezeigt",
"discussions.actionBar.searchRewriteInfo": "Keine Ergebnisse gefunden für &quot;{searchString}&quot;. {count} Ergebnisse für &quot;{textSearchRewrite}&quot; werden angezeigt.",
"discussions.actionBar.searchInfoSearching": "Suche...",
"discussions.actionBar.clearSearch": "Klare Ergebnisse",
"discussion.posts.actionBar.add": "Fügen Sie einen Beitrag hinzu",
"discussion.posts.actionBar.close": "Schließen",
"discussions.post.editor.type": "Beitragsart",
"discussions.post.editor.addPostHeading": "Fügen Sie einen Beitrag hinzu",
"discussions.post.editor.editPostHeading": "Beitrag bearbeiten",
"discussions.post.editor.typeDescription": "Wenn Sie eine konkrete Antwort für ein Problem suchen, stellen Sie eine Frage. Um sich mit anderen Nutzern über ein Thema auszutauschen und Ideen zu teilen, nutzen Sie die Diskussion. ",
"discussions.post.editor.required": "Erforderlich",
"discussions.post.editor.questionType": "Frage",
"discussions.post.editor.questionDescription": "Sprechen Sie Probleme an, die Antworten erfordern",
"discussions.post.editor.discussionType": "Diskussion",
"discussions.post.editor.discussionDescription": "Teilen Sie Ideen und beginnen Sie Gespräche",
"discussions.post.editor.topicArea": "Themenbereich",
"discussions.post.editor.topicAreaDescription": "Fügen Sie Ihren Beitrag zu einem entsprechenden Thema hinzu, um andern das Auffinden zu erleichtern.",
"discussions.post.editor.cohortVisibility": "Kohortensichtbarkeit",
"discussions.post.editor.cohortVisibilityAllLearners": "Alle Teilnehmer",
"discussions.post.editor.title": "Titel des Beitrags",
"discussions.post.editor.titleDescription": "Um zur Teilnahme zu motivieren, fügen Sie bitte einen klaren und beschreibenden Titel hinzu.",
"discussions.post.editor.title.error": "Beitragstitel darf nicht leer sein.",
"discussions.post.editor.content.error": "Der Beitragsinhalt darf nicht leer sein.",
"discussions.post.editor.questionText": "Ihre Frage oder Idee (*)",
"discussions.post.editor.preview": "Vorschau",
"discussions.post.editor.followPost": "Diesem Eintrag folgen",
"discussions.post.editor.anonymousPost": "Anonym posten",
"discussions.post.editor.anonymousToPeersPost": "Posten Sie anonym an Kollegen",
"discussions.editor.posts.editReasonCode": "Bearbeitungsgrund",
"discussions.editor.posts.showPreview.button": "Vorschau zeigen",
"discussions.topic.noName.label": "Unbenannte Kategorie",
"discussions.subtopic.noName.label": "Unbenannte Unterkategorie",
"discussions.posts.filter.showALl": "Alles anzeigen",
"discussions.posts.filter.discussions": "Diskussionen",
"discussions.posts.filter.questions": "Fragen",
"discussions.posts.filter.message": "Status: {filterBy}",
"discussions.posts.status.filter.anyStatus": "Jeder Status",
"discussions.posts.status.filter.unread": "Ungelesen",
"discussions.posts.status.filter.following": "Folge",
"discussions.posts.status.filter.reported": "Gemeldet",
"discussions.posts.status.filter.unanswered": "Unbeantwortet",
"discussions.posts.status.filter.unresponded": "Nicht geantwortet",
"discussions.posts.filter.myPosts": "Meine Posts",
"discussions.posts.filter.myDiscussions": "Meine Diskussionen",
"discussions.posts.filter.myQuestions": "Meine Fragen",
"discussions.posts.sort.message": "Sortiert nach {sortBy}",
"discussions.posts.sort.lastActivity": "Letzte Aktivität",
"discussions.posts.sort.commentCount": "Die meisten Aktivitäten",
"discussions.posts.sort.voteCount": "Die meisten Likes",
"discussions.posts.sort-filter.sortFilterStatus": "{own, select, falsch {All} Wahr {Own} other { {own} } } {status, select, statusAll {} statusUnread {unread} statusFollowing {followed} statusReported {reported} statusUnanswered {unanswered} statusUnresponded {unresponded} other { {status} } } {type, select, Diskussion { Diskussionen } Frage {questions} all {posts} other { {type} } } {cohortType, select, all {} group {in {cohort} } other { {cohortType} } } sortiert nach {sort, select, lastActivityAt { Letzte Aktivität} commentCount {die meisten Aktivitäten} voteCount {die meisten Likes} other { {sort} } }"
}

View File

@@ -1,209 +0,0 @@
{
"discussions.actions.button.alt": "Menú de acciones",
"discussions.actions.copylink": "Copiar link",
"discussions.actions.edit": "Editar",
"discussions.actions.pin": "Marcar",
"discussions.actions.unpin": "Desmarcar",
"discussions.actions.delete": "Borrar",
"discussions.confirmation.button.confirm": "Confirmar",
"discussions.actions.close": "Cerrar",
"discussions.actions.reopen": "Reabrir",
"discussions.actions.report": "Informar",
"discussions.actions.unreport": "Dejar de denunciar",
"discussions.actions.endorse": "Validar",
"discussions.actions.unendorse": "Invalidar",
"discussions.actions.markAnswered": "Marcar como respondida",
"discussions.actions.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": "Pruebe eliminando algunos filtros.",
"discussions.empty.iconAlt": "Vacío",
"discussions.authors.label.staff": "Equipo del curso",
"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.",
"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",
"navigation.course.tabs.label": "Material del Curso",
"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, =0 {Pregunta} una {# Pregunta} otra {# Preguntas} }",
"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 activo {lastActiveTime}",
"discussions.learner.loadMostLearners": "Cargar más",
"discussions.learner.back": "Volver 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, Seleccionar , actividad {actividad reportada} {más actividad} otra {{sort}} }",
"discussion.learner.allActivity": "Toda la actividad",
"discussion.learner.posts": "Publicaciones",
"discussions.comments.comment.addComment": "Añadir comentario",
"discussions.comments.comment.addResponse": "Agregar una respuesta",
"discussions.comments.comment.abuseFlaggedMessage": "Contenido informado para que el personal lo revise",
"discussions.actions.back.alt": "Volver a la lista",
"discussions.comments.comment.responseCount": "{num, plural, =0 {Sin respuestas} uno {Mostrando # respuesta} otro {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 { grupo , Seleccionar , null {Todos} otros {{ grupo }} }.",
"discussions.comments.comment.postedTime": "{postType, Seleccionar , discusión { discusión } pregunta {Question} otro {{postType}} } publicado {relativeTime} por",
"discussions.comments.comment.commentTime": "Publicado {relativeTime}",
"discussions.comments.comment.answer": "Respuesta",
"discussions.comments.comment.answeredlabel": "Marcado como respondido por",
"discussions.comments.comment.endorsed": "respaldado",
"discussions.comments.comment.endorsedlabel": "Avalado por",
"discussions.actions.label": "Menú de acciones",
"discussions.editor.submit": "Enviar",
"discussions.editor.submitting": "Enviando",
"discussions.editor.cancel": "Cancelar",
"discussions.editor.error.empty": "El contenido de la publicación no puede estar vacío.",
"discussions.editor.delete.response.title": "Eliminar respuesta",
"discussions.editor.delete.response.description": "¿Está seguro de que desea eliminar esta respuesta de forma permanente?",
"discussions.editor.delete.comment.title": "Eliminar comentario",
"discussions.editor.delete.comment.description": "¿Estás seguro de que quieres eliminar este comentario de forma permanente?",
"discussions.delete.confirmation.button.delete": "Borrar",
"discussions.editor.response.response.title": "¿Denunciar contenido inapropiado?",
"discussions.editor.response.description": "El equipo de moderación de debates revisará este contenido y tomará las medidas adecuadas.",
"discussions.editor.report.comment.title": "¿Denunciar contenido inapropiado?",
"discussions.editor.report.comment.description": "El equipo de moderación de debates revisará este contenido y tomará las medidas adecuadas.",
"discussions.editor.comments.editReasonCode": "Razón de la edición",
"discussions.editor.posts.editReasonCode.error": "Seleccione el motivo de la edición",
"discussions.comment.comments.editedBy": "Editado por",
"discussions.comment.comments.fullStop": "•",
"discussions.comment.comments.reason": "Motivo",
"discussions.post.closedBy": "Publicación cerrada por",
"discussion.comment.time": "hace {time}",
"discussion.thread.notFound": "Hilo no encontrado",
"discussions.comment.sortFilterStatus": "{sort, Seleccionar , falso {El más antiguo primero} cierto {El más nuevo primero} other {{sort}} }",
"discussions.topics.sort.message": "Ordenado por {sortBy}",
"discussions.topics.sort.lastActivity": "Actividad reciente",
"discussions.topics.sort.commentCount": "La mayoría de la actividad",
"discussions.topics.sort.courseStructure": "Estructura del curso",
"discussions.topics.unnamed.label": "Categoría sin nombre",
"discussions.subtopics.unnamed.label": "Subcategoría sin nombre",
"tour.action.advance": "Siguiente",
"tour.action.dismiss": "Descartar",
"tour.action.end": "Okey",
"tour.example.title": "ejemplo Tour",
"tour.example.body": "Este es un recorrido ejemplo .",
"learn.course.tabs.navigation.overflow.menu": "Más...",
"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.post.author.anonymous": "anónimo",
"discussions.post.addResponse": "Añadir respuesta",
"discussions.post.lastResponse": "Última respuesta {time}",
"discussions.post.postedOn": "Publicado {time} por {author} {authorLabel}",
"discussions.post.contentReported": "Informado",
"discussions.post.following": "Siguiendo",
"discussions.post.follow": "Seguir",
"discussions.post.followed": "Seguido",
"discussions.post.notFollowed": "No seguido",
"discussions.post.answered": "Respondido",
"discussions.post.unFollow": "Dejar de seguir",
"discussions.post.like": "Me gusta",
"discussions.post.removeLike": "Dejar de gustar",
"discussions.post.liked": "Me gusta",
"discussions.post.likes": "Me gustan",
"discussions.post.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": "Escribe 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ás siguiendo esta publicación",
"discussions.post.unfollow.description": "No estás siguiendo esta publicación",
"discussions.app.title": "Debates",
"discussions.posts.actionBar.searchAllPosts": "Buscar en todas las publicaciones",
"discussions.posts.actionBar.search": "{página, Seleccionar , temas {Buscar temas} publicaciones {Buscar todas las publicaciones} alumnos {Buscar alumnos} myPosts {Buscar todas las publicaciones} otros {{page}} }",
"discussions.actionBar.searchInfo": "Mostrando resultados de {count} para &quot;{text}&quot;",
"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 post",
"discussions.post.editor.typeDescription": "Las preguntas plantean cuestiones que necesitan respuestas. En la sección \"Debates\" comparte ideas y comienza conversaciones.",
"discussions.post.editor.required": "Obligatorio",
"discussions.post.editor.questionType": "Pregunta",
"discussions.post.editor.questionDescription": "Plantear cuestiones que necesiten 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": "Agrega tu publicación a un tema relevante para ayudar 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": "Agrega 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": "Tu pregunta o idea (requerido)",
"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 para tus compañeros",
"discussions.editor.posts.editReasonCode": "Motivo de la edición",
"discussions.editor.posts.showPreview.button": "Mostrar vista previa",
"discussions.topic.noName.label": "Categoría sin nombre",
"discussions.subtopic.noName.label": "Subcategoría sin nombre",
"discussions.posts.filter.showALl": "Mostrar todo",
"discussions.posts.filter.discussions": "Debates\n",
"discussions.posts.filter.questions": "Preguntas",
"discussions.posts.filter.message": "Estado: {filterBy}",
"discussions.posts.status.filter.anyStatus": "Cualquier estado",
"discussions.posts.status.filter.unread": "Sin leer",
"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": "{propio, Seleccionar , falso {Todos} cierto {Propio} otro {{propio}} } {status, Seleccionar , statusAll {} statusUnread {unread} statusFollowing {followed} statusReported {reported} statusUnan respondió {sin respuesta} estado sin respuesta {sin respuesta} otro { {status}} } {type, Seleccionar , discusión { discusiones } pregunta {preguntas} todas las {publicaciones} other {{type}} } {cohortType, Seleccionar , all {} grupo { en { dividir en cohortes }} otro {{cohortType}} } ordenado por {ordenar, Seleccionar , lastActivityAt {actividad reciente} commentCount {más actividad} voteCount {más me gusta} otros {{sort}} }"
}

View File

@@ -1,209 +0,0 @@
{
"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",
"navigation.course.tabs.label": "Material del curso",
"discussions.topics.backAlt": "Volver a la lista de temas",
"discussions.topics.discussions": "{count, plural, =0 {Discusión} uno {# Discusión} otro {# Discusiones} }",
"discussions.topics.questions": "{count, plural, =0 {Pregunta} una {# Pregunta} otra {# 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 {ordenar, seleccionar, marcar {actividad reportada} actividad {más actividad} otro { {sort} } }",
"discussion.learner.allActivity": "Toda la actividad",
"discussion.learner.posts": "Publicaciones",
"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 # respuesta} 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} otros { {group} } }.",
"discussions.comments.comment.postedTime": "{postType, select, discusión {Discusión} pregunta {Pregunta} otro { {postType} } } publicado {relativeTime} por",
"discussions.comments.comment.commentTime": "Publicado {relativeTime}",
"discussions.comments.comment.answer": "Respuesta",
"discussions.comments.comment.answeredlabel": "Marcado como respondido por",
"discussions.comments.comment.endorsed": "Respaldado",
"discussions.comments.comment.endorsedlabel": "Avalado por",
"discussions.actions.label": "Menú de acciones",
"discussions.editor.submit": "Enviar",
"discussions.editor.submitting": "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, false {El más antiguo primero} true {El más nuevo primero} other { {sort} } }",
"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.example.title": "Tour de ejemplo",
"tour.example.body": "Este es un recorrido de ejemplo.",
"learn.course.tabs.navigation.overflow.menu": "Más...",
"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.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.app.title": "Debates",
"discussions.posts.actionBar.searchAllPosts": "Buscar todas las publicaciones",
"discussions.posts.actionBar.search": "{página, seleccionar, temas {Buscar temas} publicaciones {Buscar todas las publicaciones} alumnos {Buscar alumnos} myPosts {Buscar todas las publicaciones} 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": "{propio, seleccionar, falso {Todos} verdadero {Propio} otro { {own} } } {status, select, statusAll {} statusUnread {no leído} statusSeguimiento {seguido} statusReported {reportado} statusUnanswered {sin responder} statusUnresponded {sin responder} otro { {status} } } {escribir, seleccionar, discusión {discusiones} pregunta {preguntas} todas las {publicaciones} otras { {type} } } {cohortType, seleccionar, todas {} grupo {en {cohort} } otras { {cohortType} } } ordenado por {sort, select, lastActivityAt { actividad reciente} commentCount {más actividad} voteCount {más me gusta} otro { {sort} } }"
}

Some files were not shown because too many files have changed in this diff Show More