Compare commits
30 Commits
bilalqamar
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a2fa404b | ||
|
|
472bbe2d96 | ||
|
|
dc5f097736 | ||
|
|
5e8c8254b4 | ||
|
|
0d6692cf8c | ||
|
|
3391e966f3 | ||
|
|
4297a96102 | ||
|
|
db883ca7cd | ||
|
|
422fbf6173 | ||
|
|
e862ee6fb1 | ||
|
|
eeae6d45ce | ||
|
|
71b88bcea3 | ||
|
|
c808069fe1 | ||
|
|
b9543c6d9c | ||
|
|
a545d0b9f6 | ||
|
|
8d86e6dcc0 | ||
|
|
37781566f5 | ||
|
|
50948acfeb | ||
|
|
4de1011780 | ||
|
|
d7474782b4 | ||
|
|
e1c78dda6e | ||
|
|
f282da52c1 | ||
|
|
d7fcc86847 | ||
|
|
c0873df575 | ||
|
|
12fbe7eebd | ||
|
|
7db4fde252 | ||
|
|
4914f51b6e | ||
|
|
80073e3f83 | ||
|
|
3aacdda7a1 | ||
|
|
1a2068d52f |
@@ -2,3 +2,4 @@ coverage/*
|
||||
dist/
|
||||
node_modules/
|
||||
jest.config.js
|
||||
src/i18n/messages/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig(
|
||||
'eslint',
|
||||
|
||||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
@@ -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
|
||||
14
Makefile
14
Makefile
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
20734
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
23
src/components/Head/Head.jsx
Normal file
23
src/components/Head/Head.jsx
Normal 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);
|
||||
20
src/components/Head/Head.test.jsx
Normal file
20
src/components/Head/Head.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
11
src/components/Head/messages.js
Normal file
11
src/components/Head/messages.js
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useWindowSize } from '@edx/paragon';
|
||||
import { useWindowSize } from '@openedx/paragon';
|
||||
|
||||
const invisibleStyle = {
|
||||
position: 'absolute',
|
||||
|
||||
74
src/components/PostHelpPanel.jsx
Normal file
74
src/components/PostHelpPanel.jsx
Normal 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);
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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} } }"
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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 "{text}" werden angezeigt",
|
||||
"discussions.actionBar.searchRewriteInfo": "Keine Ergebnisse gefunden für "{searchString}". {count} Ergebnisse für "{textSearchRewrite}" 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} } }"
|
||||
}
|
||||
@@ -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 "{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 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}} }"
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user