Compare commits
39 Commits
sarina/upd
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7424b60a90 | ||
|
|
1c0e6fd4b5 | ||
|
|
bc3faa4105 | ||
|
|
a94942a36e | ||
|
|
ab7c51994c | ||
|
|
67967a92cf | ||
|
|
6efa8c5356 | ||
|
|
c28669f5b2 | ||
|
|
270f4a8a12 | ||
|
|
641a169e6f | ||
|
|
25e254bbfb | ||
|
|
af0ddf532a | ||
|
|
eaf76c8dee | ||
|
|
5c0ca7b706 | ||
|
|
530b247c33 | ||
|
|
a5bc86e948 | ||
|
|
9910937269 | ||
|
|
1344c289df | ||
|
|
7f4111c12c | ||
|
|
105fdea8ef | ||
|
|
9d91e3f242 | ||
|
|
fdcb3a5e7f | ||
|
|
86974b76a9 | ||
|
|
835915750c | ||
|
|
fe8a125d1a | ||
|
|
f82e572ad2 | ||
|
|
8aa03496fb | ||
|
|
3c2c347bb9 | ||
|
|
0d166288cc | ||
|
|
f8954ef870 | ||
|
|
66afd4ddac | ||
|
|
a99eb8a44a | ||
|
|
b2981318b0 | ||
|
|
5142f3afd4 | ||
|
|
b7b3601337 | ||
|
|
50e5ca86c6 | ||
|
|
fe9a9a37e7 | ||
|
|
1c5ab42ea6 | ||
|
|
74fcbe426d |
1
.env
1
.env
@@ -28,3 +28,4 @@ ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||
ENABLE_TEAM_TYPE_SETTING=false
|
||||
ENABLE_NEW_EDITOR_PAGES=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
|
||||
|
||||
@@ -30,3 +30,4 @@ ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||
ENABLE_TEAM_TYPE_SETTING=false
|
||||
ENABLE_NEW_EDITOR_PAGES=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
|
||||
|
||||
@@ -29,3 +29,4 @@ ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||
ENABLE_TEAM_TYPE_SETTING=false
|
||||
ENABLE_NEW_EDITOR_PAGES=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint',
|
||||
module.exports = createConfig(
|
||||
'eslint',
|
||||
{
|
||||
rules: {
|
||||
'jsx-a11y/label-has-associated-control': [2, {
|
||||
@@ -9,5 +11,7 @@ module.exports = createConfig('eslint',
|
||||
'template-curly-spacing': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
indent: 'off',
|
||||
'no-restricted-exports': 'off',
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
|
||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Update Browserslist DB
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-browserslist:
|
||||
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
|
||||
|
||||
secrets:
|
||||
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}
|
||||
11
.github/workflows/validate.yml
vendored
11
.github/workflows/validate.yml
vendored
@@ -9,14 +9,13 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
- run: make validate.ci
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
2
Makefile
2
Makefile
@@ -5,8 +5,6 @@ transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
33041
package-lock.json
generated
33041
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,11 +34,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-build": "^11.0.0",
|
||||
"@edx/frontend-component-footer": "11.1.1",
|
||||
"@edx/frontend-lib-content-components": "^1.72.0",
|
||||
"@edx/frontend-lib-content-components": "^1.131.0",
|
||||
"@edx/frontend-platform": "2.5.1",
|
||||
"@edx/paragon": "^20.21.0",
|
||||
"@edx/paragon": "^20.38.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
||||
"@fortawesome/free-brands-svg-icons": "5.11.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.11.2",
|
||||
@@ -54,6 +53,7 @@
|
||||
"prop-types": "15.7.2",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-redux": "7.1.3",
|
||||
"react-responsive": "8.1.0",
|
||||
"react-router": "5.1.2",
|
||||
@@ -66,7 +66,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.0.0",
|
||||
"@edx/frontend-build": "^11.0.0",
|
||||
"@edx/frontend-build": "12.8.38",
|
||||
"@edx/reactifex": "^1.0.3",
|
||||
"@testing-library/jest-dom": "5.16.4",
|
||||
"@testing-library/react": "12.1.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Course Authoring | edX</title>
|
||||
<title>Course Authoring | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%= process.env.FAVICON_URL %>" type="image/x-icon" />
|
||||
|
||||
@@ -14,7 +14,36 @@ import { getCourseAppsApiStatus, getLoadingStatus } from './pages-and-resources/
|
||||
import { RequestStatus } from './data/constants';
|
||||
import Loading from './generic/Loading';
|
||||
|
||||
export default function CourseAuthoringPage({ courseId, children }) {
|
||||
const AppHeader = ({
|
||||
courseNumber, courseOrg, courseTitle, courseId,
|
||||
}) => (
|
||||
<Header
|
||||
courseNumber={courseNumber}
|
||||
courseOrg={courseOrg}
|
||||
courseTitle={courseTitle}
|
||||
courseId={courseId}
|
||||
/>
|
||||
);
|
||||
|
||||
AppHeader.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
courseNumber: PropTypes.string,
|
||||
courseOrg: PropTypes.string,
|
||||
courseTitle: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
AppHeader.defaultProps = {
|
||||
courseNumber: null,
|
||||
courseOrg: null,
|
||||
};
|
||||
|
||||
const AppFooter = () => (
|
||||
<div className="mt-6">
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,33 +64,26 @@ export default function CourseAuthoringPage({ courseId, children }) {
|
||||
);
|
||||
}
|
||||
|
||||
const AppHeader = () => (
|
||||
<Header
|
||||
courseNumber={courseNumber}
|
||||
courseOrg={courseOrg}
|
||||
courseTitle={courseTitle}
|
||||
courseId={courseId}
|
||||
/>
|
||||
);
|
||||
|
||||
const AppFooter = () => (
|
||||
<div className="mt-6">
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={pathname.includes('/editor/') ? '' : 'bg-light-200'}>
|
||||
{/* While V2 Editors are tempoarily served from thier own pages
|
||||
using url pattern containing /editor/,
|
||||
we shouldn't have the header and footer on these pages.
|
||||
This functionality will be removed in TNL-9591 */}
|
||||
{inProgress ? !pathname.includes('/editor/') && <Loading /> : <AppHeader />}
|
||||
{inProgress ? !pathname.includes('/editor/') && <Loading />
|
||||
: (
|
||||
<AppHeader
|
||||
courseNumber={courseNumber}
|
||||
courseOrg={courseOrg}
|
||||
courseTitle={courseTitle}
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
{!inProgress && <AppFooter />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseAuthoringPage.propTypes = {
|
||||
children: PropTypes.node,
|
||||
@@ -71,3 +93,5 @@ CourseAuthoringPage.propTypes = {
|
||||
CourseAuthoringPage.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
export default CourseAuthoringPage;
|
||||
|
||||
@@ -23,7 +23,7 @@ import EditorContainer from './editors/EditorContainer';
|
||||
* can move the Header/Footer rendering to this component and likely pull the course detail loading
|
||||
* in as well, and it'd feel a bit better-factored and the roles would feel more clear.
|
||||
*/
|
||||
export default function CourseAuthoringRoutes({ courseId }) {
|
||||
const CourseAuthoringRoutes = ({ courseId }) => {
|
||||
const { path } = useRouteMatch();
|
||||
return (
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
@@ -45,8 +45,10 @@ export default function CourseAuthoringRoutes({ courseId }) {
|
||||
</Switch>
|
||||
</CourseAuthoringPage>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseAuthoringRoutes.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseAuthoringRoutes;
|
||||
|
||||
@@ -56,7 +56,7 @@ const CollapsableEditor = ({
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body rounded px-0">{children}</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
);
|
||||
|
||||
CollapsableEditor.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
|
||||
@@ -28,7 +28,7 @@ const ConfirmationPopup = ({
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
);
|
||||
|
||||
ConfirmationPopup.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
@@ -5,23 +5,21 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function ConnectionErrorAlert({ intl }) {
|
||||
return (
|
||||
<Alert variant="danger" data-testid="connectionErrorAlert">
|
||||
<FormattedMessage
|
||||
id="authoring.alert.error.connection"
|
||||
defaultMessage="We encountered a technical error when loading this page. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help."
|
||||
values={{
|
||||
const ConnectionErrorAlert = ({ intl }) => (
|
||||
<Alert variant="danger" data-testid="connectionErrorAlert">
|
||||
<FormattedMessage
|
||||
id="authoring.alert.error.connection"
|
||||
defaultMessage="We encountered a technical error when loading this page. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help."
|
||||
values={{
|
||||
supportLink: (
|
||||
<Alert.Link href={getConfig().SUPPORT_URL}>
|
||||
{intl.formatMessage(messages.supportText)}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
ConnectionErrorAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -2,38 +2,36 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form, TransitionReplace } from '@edx/paragon';
|
||||
|
||||
function FieldFeedback({
|
||||
const FieldFeedback = ({
|
||||
feedbackClasses,
|
||||
transitionClasses,
|
||||
errorCondition,
|
||||
feedbackCondition,
|
||||
feedbackMessage,
|
||||
errorMessage,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<TransitionReplace className={transitionClasses}>
|
||||
{feedbackCondition ? (
|
||||
<React.Fragment key="open1">
|
||||
<Form.Control.Feedback type="default" hasIcon={false} key={`${feedbackMessage}-feedback`}>
|
||||
<div className={`small ${feedbackClasses}`}>{feedbackMessage}</div>
|
||||
</Form.Control.Feedback>
|
||||
</React.Fragment>
|
||||
}) => (
|
||||
<>
|
||||
<TransitionReplace className={transitionClasses}>
|
||||
{feedbackCondition ? (
|
||||
<React.Fragment key="open1">
|
||||
<Form.Control.Feedback type="default" hasIcon={false} key={`${feedbackMessage}-feedback`}>
|
||||
<div className={`small ${feedbackClasses}`}>{feedbackMessage}</div>
|
||||
</Form.Control.Feedback>
|
||||
</React.Fragment>
|
||||
) : <React.Fragment key="close1" />}
|
||||
</TransitionReplace>
|
||||
</TransitionReplace>
|
||||
|
||||
<TransitionReplace>
|
||||
{errorCondition ? (
|
||||
<React.Fragment key="open">
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false} key={`${errorMessage}-feedback`}>
|
||||
<div className={`small ${feedbackClasses}`}>{errorMessage}</div>
|
||||
</Form.Control.Feedback>
|
||||
</React.Fragment>
|
||||
<TransitionReplace>
|
||||
{errorCondition ? (
|
||||
<React.Fragment key="open">
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false} key={`${errorMessage}-feedback`}>
|
||||
<div className={`small ${feedbackClasses}`}>{errorMessage}</div>
|
||||
</Form.Control.Feedback>
|
||||
</React.Fragment>
|
||||
) : <React.Fragment key="close" />}
|
||||
</TransitionReplace>
|
||||
</>
|
||||
</TransitionReplace>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
FieldFeedback.propTypes = {
|
||||
errorCondition: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Form, SwitchControl } from '@edx/paragon';
|
||||
|
||||
import './FormSwitchGroup.scss';
|
||||
|
||||
export default function FormSwitchGroup({
|
||||
const FormSwitchGroup = ({
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
@@ -14,7 +14,7 @@ export default function FormSwitchGroup({
|
||||
onBlur,
|
||||
checked,
|
||||
disabled,
|
||||
}) {
|
||||
}) => {
|
||||
const helpTextId = `${id}HelpText`;
|
||||
|
||||
// Note that we use controlId here _and_ set some IDs and aria-describedby attributes manually.
|
||||
@@ -49,7 +49,7 @@ export default function FormSwitchGroup({
|
||||
</div>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
};
|
||||
FormSwitchGroup.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
@@ -67,3 +67,5 @@ FormSwitchGroup.defaultProps = {
|
||||
name: null,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default FormSwitchGroup;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { Form } from '@edx/paragon';
|
||||
import { getIn, useFormikContext } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FormikErrorFeedback from './FormikErrorFeedback';
|
||||
|
||||
function FormikControl({
|
||||
const FormikControl = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
className,
|
||||
...params
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
touched, errors, handleChange, handleBlur, setFieldError,
|
||||
} = useFormikContext();
|
||||
@@ -35,7 +36,7 @@ function FormikControl({
|
||||
</FormikErrorFeedback>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
FormikControl.propTypes = {
|
||||
name: PropTypes.element.isRequired,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getIn, useFormikContext } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
function FormikErrorFeedback({ name, children }) {
|
||||
const FormikErrorFeedback = ({ name, children }) => {
|
||||
const { touched, errors } = useFormikContext();
|
||||
const fieldTouched = getIn(touched, name);
|
||||
const fieldError = getIn(errors, name);
|
||||
@@ -23,7 +23,7 @@ function FormikErrorFeedback({ name, children }) {
|
||||
)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
FormikErrorFeedback.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
|
||||
@@ -2,28 +2,28 @@ import React from 'react';
|
||||
import { Spinner } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={{
|
||||
const Loading = () => (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={{
|
||||
height: '50vh',
|
||||
}}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
role="status"
|
||||
variant="primary"
|
||||
screenReaderText={(
|
||||
<span className="sr-only">
|
||||
<FormattedMessage
|
||||
id="authoring.loading"
|
||||
defaultMessage="Loading..."
|
||||
description="Screen-reader message for when a page is loading."
|
||||
/>
|
||||
</span>
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
role="status"
|
||||
variant="primary"
|
||||
screenReaderText={(
|
||||
<span className="sr-only">
|
||||
<FormattedMessage
|
||||
id="authoring.loading"
|
||||
defaultMessage="Loading..."
|
||||
description="Screen-reader message for when a page is loading."
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Loading;
|
||||
|
||||
@@ -2,15 +2,13 @@ import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
function PermissionDeniedAlert() {
|
||||
return (
|
||||
<Alert variant="danger" data-testid="permissionDeniedAlert">
|
||||
<FormattedMessage
|
||||
id="authoring.alert.error.permission"
|
||||
defaultMessage="You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access."
|
||||
/>
|
||||
</Alert>
|
||||
const PermissionDeniedAlert = () => (
|
||||
<Alert variant="danger" data-testid="permissionDeniedAlert">
|
||||
<FormattedMessage
|
||||
id="authoring.alert.error.permission"
|
||||
defaultMessage="You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access."
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default PermissionDeniedAlert;
|
||||
|
||||
@@ -5,23 +5,21 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function SaveFormConnectionErrorAlert({ intl }) {
|
||||
return (
|
||||
<Alert variant="danger" data-testid="connectionErrorAlert">
|
||||
<FormattedMessage
|
||||
id="authoring.alert.save.error.connection"
|
||||
defaultMessage="We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help."
|
||||
values={{
|
||||
const SaveFormConnectionErrorAlert = ({ intl }) => (
|
||||
<Alert variant="danger" data-testid="connectionErrorAlert">
|
||||
<FormattedMessage
|
||||
id="authoring.alert.save.error.connection"
|
||||
defaultMessage="We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help."
|
||||
values={{
|
||||
supportLink: (
|
||||
<Alert.Link href={getConfig().SUPPORT_URL}>
|
||||
{intl.formatMessage(messages.supportText)}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
SaveFormConnectionErrorAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
21
src/head/Head.jsx
Normal file
21
src/head/Head.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const Head = ({ intl }) => (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['course-authoring.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);
|
||||
17
src/head/Head.test.jsx
Normal file
17
src/head/Head.test.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { mount } from 'enzyme';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import Head from './Head';
|
||||
|
||||
describe('Head', () => {
|
||||
const props = {};
|
||||
it('should match render title tag and favicon with the site configuration values', () => {
|
||||
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
|
||||
const helmet = Helmet.peek();
|
||||
expect(helmet.title).toEqual(`Course Authoring | ${getConfig().SITE_NAME}`);
|
||||
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
|
||||
expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
|
||||
});
|
||||
});
|
||||
11
src/head/messages.js
Normal file
11
src/head/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'course-authoring.page.title': {
|
||||
id: 'course-authoring.page.title',
|
||||
defaultMessage: 'Course Authoring | {siteName}',
|
||||
description: 'Title tag',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "التحميل جارٍ...",
|
||||
"authoring.alert.error.permission": "لست مخوّلا بمشاهدة هذه الصفحة. إن كنت ترى أن لديك حقًا في ذلك، فيرجى التواصل مع مدير فريق مساقك ليمنحك حق الوصول.",
|
||||
"authoring.alert.save.error.connection": "واجهنا خطأ تقنيًا أثناء تطبيق التغييرات. قد تكون هذه مشكلة عارضة، لذا يرجى المحاولة مجددًا خلال بضع دقائق. إن استمرت المشكلة فيرجى الذهاب إلى {support_link} للحصول على المساعدة.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "صفحة الدعم",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "إلغاء",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "حفظ",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "ابدأ مناقشات مع متعلمين آخرين، اطرح أسئلة، و تفاعل مع المتعلمين في المساق.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "ابدأ مناقشات مع متعلمين آخرين، اطرح أسئلة، و تفاعل مع المتعلمين في المساق.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza مصمم لربط الطلبة، الأساتذة، و الأساتذة المساعدين بما يمكن كل طالب من الحصول على المساعدة التي يحتاجها في حينها.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig يقدم للمعلمين حلاً رقميًا تعليميًا ممتعًا لتحسين مشاركة الطلاب من خلال بناء مجتمعات متعلمين لأي نمط مساق. ",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Loading...",
|
||||
"authoring.alert.error.permission": "You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.",
|
||||
"authoring.alert.save.error.connection": "We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Support Page",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Cancel",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Save",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offers educators a gameful learning digital solution to improve student engagement by building learning communities for any course modality.",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Cargando...",
|
||||
"authoring.alert.error.permission": "No te encuentras autorizado para ingresar a esta página. Si crees que deberías tener acceso, por favor contacta al equipo administrativo del curso para solicitar acceso.",
|
||||
"authoring.alert.save.error.connection": "Hemos detectado un error técnico al cargar esta página. Esto puede ser un problema temporal, así que por favor intente nuevamente en unos minutos. Si el problema persiste, por favor solicite ayuda en el siguiente enlace {supportLink}",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Página de soporte",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Cancelar",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Guardar",
|
||||
@@ -42,12 +43,12 @@
|
||||
"authoring.discussions.configure": "Configura discusiones",
|
||||
"authoring.discussions.ok": "OK",
|
||||
"authoring.discussions.cancel": "Cancelar",
|
||||
"authoring.discussions.confirm": "Confirm",
|
||||
"authoring.discussions.confirm": "Confirmar",
|
||||
"authoring.discussions.confirmConfigurationChange": "¿Estas seguro que deseas cambiar la configuración de las discusiones? ",
|
||||
"authoring.discussions.confirmEnableDiscussionsLabel": "Enable discussions on units in graded subsections?",
|
||||
"authoring.discussions.cancelEnableDiscussionsLabel": "Disable discussions on units in graded subsections?",
|
||||
"authoring.discussions.confirmEnableDiscussions": "Enabling this toggle will automatically enable discussion on all units in graded subsections, that are not timed exams.",
|
||||
"authoring.discussions.cancelEnableDiscussions": "Disabling this toggle will automatically disable discussion on all units in graded subsections. Discussion topics containing at least 1 thread will be listed and accessible under “Archived” in Topics tab on the Discussions page.",
|
||||
"authoring.discussions.confirmEnableDiscussionsLabel": "¿Habilitar debates sobre unidades en subsecciones calificadas?",
|
||||
"authoring.discussions.cancelEnableDiscussionsLabel": "¿Deshabilitar debates sobre unidades en subsecciones calificadas?",
|
||||
"authoring.discussions.confirmEnableDiscussions": "Habilitar esta opción habilitará automáticamente la discusión sobre todas las unidades en las subsecciones calificadas, que no son exámenes cronometrados.",
|
||||
"authoring.discussions.cancelEnableDiscussions": "Al deshabilitar esta opción, se deshabilitará automáticamente la discusión en todas las unidades en las subsecciones calificadas. Los temas de debate que contengan al menos 1 hilo se enumerarán y estarán accesibles en \"Archivado\" en la pestaña Temas en la página de Debates.",
|
||||
"authoring.discussions.backButton": "Volver atrás",
|
||||
"authoring.discussions.saveButton": "Guardar",
|
||||
"authoring.discussions.savingButton": "Guardando",
|
||||
@@ -66,8 +67,8 @@
|
||||
"authoring.discussions.builtIn.divideCourseTopicsByCohorts.help": "Escoge cuales de los temas de tus discusiones de todo el cursos te gustaría dividir.",
|
||||
"authoring.discussions.builtIn.divideGeneralTopic.label": "General",
|
||||
"authoring.discussions.builtIn.divideQuestionsForTAsTopic.label": "Preguntas para las herramientas asistidas",
|
||||
"authoring.discussions.builtIn.cohortsEnabled.label": "To adjust these settings, enable cohorts on the ",
|
||||
"authoring.discussions.builtIn.instructorDashboard.label": "instructor dashboard",
|
||||
"authoring.discussions.builtIn.cohortsEnabled.label": "Para ajustar esta configuración, habilite las cohortes en la",
|
||||
"authoring.discussions.builtIn.instructorDashboard.label": "tablero del instructor",
|
||||
"authoring.discussions.builtIn.visibilityInContext": "visualización de las discusioness en contexto",
|
||||
"authoring.discussions.builtIn.gradedUnitPages.label": "Habilitar discusiones en unidades de subsecciones calificables",
|
||||
"authoring.discussions.builtIn.gradedUnitPages.help": "Permítele a los estudiantes participar con discusiones en todas las unidades de página calificables con excepción de los exámenes cronometrados.",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Inicia conversaciones con otros estudiantes, haz preguntas, e interactúa con otros estudiantes pertenecientes al curso.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Inicia conversaciones con otros estudiantes, haz preguntas, e interactúa con otros estudiantes pertenecientes al curso.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Habilitar la participación en temas de debate junto con el contenido del curso.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza está diseñada para conectar estudiantes, TAs, y profesores de tal forma que cada estudiante pueda obtener la ayuda necesaria en los momentos requeridos.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellodig ofrece a los educadores una solución de aprendizaje interactiva para mejorar la participación del estudiante mediante la construcción de comunidades para cualquier modalidad de curso.",
|
||||
@@ -215,8 +216,8 @@
|
||||
"authoring.live.appName-bigBlueButton": "GranBotónAzul",
|
||||
"authoring.live.requestPiiSharingEnableForBbb": "Esta configuración requerirá compartir los nombres de usuario de los alumnos y el equipo del curso con {provider}.",
|
||||
"authoring.live.piiSharingEnableHelpText": "Para habilitar esta función, comuníquese con el equipo de soporte de edX para habilitar el uso compartido de PII para este curso.",
|
||||
"authoring.live.freePlanMessage": "The free plan is pre-configured, and no additional configurations are required. By selecting the free plan, you are agreeing to Blindside Networks",
|
||||
"authoring.live.privacyPolicy": "Privacy Policy.",
|
||||
"authoring.live.freePlanMessage": "El plan gratuito está preconfigurado y no se requieren configuraciones adicionales. Al seleccionar el plan gratuito, acepta Blindside Networks",
|
||||
"authoring.live.privacyPolicy": "Política de privacidad.",
|
||||
"course-authoring.pages-resources.heading": "Páginas & Recursos",
|
||||
"course-authoring.pages-resources.resources.settings.button": "configuraciones",
|
||||
"course-authoring.pages-resources.viewLive.button": "Ver en vivo",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Chargement...",
|
||||
"authoring.alert.error.permission": "Vous n'êtes pas autorisé à afficher cette page. Si vous croyez que vous devriez avoir accès à cette page, veuillez contacter l'équipe administrative du cours pour obtenir la permission.",
|
||||
"authoring.alert.save.error.connection": "Nous avons rencontré une erreur technique lors de l'application des modifications. Cela peut être un problème temporaire, veuillez donc réessayer dans quelques minutes. Si le problème persiste, accédez à {support_link} pour obtenir de l'aide.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Page de support",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Annuler",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Enregistrer",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Démarrez des conversations avec d'autres apprenants, posez des questions et interagissez avec d'autres apprenants du cours.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Démarrez des conversations avec d'autres apprenants, posez des questions et interagissez avec d'autres apprenants du cours.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza est conçu pour connecter les étudiants, les assistants enseignants et les professeurs afin que chaque étudiant puisse obtenir l'aide dont il a besoin quand il en a besoin.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offre aux éducateurs une solution d'enseignement ludique pour augmenter l'engagement des étudiants en construisant des communautés d'apprentissage pour toutes les modalités de cours.",
|
||||
@@ -199,8 +200,8 @@
|
||||
"authoring.live.consumerSecret.required": "Le secret du consommateur est un champ obligatoire",
|
||||
"authoring.live.launchUrl": "URL de lancement",
|
||||
"authoring.live.launchUrl.required": "L'URL de lancement est un champ obligatoire",
|
||||
"authoring.live.launchEmail": "Lancer l'e-mail",
|
||||
"authoring.live.launchEmail.required": "L'e-mail de lancement est un champ obligatoire",
|
||||
"authoring.live.launchEmail": "Lancer l'e-mail",
|
||||
"authoring.live.launchEmail.required": "L'e-mail de lancement est un champ obligatoire",
|
||||
"authoring.live.provider.helpText": "Cette configuration nécessitera le partage du nom d'utilisateur et des courriels des apprenants et de l'équipe du cours avec {providerName}.",
|
||||
"authoring.live.requestPiiSharingEnable": "Cette configuration nécessitera le partage des noms d'utilisateur et des courriels des apprenants et de l'équipe du cours avec {provider}. Pour accéder à la configuration LTI pour {provider}, veuillez demander à votre coordinateur de projet edX d'activer le partage des PII pour ce cours.",
|
||||
"authoring.live.appDocInstructions.documentationLink": "Documentation générale",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Chargement...",
|
||||
"authoring.alert.error.permission": "Vous n'êtes pas autorisé à afficher cette page. Si vous croyez que vous devriez avoir accès à cette page, veuillez contacter l'équipe administrative du cours pour obtenir la permission.",
|
||||
"authoring.alert.save.error.connection": "Nous avons rencontré une erreur technique lors de l'application des modifications. Cela peut être un problème temporaire, veuillez donc réessayer dans quelques minutes. Si le problème persiste, accédez à {support_link} pour obtenir de l'aide.",
|
||||
"course-authoring.page.title": "Création de cours | {siteName}",
|
||||
"authoring.alert.support.text": "Page de support",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Annuler",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Sauvegarder",
|
||||
@@ -20,7 +21,7 @@
|
||||
"authoring.discussions.documentationPage": "Visitez la page de documentation de {name}",
|
||||
"authoring.discussions.formInstructions": "Complétez les champs ci-dessous pour configurer votre outil de discussion.",
|
||||
"authoring.discussions.consumerKey": "Clé du consommateur",
|
||||
"authoring.discussions.consumerKey.required": "La clé du consommateur est un champ obligatoire",
|
||||
"authoring.discussions.consumerKey.required": "Clé du consommateur est un champ obligatoire",
|
||||
"authoring.discussions.consumerSecret": "Secret du consommateur",
|
||||
"authoring.discussions.consumerSecret.required": "Le secret du consommateur est un champ obligatoire",
|
||||
"authoring.discussions.launchUrl": "URL de lancement",
|
||||
@@ -43,9 +44,9 @@
|
||||
"authoring.discussions.ok": "OK",
|
||||
"authoring.discussions.cancel": "Annuler",
|
||||
"authoring.discussions.confirm": "Confirmer",
|
||||
"authoring.discussions.confirmConfigurationChange": "Voulez-vous vraiment modifier les paramètres de discussion ?",
|
||||
"authoring.discussions.confirmEnableDiscussionsLabel": "Activer les discussions sur les unités dans les sous-sections notées ?",
|
||||
"authoring.discussions.cancelEnableDiscussionsLabel": "Désactiver les discussions sur les unités dans les sous-sections notées ?",
|
||||
"authoring.discussions.confirmConfigurationChange": "Voulez-vous vraiment modifier les paramètres de discussion?",
|
||||
"authoring.discussions.confirmEnableDiscussionsLabel": "Activer les discussions sur les unités dans les sous-sections notées?",
|
||||
"authoring.discussions.cancelEnableDiscussionsLabel": "Désactiver les discussions sur les unités dans les sous-sections notées?",
|
||||
"authoring.discussions.confirmEnableDiscussions": "L'activation de cette bascule activera automatiquement la discussion sur toutes les unités dans les sous-sections notées, qui ne sont pas des examens chronométrés.",
|
||||
"authoring.discussions.cancelEnableDiscussions": "La désactivation de cette bascule désactivera automatiquement la discussion sur toutes les unités dans les sous-sections notées. Les sujets de discussion contenant au moins 1 fil de discussion seront répertoriés et accessibles sous \"Archivés\" dans l'onglet Sujets de la page Discussions.",
|
||||
"authoring.discussions.backButton": "Retour",
|
||||
@@ -90,7 +91,7 @@
|
||||
"authoring.discussions.deleteButton": "Supprimer",
|
||||
"authoring.discussions.cancelButton": "Annuler",
|
||||
"authoring.discussions.discussionTopicDeletion.help": "EDUlib vous recommande de ne pas supprimer les sujets de discussion une fois que votre cours est en cours.",
|
||||
"authoring.discussions.discussionTopicDeletion.label": "Supprimer ce sujet ?",
|
||||
"authoring.discussions.discussionTopicDeletion.label": "Supprimer ce sujet?",
|
||||
"authoring.discussions.builtIn.renameGeneralTopic.label": "Renommez le sujet général",
|
||||
"authoring.discussions.generalTopicHelp.help": "Ceci est le sujet de discussion par défaut pour votre cours.",
|
||||
"authoring.discussions.builtIn.configureAdditionalTopic.label": "Configurez le sujet",
|
||||
@@ -100,15 +101,15 @@
|
||||
"authoring.discussions.builtIn.blackoutDates.help": "S'ils sont ajoutés, les apprenants ne pourront pas participer aux discussions entre ces dates.",
|
||||
"authoring.discussions.addBlackoutDatesButton": "Ajouter une plage de dates d'interdiction",
|
||||
"authoring.discussions.builtIn.configureBlackoutDates.label": "Configurer la plage de dates d'interdiction",
|
||||
"authoring.discussions.blackoutStartDate.help": "Entrez une date de début, par exemple le 12/10/2023.",
|
||||
"authoring.discussions.blackoutEndDate.help": "Entrez une date de fin, par exemple le 17/12/2023.",
|
||||
"authoring.discussions.blackoutStartDate.help": "Entrez une date de début, par exemple le 12/10/2023",
|
||||
"authoring.discussions.blackoutEndDate.help": "Entrez une date de fin, par exemple le 17/12/2023",
|
||||
"authoring.discussions.blackoutStartTime.help": "Entrez une heure de début, par exemple 09h00",
|
||||
"authoring.discussions.blackoutEndTime.help": "Entrez une heure de fin, par exemple 17h00",
|
||||
"authoring.discussions.activeBlackoutDatesDeletion.help": "Ces dates d'interdiction sont actuellement actives. Si elles sont supprimées, les apprenants pourront publier dans les discussions pendant ces dates. Êtes-vous sûr de vouloir continuer ?",
|
||||
"authoring.discussions.activeBlackoutDatesDeletion.help": "Ces dates d'interdiction sont actuellement actives. Si elles sont supprimées, les apprenants pourront publier dans les discussions pendant ces dates. Êtes-vous sûr de vouloir continuer?",
|
||||
"authoring.discussions.blackoutDatesDeletion.help": "Si supprimé, les apprenants pourront participer aux discussions pendant ces dates.",
|
||||
"authoring.discussions.completeBlackoutDatesDeletion.help": "Êtes-vous sûr de vouloir supprimer ces dates d'interdiction ?",
|
||||
"authoring.discussions.activeBlackoutDatesDeletion.label": "Supprimer les dates d'interdiction actives ?",
|
||||
"authoring.discussions.blackoutDatesDeletion.label": "Supprimer les dates d'interdiction ?",
|
||||
"authoring.discussions.completeBlackoutDatesDeletion.help": "Êtes-vous sûr de vouloir supprimer ces dates d'interdiction?",
|
||||
"authoring.discussions.activeBlackoutDatesDeletion.label": "Supprimer les dates d'interdiction actives?",
|
||||
"authoring.discussions.blackoutDatesDeletion.label": "Supprimer les dates d'interdiction?",
|
||||
"authoring.blackoutDates.delete": "Supprimer les dates d'interdiction",
|
||||
"authoring.blackoutDates.status": "{status}",
|
||||
"authoring.blackoutDates.startDate.required": "La date de début est un champ obligatoire",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Démarrez des conversations avec d'autres apprenants, posez des questions et interagissez avec d'autres apprenants du cours.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Entamez des conversations avec d'autres apprenants, posez des questions et interagissez avec d'autres apprenants du cours.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Activez la participation aux sujets de discussion parallèlement au contenu du cours.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza est conçu pour connecter les étudiants, les assistants enseignants et les professeurs afin que chaque étudiant puisse obtenir l'aide dont il a besoin quand il en a besoin.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offre aux éducateurs une solution d'enseignement ludique pour augmenter l'engagement des étudiants en construisant des communautés d'apprentissage pour toutes les modalités de cours.",
|
||||
@@ -166,7 +167,7 @@
|
||||
"authoring.discussions.featureName-primary-discussion-app-experience": "Application de discussion primaire",
|
||||
"authoring.discussions.featureName-question-&-discussion-support": "Support aux Questions et Discussions",
|
||||
"authoring.discussions.featureName-report/flag-content-to-moderators": "Signaler du contenu aux modérateurs",
|
||||
"authoring.discussions.featureName-research-data-events": "Recherche de données d'évènements",
|
||||
"authoring.discussions.featureName-research-data-events": "Recherche de données d'événements",
|
||||
"authoring.discussions.featureName-simplified-in-context-discussion": "Simplifié dans le contexte de la discussion",
|
||||
"authoring.discussions.featureName-user-mentions": "Mentions utilisatrices",
|
||||
"authoring.discussions.featureName-wcag-2.1": "Support WCAG 2.1",
|
||||
@@ -193,14 +194,14 @@
|
||||
"authoring.pagesAndResources.live.enableLive.link": "En savoir plus sur le direct",
|
||||
"authoring.live.selectProvider": "Sélectionnez un outil de visioconférence",
|
||||
"authoring.live.formInstructions": "Complétez les champs ci-dessous pour paramétrer votre outil de visioconférence.",
|
||||
"authoring.live.consumerKey": "La clé du consommateur",
|
||||
"authoring.live.consumerKey.required": "La clé du consommateur est un champ obligatoire",
|
||||
"authoring.live.consumerKey": "Clé du consommateur",
|
||||
"authoring.live.consumerKey.required": "Clé du consommateur est un champ obligatoire",
|
||||
"authoring.live.consumerSecret": "Secret du consommateur",
|
||||
"authoring.live.consumerSecret.required": "Le secret du consommateur est un champ obligatoire",
|
||||
"authoring.live.launchUrl": "URL de lancement",
|
||||
"authoring.live.launchUrl.required": "L'URL de lancement est un champ obligatoire",
|
||||
"authoring.live.launchEmail": "Lancer l'e-mail",
|
||||
"authoring.live.launchEmail.required": "L'e-mail de lancement est un champ obligatoire",
|
||||
"authoring.live.launchEmail": "Lancer le courriel",
|
||||
"authoring.live.launchEmail.required": "Le courriel de lancement est un champ obligatoire",
|
||||
"authoring.live.provider.helpText": "Cette configuration nécessitera le partage du nom d'utilisateur et des courriels des apprenants et de l'équipe du cours avec {providerName}.",
|
||||
"authoring.live.requestPiiSharingEnable": "Cette configuration nécessitera le partage des noms d'utilisateur et des courriels des apprenants et de l'équipe du cours avec {provider}. Pour accéder à la configuration LTI pour {provider}, veuillez demander à votre coordinateur de projet edX d'activer le partage des PII pour ce cours.",
|
||||
"authoring.live.appDocInstructions.documentationLink": "Documentation générale",
|
||||
@@ -274,11 +275,11 @@
|
||||
"authoring.pagesAndResources.teams.group.type.label": "Type",
|
||||
"authoring.pagesAndResources.teams.group.type.help": "Contrôlez qui peut voir, créer et rejoindre des équipes",
|
||||
"authoring.pagesAndResources.teams.group.types.open": "Ouvert",
|
||||
"authoring.pagesAndResources.teams.group.types.open.description": "Les apprenants peuvent créer, rejoindre, quitter et voir d'autres équipes.",
|
||||
"authoring.pagesAndResources.teams.group.types.open.description": "Les apprenants peuvent créer, rejoindre, quitter et voir d'autres équipes",
|
||||
"authoring.pagesAndResources.teams.group.types.public_managed": "Géré par le public",
|
||||
"authoring.pagesAndResources.teams.group.types.public_managed.description": "Seul le personnel du cours peut contrôler les équipes et les adhésions. Les apprenants peuvent voir les autres équipes.",
|
||||
"authoring.pagesAndResources.teams.group.types.private_managed": "Gestion privée",
|
||||
"authoring.pagesAndResources.teams.group.types.private_managed.description": "Seul le personnel du cours peut contrôler les équipes, les adhésions et voir les autres équipes.",
|
||||
"authoring.pagesAndResources.teams.group.types.private_managed.description": "Seul le personnel du cours peut contrôler les équipes, les adhésions et voir les autres équipes",
|
||||
"authoring.pagesAndResources.teams.group.maxSize.label": "Taille maximale de l'équipe (optionelle)",
|
||||
"authoring.pagesAndResources.teams.group.maxSize.help": "Outrepasser la taille maximale globale des équipes",
|
||||
"authoring.pagesAndResources.teams.addGroup.button": "Ajouter un groupe",
|
||||
@@ -287,7 +288,7 @@
|
||||
"authoring.pagesAndResources.teams.group.collapse": "Fermer l'éditeur du groupe",
|
||||
"authoring.pagesAndResources.teams.deleteGroup.initiateDelete": "Supprimer",
|
||||
"authoring.pagesAndResources.teams.deleteGroup.cancel-delete.button": "Annuler",
|
||||
"authoring.pagesAndResources.teams.deleteGroup.heading": "Supprimer ce groupe ?",
|
||||
"authoring.pagesAndResources.teams.deleteGroup.heading": "Supprimer ce groupe?",
|
||||
"authoring.pagesAndResources.teams.deleteGroup.body": "edX recommande de ne pas supprimer les groupes une fois que le cours a commencé.\nVos groupes ne seront plus visibles dans le LMS et les apprenants ne seront plus en mesure de quitter les équipes associées aux groupes supprimés.\nVeuillez retirer les apprenants des équipes avant de supprimer le groupe associé.",
|
||||
"authoring.pagesAndResources.teams.enableGroups.error.noGroupsFound.title": "Aucun groupe trouvé",
|
||||
"authoring.pagesAndResources.teams.enableGroups.error.noGroupsFound.message": "Ajouter un ou plusieurs groupes pour permettre les équipes.",
|
||||
@@ -296,15 +297,15 @@
|
||||
"course-authoring.pages-resources.wiki.enable-wiki.help": "Le wiki du cours peut être configuré en fonction des besoins de votre\ncours. Les utilisations courantes peuvent inclure le partage de réponses aux FAQ du cours, le partage\n d'informations de cours modifiables ou donner accès à des informations créées par\n les apprenants.",
|
||||
"course-authoring.pages-resources.wiki.enable-wiki.link": "Apprenez en plus sur le wiki",
|
||||
"course-authoring.pages-resources.wiki.enable-public-wiki.label": "Activer l'accès public au wiki",
|
||||
"course-authoring.pages-resources.wiki.enable-public-wiki.help": "Si activé, les utilisateurs edX peuvent afficher le wiki du cours même lorsqu'ils\n ne sont pas inscrits au cours.",
|
||||
"course-authoring.pages-resources.wiki.enable-public-wiki.help": "Si activé, les utilisateurs edX peuvent afficher le wiki du cours même lorsqu'ils\nne sont pas inscrits au cours.",
|
||||
"authoring.examsettings.enableproctoredexams.help": "Si coché, les examens surveillés seront permis dans votre cours.",
|
||||
"authoring.examsettings.allowoptout.label": "Autoriser l'exclusion des examens surveillés",
|
||||
"authoring.examsettings.allowoptout.help": "\n Si cette valeur est «Oui», les apprenants peuvent choisir de passer des examens surveillés sans surveillance.\n Si cette valeur est \"Non\", tous les apprenants doivent passer l'examen avec surveillance.\n ",
|
||||
"authoring.examsettings.allowoptout.help": "\n Si cette valeur est \"Oui\", les apprenants peuvent choisir de passer des examens surveillés sans surveillance.\n Si cette valeur est \"Non\", tous les apprenants doivent passer l'examen avec surveillance.\n",
|
||||
"authoring.examsettings.provider.label": "Fournisseur de service de surveillance",
|
||||
"authoring.examsettings.escalationemail.label": "Courriel d'escalade Proctortrack",
|
||||
"authoring.examsettings.escalationemail.help": "\n Requis si «proctortrack» est choisi comme votre fournisseur de surveillance. Entrez une adresse de courriel à\n contacter par l'équipe de support lorsqu'il y a des escalades (ex. appels, revues en retard, etc.).\n ",
|
||||
"authoring.examsettings.escalationemail.help": "\n Requis si «proctortrack» est choisi comme votre fournisseur de surveillance. Entrez une adresse de courriel à\ncontacter par l'équipe de support lorsqu'il y a des escalades (ex. appels, revues en retard, etc.).\n",
|
||||
"authoring.examsettings.createzendesk.label": "Créer des billets ZenDesk pour les tentatives d'examen surveillé suspectes",
|
||||
"authoring.examsettings.createzendesk.help": "Si cette valeur est «Oui», un billet ZenDesk sera créé pour les tentatives d'examen surveillé suspectes.",
|
||||
"authoring.examsettings.createzendesk.help": "Si cette valeur est \"Oui\", un billet ZenDesk sera créé pour les tentatives d'examen surveillé suspectes.",
|
||||
"authoring.examsettings.submit": "Soumettre",
|
||||
"authoring.examsettings.alert.success": "\n Paramètres d'examen sauvegardés avec succès.\n Vous pouvez retourner dans le Studio du cours {studioCourseRunURL}.\n ",
|
||||
"authoring.examsettings.allowoptout.no": "Non",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Loading...",
|
||||
"authoring.alert.error.permission": "You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.",
|
||||
"authoring.alert.save.error.connection": "We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Support Page",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Cancel",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Save",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offers educators a gameful learning digital solution to improve student engagement by building learning communities for any course modality.",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Loading...",
|
||||
"authoring.alert.error.permission": "You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.",
|
||||
"authoring.alert.save.error.connection": "We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Support Page",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Cancel",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Save",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offers educators a gameful learning digital solution to improve student engagement by building learning communities for any course modality.",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Loading...",
|
||||
"authoring.alert.error.permission": "You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.",
|
||||
"authoring.alert.save.error.connection": "We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Support Page",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Cancel",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Save",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offers educators a gameful learning digital solution to improve student engagement by building learning communities for any course modality.",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Loading...",
|
||||
"authoring.alert.error.permission": "You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.",
|
||||
"authoring.alert.save.error.connection": "We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Support Page",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Cancel",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Save",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offers educators a gameful learning digital solution to improve student engagement by building learning communities for any course modality.",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Loading...",
|
||||
"authoring.alert.error.permission": "You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.",
|
||||
"authoring.alert.save.error.connection": "We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Support Page",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Cancel",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Save",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offers educators a gameful learning digital solution to improve student engagement by building learning communities for any course modality.",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"authoring.loading": "Loading...",
|
||||
"authoring.alert.error.permission": "You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.",
|
||||
"authoring.alert.save.error.connection": "We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help.",
|
||||
"course-authoring.page.title": "Course Authoring | {siteName}",
|
||||
"authoring.alert.support.text": "Support Page",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.cancel": "Cancel",
|
||||
"course-authoring.pages-resources.app-settings-modal.button.save": "Save",
|
||||
@@ -140,7 +141,7 @@
|
||||
"authoring.discussions.appList.appName-legacy": "edX",
|
||||
"authoring.discussions.appList.appDescription-legacy": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appName-openedx": "edX",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Start conversations with other learners, ask questions, and interact with other learners in the course.",
|
||||
"authoring.discussions.appList.appDescription-openedx": "Enable participation in discussion topics alongside course content.",
|
||||
"authoring.discussions.appList.appName-piazza": "Piazza",
|
||||
"authoring.discussions.appList.appDescription-piazza": "Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.",
|
||||
"authoring.discussions.appList.appDescription-yellowdig": "Yellowdig offers educators a gameful learning digital solution to improve student engagement by building learning communities for any course modality.",
|
||||
|
||||
@@ -16,10 +16,12 @@ import appMessages from './i18n';
|
||||
import initializeStore from './store';
|
||||
import './index.scss';
|
||||
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
|
||||
import Head from './head/Head';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={initializeStore()}>
|
||||
<Head />
|
||||
<Switch>
|
||||
<Route
|
||||
path="/course/:courseId"
|
||||
|
||||
@@ -17,7 +17,7 @@ import { getLoadingStatus } from './data/selectors';
|
||||
import PagesAndResourcesProvider from './PagesAndResourcesProvider';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
|
||||
function PagesAndResources({ courseId, intl }) {
|
||||
const PagesAndResources = ({ courseId, intl }) => {
|
||||
const { path, url } = useRouteMatch();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@@ -34,6 +34,7 @@ function PagesAndResources({ courseId, intl }) {
|
||||
// Each page here is driven by a course app
|
||||
const pages = useModels('courseApps', courseAppIds);
|
||||
if (loadingStatus === RequestStatus.IN_PROGRESS) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ function PagesAndResources({ courseId, intl }) {
|
||||
</main>
|
||||
</PagesAndResourcesProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PagesAndResources.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const PagesAndResourcesContext = React.createContext({});
|
||||
|
||||
export default function PagesAndResourcesProvider({ courseId, children }) {
|
||||
const PagesAndResourcesProvider = ({ courseId, children }) => {
|
||||
const contextValue = useMemo(() => ({
|
||||
courseId,
|
||||
path: `/course/${courseId}/pages-and-resources`,
|
||||
}), []);
|
||||
return (
|
||||
<PagesAndResourcesContext.Provider
|
||||
value={{
|
||||
courseId,
|
||||
path: `/course/${courseId}/pages-and-resources`,
|
||||
}}
|
||||
value={contextValue}
|
||||
>
|
||||
{children}
|
||||
</PagesAndResourcesContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PagesAndResourcesProvider.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default PagesAndResourcesProvider;
|
||||
|
||||
@@ -33,21 +33,19 @@ import AppConfigFormDivider from '../discussions/app-config-form/apps/shared/App
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
import messages from './messages';
|
||||
|
||||
function AppSettingsForm({
|
||||
const AppSettingsForm = ({
|
||||
formikProps, children, showForm,
|
||||
}) {
|
||||
return children && (
|
||||
<TransitionReplace>
|
||||
{showForm ? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
}) => children && (
|
||||
<TransitionReplace>
|
||||
{showForm ? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="app-disabled" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
|
||||
AppSettingsForm.propTypes = {
|
||||
// Ignore the warning here since we're just passing along the props as-is and the child component should validate
|
||||
@@ -61,38 +59,36 @@ AppSettingsForm.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
function AppSettingsModalBase({
|
||||
const AppSettingsModalBase = ({
|
||||
intl, title, onClose, variant, isMobile, children, footer,
|
||||
}) {
|
||||
return (
|
||||
<ModalDialog
|
||||
title={title}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
size="lg"
|
||||
variant={variant}
|
||||
hasCloseButton={isMobile}
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title data-testid="modal-title">
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
{children}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="p-4">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</ModalDialog.CloseButton>
|
||||
{footer}
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
}) => (
|
||||
<ModalDialog
|
||||
title={title}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
size="lg"
|
||||
variant={variant}
|
||||
hasCloseButton={isMobile}
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title data-testid="modal-title">
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
{children}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="p-4">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</ModalDialog.CloseButton>
|
||||
{footer}
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
|
||||
AppSettingsModalBase.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
@@ -108,7 +104,7 @@ AppSettingsModalBase.defaultProps = {
|
||||
footer: null,
|
||||
};
|
||||
|
||||
function AppSettingsModal({
|
||||
const AppSettingsModal = ({
|
||||
intl,
|
||||
appId,
|
||||
title,
|
||||
@@ -122,7 +118,7 @@ function AppSettingsModal({
|
||||
enableAppHelp,
|
||||
learnMoreText,
|
||||
enableReinitialize,
|
||||
}) {
|
||||
}) => {
|
||||
const { courseId } = useContext(PagesAndResourcesContext);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const updateSettingsRequestStatus = useSelector(getSavingStatus);
|
||||
@@ -271,7 +267,7 @@ function AppSettingsModal({
|
||||
{loadingStatus === RequestStatus.DENIED && <PermissionDeniedAlert />}
|
||||
</AppSettingsModalBase>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AppSettingsModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
@@ -279,8 +275,8 @@ AppSettingsModal.propTypes = {
|
||||
appId: PropTypes.string.isRequired,
|
||||
children: PropTypes.func,
|
||||
onSettingsSave: PropTypes.func,
|
||||
initialValues: PropTypes.objectOf(PropTypes.any),
|
||||
validationSchema: PropTypes.objectOf(PropTypes.any),
|
||||
initialValues: PropTypes.shape({}),
|
||||
validationSchema: PropTypes.shape({}),
|
||||
onClose: PropTypes.func.isRequired,
|
||||
enableAppLabel: PropTypes.string.isRequired,
|
||||
enableAppHelp: PropTypes.string.isRequired,
|
||||
|
||||
@@ -6,18 +6,16 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
|
||||
function CalculatorSettings({ intl, onClose }) {
|
||||
return (
|
||||
<AppSettingsModal
|
||||
appId="calculator"
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
enableAppHelp={intl.formatMessage(messages.enableCalculatorHelp)}
|
||||
enableAppLabel={intl.formatMessage(messages.enableCalculatorLabel)}
|
||||
learnMoreText={intl.formatMessage(messages.enableCalculatorLink)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
const CalculatorSettings = ({ intl, onClose }) => (
|
||||
<AppSettingsModal
|
||||
appId="calculator"
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
enableAppHelp={intl.formatMessage(messages.enableCalculatorHelp)}
|
||||
enableAppLabel={intl.formatMessage(messages.enableCalculatorLabel)}
|
||||
learnMoreText={intl.formatMessage(messages.enableCalculatorLink)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
CalculatorSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const DiscussionsContext = React.createContext({});
|
||||
|
||||
export default function DiscussionsProvider({ children, path }) {
|
||||
const DiscussionsProvider = ({ children, path }) => {
|
||||
const contextValue = useMemo(() => ({ path }), []);
|
||||
return (
|
||||
<DiscussionsContext.Provider
|
||||
value={{
|
||||
path,
|
||||
}}
|
||||
value={contextValue}
|
||||
>
|
||||
{children}
|
||||
</DiscussionsContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionsProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DiscussionsProvider;
|
||||
|
||||
@@ -28,7 +28,7 @@ import Loading from '../../generic/Loading';
|
||||
const SELECTION_STEP = 'selection';
|
||||
const SETTINGS_STEP = 'settings';
|
||||
|
||||
function DiscussionsSettings({ courseId, intl }) {
|
||||
const DiscussionsSettings = ({ courseId, intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
|
||||
const { status, hasValidationError } = useSelector(state => state.discussions);
|
||||
@@ -145,7 +145,7 @@ function DiscussionsSettings({ courseId, intl }) {
|
||||
</AppConfigForm.Provider>
|
||||
</DiscussionsProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionsSettings.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
getConfig, history, initializeMockApp, setConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
@@ -42,6 +43,9 @@ let axiosMock;
|
||||
let store;
|
||||
let container;
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = jest.fn(node => node);
|
||||
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
<AppProvider store={store}>
|
||||
|
||||
@@ -28,9 +28,9 @@ import AppConfigFormProvider, { AppConfigFormContext } from './AppConfigFormProv
|
||||
import AppConfigFormSaveButton from './AppConfigFormSaveButton';
|
||||
import messages from './messages';
|
||||
|
||||
function AppConfigForm({
|
||||
const AppConfigForm = ({
|
||||
courseId, intl,
|
||||
}) {
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { formRef } = useContext(AppConfigFormContext);
|
||||
@@ -141,7 +141,7 @@ function AppConfigForm({
|
||||
</ModalDialog>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AppConfigForm.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const AppConfigFormContext = React.createContext({});
|
||||
|
||||
export default function AppConfigFormProvider({ children }) {
|
||||
const AppConfigFormProvider = ({ children }) => {
|
||||
const formRef = React.createRef();
|
||||
const contextValue = useMemo(() => ({ formRef }), []);
|
||||
|
||||
return (
|
||||
<AppConfigFormContext.Provider
|
||||
value={{
|
||||
formRef,
|
||||
}}
|
||||
value={contextValue}
|
||||
>
|
||||
{children}
|
||||
</AppConfigFormContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AppConfigFormProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default AppConfigFormProvider;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SAVING } from '../data/slice';
|
||||
import { AppConfigFormContext } from './AppConfigFormProvider';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function AppConfigFormSaveButton({ intl, labelText }) {
|
||||
const AppConfigFormSaveButton = ({ intl, labelText }) => {
|
||||
const saveStatus = useSelector(state => state.discussions.saveStatus);
|
||||
const { selectedAppId } = useSelector((state) => state.discussions);
|
||||
|
||||
@@ -45,7 +45,7 @@ function AppConfigFormSaveButton({ intl, labelText }) {
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AppConfigFormSaveButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useModel } from '../../../../../generic/model-store';
|
||||
|
||||
ensureConfig(['SITE_NAME', 'SUPPORT_EMAIL'], 'LTI Config Form');
|
||||
|
||||
function LtiConfigForm({ onSubmit, intl, formRef }) {
|
||||
const LtiConfigForm = ({ onSubmit, intl, formRef }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { selectedAppId, piiConfig } = useSelector((state) => state.discussions);
|
||||
@@ -177,7 +177,7 @@ function LtiConfigForm({ onSubmit, intl, formRef }) {
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LtiConfigForm.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -21,9 +21,9 @@ import OpenedXConfigFormProvider from './OpenedXConfigFormProvider';
|
||||
|
||||
setupYupExtensions();
|
||||
|
||||
function OpenedXConfigForm({
|
||||
const OpenedXConfigForm = ({
|
||||
onSubmit, formRef, intl, legacy,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
selectedAppId, enableGradedUnits, discussionTopicIds, divideDiscussionIds,
|
||||
} = useSelector(state => state.discussions);
|
||||
@@ -52,6 +52,7 @@ function OpenedXConfigForm({
|
||||
groupAtSubsection: Yup.bool().default(false),
|
||||
};
|
||||
const validationSchema = Yup.object().shape({
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
blackoutDates: Yup.array(
|
||||
Yup.object().shape({
|
||||
startDate: Yup.string()
|
||||
@@ -76,6 +77,7 @@ function OpenedXConfigForm({
|
||||
}),
|
||||
}),
|
||||
),
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
discussionTopics: Yup.array(
|
||||
Yup.object({
|
||||
name: Yup.string().required(intl.formatMessage(messages.discussionTopicRequired)),
|
||||
@@ -145,7 +147,7 @@ function OpenedXConfigForm({
|
||||
}}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
OpenedXConfigForm.propTypes = {
|
||||
legacy: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -212,7 +212,8 @@ describe('OpenedXConfigForm', () => {
|
||||
expect(container.querySelector('#reportedContentEmailNotifications')).toBeChecked();
|
||||
});
|
||||
|
||||
test('folded discussion topics are in the DOM when divideByCohorts and divideCourseWideTopics are enabled',
|
||||
test(
|
||||
'folded discussion topics are in the DOM when divideByCohorts and divideCourseWideTopics are enabled',
|
||||
async () => {
|
||||
await mockStore({
|
||||
...legacyApiResponse,
|
||||
@@ -235,7 +236,8 @@ describe('OpenedXConfigForm', () => {
|
||||
divideDiscussionIds.forEach(id => {
|
||||
expect(container.querySelector(`#checkbox-${id}`)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const updateTopicName = async (topicId, topicName) => {
|
||||
const topicCard = queryByTestId(container, topicId);
|
||||
@@ -263,7 +265,8 @@ describe('OpenedXConfigForm', () => {
|
||||
expect(store.getState().discussions.hasValidationError).toBe(expectExists);
|
||||
};
|
||||
|
||||
test('show required error on field when leaving empty topic name',
|
||||
test(
|
||||
'show required error on field when leaving empty topic name',
|
||||
async () => {
|
||||
await mockStore(legacyApiResponse);
|
||||
createComponent();
|
||||
@@ -272,7 +275,8 @@ describe('OpenedXConfigForm', () => {
|
||||
await waitForElementToBeRemoved(queryByText(topicCard, messages.addTopicHelpText.defaultMessage));
|
||||
assertTopicNameRequiredValidation(topicCard);
|
||||
assertHasErrorValidation();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('check field is not collapsible in case of error', async () => {
|
||||
await mockStore(legacyApiResponse);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { updateValidationStatus } from '../../../data/slice';
|
||||
|
||||
export const OpenedXConfigFormContext = createContext({});
|
||||
|
||||
export default function OpenedXConfigFormProvider({ children, value }) {
|
||||
const OpenedXConfigFormProvider = ({ children, value }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -17,7 +17,7 @@ export default function OpenedXConfigFormProvider({ children, value }) {
|
||||
{children}
|
||||
</OpenedXConfigFormContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
OpenedXConfigFormProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
@@ -33,3 +33,5 @@ OpenedXConfigFormProvider.propTypes = {
|
||||
isFormInvalid: PropTypes.bool,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default OpenedXConfigFormProvider;
|
||||
|
||||
@@ -5,27 +5,25 @@ import FormSwitchGroup from '../../../../../generic/FormSwitchGroup';
|
||||
import messages from '../../messages';
|
||||
import AppConfigFormDivider from './AppConfigFormDivider';
|
||||
|
||||
function AnonymousPostingFields({
|
||||
const AnonymousPostingFields = ({
|
||||
onBlur,
|
||||
onChange,
|
||||
intl,
|
||||
values,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-500">{intl.formatMessage(messages.anonymousPosting)}</h5>
|
||||
<AppConfigFormDivider />
|
||||
<FormSwitchGroup
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
id="allowAnonymousPostsPeers"
|
||||
checked={values.allowAnonymousPostsPeers}
|
||||
label={intl.formatMessage(messages.allowAnonymousPostsPeersLabel)}
|
||||
helpText={intl.formatMessage(messages.allowAnonymousPostsPeersHelp)}
|
||||
/>
|
||||
</>
|
||||
}) => (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-500">{intl.formatMessage(messages.anonymousPosting)}</h5>
|
||||
<AppConfigFormDivider />
|
||||
<FormSwitchGroup
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
id="allowAnonymousPostsPeers"
|
||||
checked={values.allowAnonymousPostsPeers}
|
||||
label={intl.formatMessage(messages.allowAnonymousPostsPeersLabel)}
|
||||
helpText={intl.formatMessage(messages.allowAnonymousPostsPeersHelp)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AnonymousPostingFields.propTypes = {
|
||||
onBlur: PropTypes.func.isRequired,
|
||||
|
||||
@@ -2,22 +2,20 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default function AppConfigFormDivider({ thick, marginAdj }) {
|
||||
return (
|
||||
<hr
|
||||
className={classNames(
|
||||
const AppConfigFormDivider = ({ thick, marginAdj }) => (
|
||||
<hr
|
||||
className={classNames(
|
||||
'my-2 mx-n4 border-light-300',
|
||||
{
|
||||
[`mx-sm-n${marginAdj.sm}`]: marginAdj.sm !== null,
|
||||
[`mx-n${marginAdj.default}`]: marginAdj.default !== null,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
style={{
|
||||
borderTopWidth: thick ? '3px' : '1px',
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
AppConfigFormDivider.propTypes = {
|
||||
thick: PropTypes.bool,
|
||||
@@ -34,3 +32,5 @@ AppConfigFormDivider.defaultProps = {
|
||||
sm: 5,
|
||||
},
|
||||
};
|
||||
|
||||
export default AppConfigFormDivider;
|
||||
|
||||
@@ -9,13 +9,13 @@ import AppConfigFormDivider from './AppConfigFormDivider';
|
||||
|
||||
import messages from '../lti/messages';
|
||||
|
||||
function AppExternalLinks({
|
||||
const AppExternalLinks = ({
|
||||
externalLinks,
|
||||
intl,
|
||||
providerName,
|
||||
showLaunchIcon,
|
||||
customClasses,
|
||||
}) {
|
||||
}) => {
|
||||
const { contactEmail, ...links } = externalLinks;
|
||||
const linkTypes = Object.keys(links).filter(key => links[key]);
|
||||
return (
|
||||
@@ -60,7 +60,7 @@ function AppExternalLinks({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AppExternalLinks.propTypes = {
|
||||
externalLinks: PropTypes.shape({
|
||||
|
||||
@@ -7,12 +7,12 @@ import messages from '../../messages';
|
||||
import AppConfigFormDivider from './AppConfigFormDivider';
|
||||
import ConfirmationPopup from '../../../../../generic/ConfirmationPopup';
|
||||
|
||||
function InContextDiscussionFields({
|
||||
const InContextDiscussionFields = ({
|
||||
onBlur,
|
||||
onChange,
|
||||
intl,
|
||||
values,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
setFieldValue,
|
||||
} = useFormikContext();
|
||||
@@ -63,7 +63,7 @@ function InContextDiscussionFields({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
InContextDiscussionFields.propTypes = {
|
||||
onBlur: PropTypes.func.isRequired,
|
||||
|
||||
@@ -5,7 +5,7 @@ import FormSwitchGroup from '../../../../../generic/FormSwitchGroup';
|
||||
import AppConfigFormDivider from './AppConfigFormDivider';
|
||||
import messages from '../../messages';
|
||||
|
||||
function ReportedContentEmailNotifications({ intl }) {
|
||||
const ReportedContentEmailNotifications = ({ intl }) => {
|
||||
const {
|
||||
handleChange,
|
||||
handleBlur,
|
||||
@@ -13,6 +13,7 @@ function ReportedContentEmailNotifications({ intl }) {
|
||||
} = useFormikContext();
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{values.enableReportedContentEmailNotifications && (
|
||||
<div>
|
||||
@@ -31,7 +32,7 @@ function ReportedContentEmailNotifications({ intl }) {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ReportedContentEmailNotifications.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -38,9 +38,7 @@ export const hasValidTimeFormat = (time) => time && moment(time, validTimeFormat
|
||||
export const startOfDayTime = (time) => time || moment().startOf('day').format('HH:mm');
|
||||
export const endOfDayTime = (time) => time || moment().endOf('day').format('HH:mm');
|
||||
export const normalizeTime = (time) => time && moment(time, validTimeFormats, true).format('HH:mm');
|
||||
export const normalizeDate = (date) => moment(
|
||||
date, ['MM/DD/YYYY', 'YYYY-MM-DDTHH:mm', 'YYYY-MM-DD'], true,
|
||||
).format('YYYY-MM-DD');
|
||||
export const normalizeDate = (date) => moment(date, ['MM/DD/YYYY', 'YYYY-MM-DDTHH:mm', 'YYYY-MM-DD'], true).format('YYYY-MM-DD');
|
||||
|
||||
export const decodeDateTime = (date, time) => {
|
||||
const nDate = normalizeDate(date);
|
||||
@@ -50,8 +48,11 @@ export const decodeDateTime = (date, time) => {
|
||||
};
|
||||
|
||||
export const sortBlackoutDatesByStatus = (data, status, order) => (
|
||||
_.orderBy(data.filter(date => date.status === status),
|
||||
[(obj) => decodeDateTime(obj.startDate, startOfDayTime(obj.startTime))], [order])
|
||||
_.orderBy(
|
||||
data.filter(date => date.status === status),
|
||||
[(obj) => decodeDateTime(obj.startDate, startOfDayTime(obj.startTime))],
|
||||
[order],
|
||||
)
|
||||
);
|
||||
|
||||
export const formatBlackoutDates = ({
|
||||
|
||||
@@ -11,9 +11,9 @@ import messages from './messages';
|
||||
import appMessages from '../app-config-form/messages';
|
||||
import FeaturesList from './FeaturesList';
|
||||
|
||||
function AppCard({
|
||||
const AppCard = ({
|
||||
app, onClick, intl, selected, features,
|
||||
}) {
|
||||
}) => {
|
||||
const { canChangeProviders } = useSelector(state => state.courseDetail);
|
||||
const supportText = app.hasFullSupport
|
||||
? intl.formatMessage(messages.appFullSupport)
|
||||
@@ -62,7 +62,7 @@ function AppCard({
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AppCard.propTypes = {
|
||||
app: PropTypes.shape({
|
||||
@@ -73,7 +73,7 @@ AppCard.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
features: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
features: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AppCard);
|
||||
|
||||
@@ -14,7 +14,7 @@ import FeaturesTable from './FeaturesTable';
|
||||
import AppListNextButton from './AppListNextButton';
|
||||
import Loading from '../../../generic/Loading';
|
||||
|
||||
function AppList({ intl }) {
|
||||
const AppList = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
@@ -90,7 +90,7 @@ function AppList({ intl }) {
|
||||
</Responsive>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AppList.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React from 'react';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DiscussionsContext } from '../DiscussionsProvider';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function AppListNextButton({ intl }) {
|
||||
const AppListNextButton = ({ intl }) => {
|
||||
const { selectedAppId } = useSelector(state => state.discussions);
|
||||
const { path: discussionsPath } = useContext(DiscussionsContext);
|
||||
|
||||
@@ -24,7 +24,7 @@ function AppListNextButton({ intl }) {
|
||||
{intl.formatMessage(messages.nextButton)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AppListNextButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -41,9 +41,7 @@ describe('FeaturesList', () => {
|
||||
const button = getByRole(container, 'button');
|
||||
userEvent.click(button);
|
||||
app.featureIds.forEach((id) => {
|
||||
const featureNodes = queryAllByText(
|
||||
container, messages[`featureName-${id}`].defaultMessage,
|
||||
);
|
||||
const featureNodes = queryAllByText(container, messages[`featureName-${id}`].defaultMessage);
|
||||
expect(featureNodes.map(node => node.closest('div'))).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,37 +6,35 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import SupportedFeature from './SupportedFeature';
|
||||
import messages from './messages';
|
||||
|
||||
function FeaturesList({ app, intl }) {
|
||||
return (
|
||||
<Collapsible
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
title={(
|
||||
<>
|
||||
<Collapsible.Visible whenClosed>
|
||||
{intl.formatMessage(messages['supportedFeatureList-mobile-show'])}
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
{intl.formatMessage(messages['supportedFeatureList-mobile-hide'])}
|
||||
</Collapsible.Visible>
|
||||
</>
|
||||
const FeaturesList = ({ app, intl }) => (
|
||||
<Collapsible
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
title={(
|
||||
<>
|
||||
<Collapsible.Visible whenClosed>
|
||||
{intl.formatMessage(messages['supportedFeatureList-mobile-show'])}
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
{intl.formatMessage(messages['supportedFeatureList-mobile-hide'])}
|
||||
</Collapsible.Visible>
|
||||
</>
|
||||
)}
|
||||
styling="basic"
|
||||
>
|
||||
{app.featureIds.map((id) => (
|
||||
<div key={`collapsible-${app.id}&${id}`} className="d-flex mb-1">
|
||||
<SupportedFeature name={intl.formatMessage(messages[`featureName-${id}`])} />
|
||||
</div>
|
||||
styling="basic"
|
||||
>
|
||||
{app.featureIds.map((id) => (
|
||||
<div key={`collapsible-${app.id}&${id}`} className="d-flex mb-1">
|
||||
<SupportedFeature name={intl.formatMessage(messages[`featureName-${id}`])} />
|
||||
</div>
|
||||
))}
|
||||
</Collapsible>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export default injectIntl(FeaturesList);
|
||||
|
||||
FeaturesList.propTypes = {
|
||||
app: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
featureIds: PropTypes.array.isRequired,
|
||||
featureIds: PropTypes.shape([]).isRequired,
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import appMessages from '../app-config-form/messages';
|
||||
import { FEATURE_TYPES } from '../data/constants';
|
||||
import './FeaturesTable.scss';
|
||||
|
||||
function FeaturesTable({ apps, features, intl }) {
|
||||
const FeaturesTable = ({ apps, features, intl }) => {
|
||||
const {
|
||||
basic, partial, full, common,
|
||||
} = _.groupBy(features, (feature) => feature.featureSupportType);
|
||||
@@ -87,12 +87,12 @@ function FeaturesTable({ apps, features, intl }) {
|
||||
<DataTable.Table />
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(FeaturesTable);
|
||||
|
||||
FeaturesTable.propTypes = {
|
||||
apps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
features: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
apps: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
features: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const SupportedFeature = ({ name }) => (
|
||||
</span>
|
||||
{name}
|
||||
</>
|
||||
);
|
||||
);
|
||||
|
||||
SupportedFeature.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
|
||||
@@ -64,7 +64,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'appDescription-openedx': {
|
||||
id: 'authoring.discussions.appList.appDescription-openedx',
|
||||
defaultMessage: 'Start conversations with other learners, ask questions, and interact with other learners in the course.',
|
||||
defaultMessage: 'Enable participation in discussion topics alongside course content.',
|
||||
description: 'A description of the new edX Discussions app.',
|
||||
},
|
||||
// Piazza
|
||||
|
||||
@@ -6,18 +6,16 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
|
||||
function NotesSettings({ intl, onClose }) {
|
||||
return (
|
||||
<AppSettingsModal
|
||||
appId="edxnotes"
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
enableAppHelp={intl.formatMessage(messages.enableNotesHelp)}
|
||||
enableAppLabel={intl.formatMessage(messages.enableNotesLabel)}
|
||||
learnMoreText={intl.formatMessage(messages.enableNotesLink)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
const NotesSettings = ({ intl, onClose }) => (
|
||||
<AppSettingsModal
|
||||
appId="edxnotes"
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
enableAppHelp={intl.formatMessage(messages.enableNotesHelp)}
|
||||
enableAppLabel={intl.formatMessage(messages.enableNotesLabel)}
|
||||
learnMoreText={intl.formatMessage(messages.enableNotesLink)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
NotesSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -9,11 +9,11 @@ import AppConfigFormDivider from '../discussions/app-config-form/apps/shared/App
|
||||
import LiveCommonFields from './LiveCommonFields';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
function BbbSettings({
|
||||
const BbbSettings = ({
|
||||
intl,
|
||||
values,
|
||||
setFieldValue,
|
||||
}) {
|
||||
}) => {
|
||||
const [bbbPlan, setBbbPlan] = useState(values.tierType);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -108,7 +108,7 @@ function BbbSettings({
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BbbSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import { initializeMockApp, history } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
@@ -34,6 +35,9 @@ let container;
|
||||
let store;
|
||||
const liveSettingsUrl = `/course/${courseId}/pages-and-resources/live/settings`;
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = jest.fn(node => node);
|
||||
|
||||
const renderComponent = () => {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
@@ -103,7 +107,8 @@ describe('BBB Settings', () => {
|
||||
expect(getAllByRole(dropDown, 'option').length).toBe(noOfOptions);
|
||||
});
|
||||
|
||||
test('Connect to support and PII sharing message is visible and plans selection is disabled, When pii sharing is disabled, ',
|
||||
test(
|
||||
'Connect to support and PII sharing message is visible and plans selection is disabled, When pii sharing is disabled, ',
|
||||
async () => {
|
||||
await mockStore({ piiSharingAllowed: false });
|
||||
renderComponent();
|
||||
@@ -116,7 +121,8 @@ describe('BBB Settings', () => {
|
||||
);
|
||||
expect(helpRequestPiiText).toHaveTextContent(messages.piiSharingEnableHelpTextBbb.defaultMessage);
|
||||
expect(container.querySelector('select[name="tierType"]')).toBeDisabled();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('free plans message is visible when free plan is selected', async () => {
|
||||
await mockStore({ emailSharing: true, isFreeTier: true });
|
||||
|
||||
@@ -4,37 +4,35 @@ import PropTypes from 'prop-types';
|
||||
import messages from './messages';
|
||||
import FormikControl from '../../generic/FormikControl';
|
||||
|
||||
function LiveCommonFields({
|
||||
const LiveCommonFields = ({
|
||||
intl,
|
||||
values,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
|
||||
<FormikControl
|
||||
name="consumerKey"
|
||||
value={values.consumerKey}
|
||||
floatingLabel={intl.formatMessage(messages.consumerKey)}
|
||||
className="pb-1"
|
||||
type="input"
|
||||
/>
|
||||
<FormikControl
|
||||
name="consumerSecret"
|
||||
value={values.consumerSecret}
|
||||
floatingLabel={intl.formatMessage(messages.consumerSecret)}
|
||||
className="pb-1"
|
||||
type="password"
|
||||
/>
|
||||
<FormikControl
|
||||
name="launchUrl"
|
||||
value={values.launchUrl}
|
||||
floatingLabel={intl.formatMessage(messages.launchUrl)}
|
||||
className="pb-1"
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
}) => (
|
||||
<>
|
||||
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
|
||||
<FormikControl
|
||||
name="consumerKey"
|
||||
value={values.consumerKey}
|
||||
floatingLabel={intl.formatMessage(messages.consumerKey)}
|
||||
className="pb-1"
|
||||
type="input"
|
||||
/>
|
||||
<FormikControl
|
||||
name="consumerSecret"
|
||||
value={values.consumerSecret}
|
||||
floatingLabel={intl.formatMessage(messages.consumerSecret)}
|
||||
className="pb-1"
|
||||
type="password"
|
||||
/>
|
||||
<FormikControl
|
||||
name="launchUrl"
|
||||
value={values.launchUrl}
|
||||
floatingLabel={intl.formatMessage(messages.launchUrl)}
|
||||
className="pb-1"
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
LiveCommonFields.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -16,10 +16,10 @@ import messages from './messages';
|
||||
import ZoomSettings from './ZoomSettings';
|
||||
import BBBSettings from './BBBSettings';
|
||||
|
||||
function LiveSettings({
|
||||
const LiveSettings = ({
|
||||
intl,
|
||||
onClose,
|
||||
}) {
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const courseId = useSelector(state => state.courseDetail.courseId);
|
||||
const availableProviders = useSelector((state) => state.live.appIds);
|
||||
@@ -75,59 +75,55 @@ function LiveSettings({
|
||||
}, [courseId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppSettingsModal
|
||||
appId="live"
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
enableAppHelp={intl.formatMessage(messages.enableLiveHelp)}
|
||||
enableAppLabel={intl.formatMessage(messages.enableLiveLabel)}
|
||||
learnMoreText={intl.formatMessage(messages.enableLiveLink)}
|
||||
onClose={onClose}
|
||||
initialValues={liveConfiguration}
|
||||
validationSchema={validationSchema}
|
||||
onSettingsSave={handleSettingsSave}
|
||||
configureBeforeEnable
|
||||
enableReinitialize
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<>
|
||||
{(status === RequestStatus.IN_PROGRESS) ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<>
|
||||
<h4 className="my-3">{intl.formatMessage(messages.selectProvider)}</h4>
|
||||
<SelectableBox.Set
|
||||
type="checkbox"
|
||||
value={values.provider}
|
||||
onChange={(event) => handleProviderChange(event.target.value, setFieldValue, values)}
|
||||
name="provider"
|
||||
columns={3}
|
||||
className="mb-3"
|
||||
>
|
||||
{availableProviders.map((provider) => (
|
||||
<SelectableBox value={provider} type="checkbox" key={provider}>
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Icon src={iconsSrc[`${camelCase(provider)}`]} alt={provider} />
|
||||
<span>{intl.formatMessage(messages[`appName-${camelCase(provider)}`])}</span>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
))}
|
||||
</SelectableBox.Set>
|
||||
{values.provider === 'zoom' ? <ZoomSettings values={values} />
|
||||
: (
|
||||
<BBBSettings
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<AppSettingsModal
|
||||
appId="live"
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
enableAppHelp={intl.formatMessage(messages.enableLiveHelp)}
|
||||
enableAppLabel={intl.formatMessage(messages.enableLiveLabel)}
|
||||
learnMoreText={intl.formatMessage(messages.enableLiveLink)}
|
||||
onClose={onClose}
|
||||
initialValues={liveConfiguration}
|
||||
validationSchema={validationSchema}
|
||||
onSettingsSave={handleSettingsSave}
|
||||
configureBeforeEnable
|
||||
enableReinitialize
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
(status === RequestStatus.IN_PROGRESS) ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<>
|
||||
<h4 className="my-3">{intl.formatMessage(messages.selectProvider)}</h4>
|
||||
<SelectableBox.Set
|
||||
type="checkbox"
|
||||
value={values.provider}
|
||||
onChange={(event) => handleProviderChange(event.target.value, setFieldValue, values)}
|
||||
name="provider"
|
||||
columns={3}
|
||||
className="mb-3"
|
||||
>
|
||||
{availableProviders.map((provider) => (
|
||||
<SelectableBox value={provider} type="checkbox" key={provider}>
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Icon src={iconsSrc[`${camelCase(provider)}`]} alt={provider} />
|
||||
<span>{intl.formatMessage(messages[`appName-${camelCase(provider)}`])}</span>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
))}
|
||||
</SelectableBox.Set>
|
||||
{values.provider === 'zoom' ? <ZoomSettings values={values} />
|
||||
: (
|
||||
<BBBSettings
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</AppSettingsModal>
|
||||
</>
|
||||
</AppSettingsModal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LiveSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import { initializeMockApp, history } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
@@ -37,6 +38,9 @@ let container;
|
||||
let store;
|
||||
const liveSettingsUrl = `/course/${courseId}/pages-and-resources/live/settings`;
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = jest.fn(node => node);
|
||||
|
||||
const renderComponent = () => {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
|
||||
@@ -6,16 +6,16 @@ import { providerNames } from './constants';
|
||||
import LiveCommonFields from './LiveCommonFields';
|
||||
import FormikControl from '../../generic/FormikControl';
|
||||
|
||||
function ZoomSettings({
|
||||
const ZoomSettings = ({
|
||||
intl,
|
||||
values,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{!values.piiSharingEnable ? (
|
||||
<p data-testid="request-pii-sharing">
|
||||
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })}
|
||||
</p>
|
||||
}) => (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{!values.piiSharingEnable ? (
|
||||
<p data-testid="request-pii-sharing">
|
||||
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{(values.piiSharingEmail || values.piiSharingUsername)
|
||||
@@ -33,9 +33,8 @@ function ZoomSettings({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ZoomSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import { initializeMockApp, history } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
@@ -32,6 +33,9 @@ let container;
|
||||
let store;
|
||||
const liveSettingsUrl = `/course/${courseId}/pages-and-resources/live/settings`;
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = jest.fn(node => node);
|
||||
|
||||
const renderComponent = () => {
|
||||
const wrapper = render(
|
||||
<IntlProvider locale="en">
|
||||
|
||||
@@ -36,11 +36,11 @@ export const initialState = {
|
||||
export const configurationProviders = (
|
||||
emailSharing,
|
||||
usernameSharing,
|
||||
activeProvider = 'zoom',
|
||||
activeProvider,
|
||||
hasFreeTier,
|
||||
) => ({
|
||||
providers: {
|
||||
active: activeProvider,
|
||||
active: activeProvider || 'zoom',
|
||||
available: {
|
||||
zoom: {
|
||||
features: [],
|
||||
@@ -65,7 +65,7 @@ export const generateLiveConfigurationApiResponse = (
|
||||
enabled,
|
||||
piiSharingAllowed,
|
||||
providerType = 'zoom',
|
||||
isFreeTier,
|
||||
isFreeTier = undefined,
|
||||
) => ({
|
||||
course_key: courseId,
|
||||
enabled,
|
||||
|
||||
@@ -17,6 +17,7 @@ const CoursePageShape = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
legacyLink: PropTypes.string,
|
||||
allowedOperations: PropTypes.shape({
|
||||
enable: PropTypes.bool.isRequired,
|
||||
configure: PropTypes.bool.isRequired,
|
||||
@@ -25,13 +26,14 @@ const CoursePageShape = PropTypes.shape({
|
||||
|
||||
export { CoursePageShape };
|
||||
|
||||
function PageCard({
|
||||
const PageCard = ({
|
||||
intl,
|
||||
page,
|
||||
}) {
|
||||
}) => {
|
||||
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
|
||||
const isDesktop = useIsDesktop();
|
||||
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
const SettingsButton = () => {
|
||||
if (page.legacyLink) {
|
||||
return (
|
||||
@@ -80,7 +82,7 @@ function PageCard({
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PageCard.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -4,21 +4,19 @@ import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { CardGrid } from '@edx/paragon';
|
||||
import PageCard, { CoursePageShape } from './PageCard';
|
||||
|
||||
function PageGrid({ pages }) {
|
||||
return (
|
||||
<CardGrid columnSizes={{
|
||||
const PageGrid = ({ pages }) => (
|
||||
<CardGrid columnSizes={{
|
||||
xs: 12,
|
||||
sm: 6,
|
||||
lg: 4,
|
||||
xl: 4,
|
||||
}}
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<PageCard page={page} key={page.id} />
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<PageCard page={page} key={page.id} />
|
||||
))}
|
||||
</CardGrid>
|
||||
</CardGrid>
|
||||
);
|
||||
}
|
||||
|
||||
PageGrid.propTypes = {
|
||||
pages: PropTypes.arrayOf(CoursePageShape.isRequired).isRequired,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useIsMobile } from '../../utils';
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
import messages from './messages';
|
||||
|
||||
function ProctoringSettings({ intl, onClose }) {
|
||||
const ProctoringSettings = ({ intl, onClose }) => {
|
||||
const initialFormValues = {
|
||||
enableProctoredExams: false,
|
||||
proctoringProvider: false,
|
||||
@@ -60,7 +60,7 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
const proctoringEscalationEmailInputRef = useRef(null);
|
||||
const submitButtonState = submissionInProgress ? 'pending' : 'default';
|
||||
|
||||
function handleChange(event) {
|
||||
const handleChange = (event) => {
|
||||
const { target } = event;
|
||||
const value = target.type === 'checkbox' ? target.checked : target.value;
|
||||
const { name } = target;
|
||||
@@ -86,17 +86,17 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
} else {
|
||||
setFormValues({ ...formValues, [name]: value });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function isLtiProvider(provider) {
|
||||
return ltiProctoringProviders.some(p => p.name === provider);
|
||||
}
|
||||
|
||||
function setFocusToProctortrackEscalationEmailInput() {
|
||||
const setFocusToProctortrackEscalationEmailInput = () => {
|
||||
if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) {
|
||||
proctoringEscalationEmailInputRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function postSettingsBackToServer() {
|
||||
const providerIsLti = isLtiProvider(formValues.proctoringProvider);
|
||||
@@ -122,8 +122,9 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
if (allowLtiProviders && ExamsApiService.isAvailable()) {
|
||||
saveOperations.push(
|
||||
ExamsApiService.saveCourseExamConfiguration(
|
||||
courseId, { provider: providerIsLti ? formValues.proctoringProvider : null },
|
||||
),
|
||||
courseId,
|
||||
{ provider: providerIsLti ? formValues.proctoringProvider : null },
|
||||
),
|
||||
);
|
||||
}
|
||||
Promise.all(saveOperations)
|
||||
@@ -138,7 +139,7 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
formValues.proctoringProvider === 'proctortrack'
|
||||
@@ -152,7 +153,11 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
isValid: false,
|
||||
errors: {
|
||||
formProctortrackEscalationEmail: {
|
||||
dialogErrorMessage: (<Alert.Link onClick={setFocusToProctortrackEscalationEmailInput} href="#formProctortrackEscalationEmail" data-testid="proctorTrackEscalationEmailErrorLink">{errorMessage}</Alert.Link>),
|
||||
dialogErrorMessage: (
|
||||
<Alert.Link onClick={setFocusToProctortrackEscalationEmailInput} href="#formProctortrackEscalationEmail" data-testid="proctorTrackEscalationEmailErrorLink">
|
||||
{errorMessage}
|
||||
</Alert.Link>
|
||||
),
|
||||
inputErrorMessage: errorMessage,
|
||||
},
|
||||
},
|
||||
@@ -179,7 +184,7 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
errors,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function cannotEditProctoringProvider() {
|
||||
const currentDate = moment(moment()).format('YYYY-MM-DD[T]hh:mm:ss[Z]');
|
||||
@@ -468,8 +473,7 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
StudioApiService.getProctoredExamSettingsData(courseId),
|
||||
ExamsApiService.isAvailable() ? ExamsApiService.getCourseExamConfiguration(courseId) : Promise.resolve(),
|
||||
@@ -500,8 +504,9 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
let availableProviders = proctoringProvidersStudio.filter(value => value !== 'lti_external');
|
||||
if (enableLtiProviders) {
|
||||
availableProviders = proctoringProvidersLti.reduce(
|
||||
(result, provider) => [...result, provider.name], availableProviders,
|
||||
);
|
||||
(result, provider) => [...result, provider.name],
|
||||
availableProviders,
|
||||
);
|
||||
}
|
||||
setAvailableProctoringProviders(availableProviders);
|
||||
|
||||
@@ -535,8 +540,7 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
setSubmissionInProgress(false);
|
||||
},
|
||||
);
|
||||
}, [],
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if ((saveSuccess || saveError) && !!saveStatusAlertRef.current) {
|
||||
@@ -597,7 +601,7 @@ function ProctoringSettings({ intl, onClose }) {
|
||||
</Form>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProctoringSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -8,9 +8,9 @@ import { useAppSetting } from '../../utils';
|
||||
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
|
||||
function ProgressSettings({ intl, onClose }) {
|
||||
const ProgressSettings = ({ intl, onClose }) => {
|
||||
const [disableProgressGraph, saveSetting] = useAppSetting('disableProgressGraph');
|
||||
const showProgressGraphSetting = getConfig().ENABLE_PROGRESS_GRAPH_SETTINGS.toLowerCase() === 'true';
|
||||
const showProgressGraphSetting = getConfig().ENABLE_PROGRESS_GRAPH_SETTINGS.toString().toLowerCase() === 'true';
|
||||
|
||||
const handleSettingsSave = (values) => {
|
||||
if (showProgressGraphSetting) { saveSetting(!values.enableProgressGraph); }
|
||||
@@ -45,7 +45,7 @@ function ProgressSettings({ intl, onClose }) {
|
||||
}
|
||||
</AppSettingsModal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProgressSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -24,9 +24,9 @@ const TeamTypeNameMessage = {
|
||||
},
|
||||
};
|
||||
|
||||
function GroupEditor({
|
||||
const GroupEditor = ({
|
||||
intl, group, onDelete, onChange, onBlur, fieldNameCommonBase, errors,
|
||||
}) {
|
||||
}) => {
|
||||
const [isDeleting, setDeleting] = useState(false);
|
||||
const [isOpen, setOpen] = useState(group.id === null);
|
||||
const initiateDeletion = () => setDeleting(true);
|
||||
@@ -133,7 +133,7 @@ function GroupEditor({
|
||||
)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const groupShape = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
|
||||
@@ -16,10 +16,10 @@ import messages from './messages';
|
||||
|
||||
setupYupExtensions();
|
||||
|
||||
function TeamSettings({
|
||||
const TeamSettings = ({
|
||||
intl,
|
||||
onClose,
|
||||
}) {
|
||||
}) => {
|
||||
const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration');
|
||||
const blankNewGroup = {
|
||||
name: '',
|
||||
@@ -161,7 +161,7 @@ function TeamSettings({
|
||||
}
|
||||
</AppSettingsModal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
TeamSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useAppSetting } from '../../utils';
|
||||
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
|
||||
function WikiSettings({ intl, onClose }) {
|
||||
const WikiSettings = ({ intl, onClose }) => {
|
||||
const [enablePublicWiki, saveSetting] = useAppSetting('allowPublicWikiAccess');
|
||||
const handleSettingsSave = (values) => saveSetting(values.enablePublicWiki);
|
||||
|
||||
@@ -39,7 +39,7 @@ function WikiSettings({ intl, onClose }) {
|
||||
}
|
||||
</AppSettingsModal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
WikiSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
fetchExamSettingsSuccess,
|
||||
} from './data/thunks';
|
||||
|
||||
function ProctoredExamSettings({ courseId, intl }) {
|
||||
const ProctoredExamSettings = ({ courseId, intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
@@ -55,9 +55,9 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
const saveStatusAlertRef = React.createRef();
|
||||
const proctoringEscalationEmailInputRef = useRef(null);
|
||||
|
||||
function onEnableProctoredExamsChange(event) {
|
||||
const onEnableProctoredExamsChange = (event) => {
|
||||
setEnableProctoredExams(event.target.checked);
|
||||
}
|
||||
};
|
||||
|
||||
function onAllowOptingOutChange(value) {
|
||||
setAllowOptingOut(value);
|
||||
@@ -67,7 +67,7 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
setCreateZendeskTickets(value);
|
||||
}
|
||||
|
||||
function onProctoringProviderChange(event) {
|
||||
const onProctoringProviderChange = (event) => {
|
||||
const provider = event.target.value;
|
||||
setProctoringProvider(provider);
|
||||
|
||||
@@ -80,17 +80,17 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
}
|
||||
setShowProctortrackEscalationEmail(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function onProctortrackEscalationEmailChange(event) {
|
||||
const onProctortrackEscalationEmailChange = (event) => {
|
||||
setProctortrackEscalationEmail(event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
function setFocusToProctortrackEscalationEmailInput() {
|
||||
const setFocusToProctortrackEscalationEmailInput = () => {
|
||||
if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) {
|
||||
proctoringEscalationEmailInputRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function isLtiProvider(provider) {
|
||||
return ltiProctoringProviders.some(p => p.name === provider);
|
||||
@@ -120,9 +120,7 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)];
|
||||
if (allowLtiProviders && ExamsApiService.isAvailable()) {
|
||||
saveOperations.push(
|
||||
ExamsApiService.saveCourseExamConfiguration(
|
||||
courseId, { provider: providerIsLti ? proctoringProvider : null },
|
||||
),
|
||||
ExamsApiService.saveCourseExamConfiguration(courseId, { provider: providerIsLti ? proctoringProvider : null }),
|
||||
);
|
||||
}
|
||||
Promise.all(saveOperations)
|
||||
@@ -137,7 +135,7 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
if (proctoringProvider === 'proctortrack' && !EmailValidator.validate(proctortrackEscalationEmail) && !(proctortrackEscalationEmail === '' && !enableProctoredExams)) {
|
||||
if (proctortrackEscalationEmail === '') {
|
||||
@@ -174,7 +172,7 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
errors,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function cannotEditProctoringProvider() {
|
||||
const currentDate = moment(moment()).format('YYYY-MM-DD[T]hh:mm:ss[Z]');
|
||||
@@ -492,8 +490,7 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
useEffect(() => {
|
||||
dispatch(fetchExamSettingsPending(courseId));
|
||||
|
||||
Promise.all([
|
||||
@@ -526,7 +523,8 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
let availableProviders = proctoringProvidersStudio.filter(value => value !== 'lti_external');
|
||||
if (enableLtiProviders) {
|
||||
availableProviders = proctoringProvidersLti.reduce(
|
||||
(result, provider) => [...result, provider.name], availableProviders,
|
||||
(result, provider) => [...result, provider.name],
|
||||
availableProviders,
|
||||
);
|
||||
}
|
||||
setAvailableProctoringProviders(availableProviders);
|
||||
@@ -560,8 +558,7 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
dispatch(fetchExamSettingsFailure(courseId));
|
||||
},
|
||||
);
|
||||
}, [],
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if ((saveSuccess || saveError) && !!saveStatusAlertRef.current) {
|
||||
@@ -587,7 +584,7 @@ function ProctoredExamSettings({ courseId, intl }) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProctoredExamSettings.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import '@testing-library/jest-dom';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import Enzyme from 'enzyme';
|
||||
@@ -29,9 +28,6 @@ Object.defineProperty(window, 'matchMedia', {
|
||||
})),
|
||||
});
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
// Mock Intersection Observer which is unavailable in the context of a test.
|
||||
global.IntersectionObserver = jest.fn(function mockIntersectionObserver() {
|
||||
this.observe = jest.fn();
|
||||
|
||||
@@ -4,12 +4,12 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { AvatarIcon } from './Icons';
|
||||
|
||||
function Avatar({
|
||||
const Avatar = ({
|
||||
size,
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
}) {
|
||||
}) => {
|
||||
const avatar = src ? (
|
||||
<img className="d-block w-100 h-100" src={src} alt={alt} />
|
||||
) : (
|
||||
@@ -24,7 +24,7 @@ function Avatar({
|
||||
{avatar}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Avatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
|
||||
@@ -15,17 +15,16 @@ import DesktopHeader from './DesktopHeader';
|
||||
import MobileHeader from './MobileHeader';
|
||||
import messages from './Header.messages';
|
||||
|
||||
import StudioLogoSVG from './assets/studio-logo.svg';
|
||||
|
||||
ensureConfig([
|
||||
'STUDIO_BASE_URL',
|
||||
'LOGOUT_URL',
|
||||
'LOGIN_URL',
|
||||
'LOGO_URL',
|
||||
], 'Header component');
|
||||
|
||||
function Header({
|
||||
const Header = ({
|
||||
courseId, courseNumber, courseOrg, courseTitle, intl,
|
||||
}) {
|
||||
}) => {
|
||||
const { authenticatedUser, config } = useContext(AppContext);
|
||||
|
||||
const mainMenu = [
|
||||
@@ -49,9 +48,11 @@ function Header({
|
||||
<div className="mb-1 small">
|
||||
<a rel="noopener" href={`${config.STUDIO_BASE_URL}/textbooks/${courseId}`}>{intl.formatMessage(messages['header.links.textbooks'])}</a>
|
||||
</div>
|
||||
<div className="mb-1 small">
|
||||
<a rel="noopener" href={`${config.STUDIO_BASE_URL}/videos/${courseId}`}>{intl.formatMessage(messages['header.links.videoUploads'])}</a>
|
||||
</div>
|
||||
{process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true' && (
|
||||
<div className="mb-1 small">
|
||||
<a rel="noopener" href={`${config.STUDIO_BASE_URL}/videos/${courseId}`}>{intl.formatMessage(messages['header.links.videoUploads'])}</a>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -154,7 +155,7 @@ function Header({
|
||||
);
|
||||
|
||||
const props = {
|
||||
logo: StudioLogoSVG,
|
||||
logo: config.LOGO_URL,
|
||||
logoAltText: 'Studio edX',
|
||||
siteName: 'edX',
|
||||
logoDestination: config.STUDIO_BASE_URL,
|
||||
@@ -175,7 +176,7 @@ function Header({
|
||||
</Responsive>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
// This file was copied from edx/frontend-component-header-edx.
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
@@ -5,6 +6,7 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -39,14 +41,16 @@ describe('<Header />', () => {
|
||||
}
|
||||
|
||||
it('renders desktop header correctly with API call', async () => {
|
||||
const component = createComponent(1280, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber="DemoX"
|
||||
courseOrg="edX"
|
||||
courseTitle="Demonstration Course"
|
||||
/>
|
||||
));
|
||||
const component = createComponent(
|
||||
1280, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber="DemoX"
|
||||
courseOrg="edX"
|
||||
courseTitle="Demonstration Course"
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
render(component);
|
||||
expect(screen.getByTestId('course-org-number').textContent).toEqual(expect.stringContaining('edX DemoX'));
|
||||
@@ -54,47 +58,93 @@ describe('<Header />', () => {
|
||||
});
|
||||
|
||||
it('renders mobile header correctly with API call', async () => {
|
||||
const component = createComponent(500, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber="DemoX"
|
||||
courseOrg="edX"
|
||||
courseTitle="Demonstration Course"
|
||||
/>
|
||||
));
|
||||
const component = createComponent(
|
||||
500, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber="DemoX"
|
||||
courseOrg="edX"
|
||||
courseTitle="Demonstration Course"
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
render(component);
|
||||
expect(screen.getByTestId('edx-header-logo'));
|
||||
});
|
||||
|
||||
it('renders desktop header correctly with bad API call', async () => {
|
||||
const component = createComponent(1280, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber={null}
|
||||
courseOrg={null}
|
||||
courseTitle="course-v1:edX+DemoX+Demo_Course"
|
||||
/>
|
||||
));
|
||||
const component = createComponent(
|
||||
1280, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber={null}
|
||||
courseOrg={null}
|
||||
courseTitle="course-v1:edX+DemoX+Demo_Course"
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
render(component);
|
||||
expect(screen.getByTestId('course-title').textContent).toEqual(expect.stringContaining('course-v1:edX+DemoX+Demo_Course'));
|
||||
});
|
||||
|
||||
it('renders mobile header correctly with bad API call', async () => {
|
||||
const component = createComponent(500, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber={null}
|
||||
courseOrg={null}
|
||||
courseTitle="course-v1:edX+DemoX+Demo_Course"
|
||||
/>
|
||||
));
|
||||
const component = createComponent(
|
||||
500, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber={null}
|
||||
courseOrg={null}
|
||||
courseTitle="course-v1:edX+DemoX+Demo_Course"
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
render(component);
|
||||
expect(screen.getByTestId('edx-header-logo'));
|
||||
});
|
||||
|
||||
it('renders Video Uploads link', () => {
|
||||
process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = 'true';
|
||||
|
||||
const component = createComponent(
|
||||
1280, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber="DemoX"
|
||||
courseOrg="edX"
|
||||
courseTitle="Demonstration Course"
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
render(component);
|
||||
fireEvent.click(screen.getByText('Content'));
|
||||
|
||||
expect(screen.getByText('Video Uploads')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Video Uploads link', () => {
|
||||
process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = 'false';
|
||||
|
||||
const component = createComponent(
|
||||
1280, (
|
||||
<Header
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
courseNumber="DemoX"
|
||||
courseOrg="edX"
|
||||
courseTitle="Demonstration Course"
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
render(component);
|
||||
fireEvent.click(screen.getByText('Content'));
|
||||
|
||||
expect(screen.queryByText('Video Uploads')).toBeNull();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file was copied from edx/frontend-component-header-edx.
|
||||
import React from 'react';
|
||||
|
||||
export const MenuIcon = props => (
|
||||
export const MenuIcon = (props) => (
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
@@ -13,9 +13,9 @@ export const MenuIcon = props => (
|
||||
<rect fill="currentColor" x="2" y="11" width="20" height="2" />
|
||||
<rect fill="currentColor" x="2" y="17" width="20" height="2" />
|
||||
</svg>
|
||||
);
|
||||
);
|
||||
|
||||
export const AvatarIcon = props => (
|
||||
export const AvatarIcon = (props) => (
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
@@ -28,9 +28,9 @@ export const AvatarIcon = props => (
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
);
|
||||
|
||||
export const CaretIcon = props => (
|
||||
export const CaretIcon = (props) => (
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
@@ -44,4 +44,4 @@ export const CaretIcon = props => (
|
||||
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
);
|
||||
|
||||
@@ -2,29 +2,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function Logo({ src, alt, ...attributes }) {
|
||||
return (
|
||||
<img src={src} alt={alt} {...attributes} />
|
||||
const Logo = ({ src, alt, ...attributes }) => (
|
||||
<img src={src} alt={alt} {...attributes} />
|
||||
);
|
||||
}
|
||||
|
||||
Logo.propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function LinkedLogo({
|
||||
const LinkedLogo = ({
|
||||
href,
|
||||
src,
|
||||
alt,
|
||||
...attributes
|
||||
}) {
|
||||
return (
|
||||
<a href={href} {...attributes}>
|
||||
<img className="d-block" src={src} alt={alt} />
|
||||
</a>
|
||||
}) => (
|
||||
<a href={href} {...attributes}>
|
||||
<img className="d-block" src={src} alt={alt} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
LinkedLogo.propTypes = {
|
||||
href: PropTypes.string.isRequired,
|
||||
|
||||
@@ -3,12 +3,10 @@ import React from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function MenuTrigger({ tag, className, ...attributes }) {
|
||||
return React.createElement(tag, {
|
||||
const MenuTrigger = ({ tag, className, ...attributes }) => React.createElement(tag, {
|
||||
className: `menu-trigger ${className}`,
|
||||
...attributes,
|
||||
});
|
||||
}
|
||||
MenuTrigger.propTypes = {
|
||||
tag: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
@@ -19,12 +17,10 @@ MenuTrigger.defaultProps = {
|
||||
};
|
||||
const MenuTriggerType = <MenuTrigger />.type;
|
||||
|
||||
function MenuContent({ tag, className, ...attributes }) {
|
||||
return React.createElement(tag, {
|
||||
const MenuContent = ({ tag, className, ...attributes }) => React.createElement(tag, {
|
||||
className: ['menu-content', className].join(' '),
|
||||
...attributes,
|
||||
});
|
||||
}
|
||||
MenuContent.propTypes = {
|
||||
tag: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 271.93 76.05" style="enable-background:new 0 0 271.93 76.05;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#00262B;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M161.07,32.72c-3.8,0-6.52,1.93-6.94,4.57c-0.51,2.96,2.54,4.11,4.83,4.71l2.9,0.78
|
||||
c3.74,0.97,8.35,3.13,7.43,8.7c-0.9,5.24-5.67,9.06-12.93,9.06c-6.93,0-10.72-3.46-10.09-9.06h4.7c-0.31,3.37,2.43,4.97,6.02,4.97
|
||||
c3.97,0,7.19-2.01,7.67-5.01c0.48-2.73-1.9-3.84-5.09-4.71l-3.49-1c-4.73-1.36-7.38-3.87-6.69-8.17
|
||||
c0.92-5.35,6.24-8.88,12.44-8.88c6.28,0,9.98,3.6,9.34,8.73h-4.5C166.73,34.37,164.63,32.72,161.07,32.72z"/>
|
||||
<path class="st0" d="M186.76,40.47h-4.79l-2.13,12.76c-0.42,2.61,0.83,3.09,2.3,3.09c0.72,0,1.28-0.14,1.6-0.19l0.25,3.73
|
||||
c-0.6,0.2-1.62,0.47-3.07,0.48c-3.58,0.06-6.42-1.98-5.71-6.18l2.26-13.69h-3.38l0.59-3.62h3.38l0.92-5.56h4.51l-0.92,5.56h4.77
|
||||
L186.76,40.47z"/>
|
||||
<path class="st0" d="M206.59,36.84h4.53l-3.87,23.19h-4.44l0.68-4.02h-0.24c-1.45,2.48-4.19,4.32-7.67,4.32
|
||||
c-4.46,0-7.1-2.99-6.16-8.74l2.48-14.75h4.51l-2.37,14.21c-0.5,3.16,1.09,5.18,3.88,5.18c2.54,0,5.75-1.87,6.4-5.82L206.59,36.84z
|
||||
"/>
|
||||
<path class="st0" d="M213.82,48.48c1.25-7.52,6.11-11.94,11.73-11.94c4.3,0,5.42,2.63,5.98,4.06h0.27l1.92-11.49h4.51l-5.13,30.93
|
||||
h-4.41l0.6-3.61h-0.36c-1.07,1.48-3.16,4.06-7.37,4.06C215.92,60.49,212.6,55.99,213.82,48.48z M230.6,48.44
|
||||
c0.8-4.85-0.88-8.06-4.83-8.06c-4.06,0-6.6,3.46-7.35,8.06c-0.77,4.65,0.63,8.2,4.68,8.2C226.97,56.64,229.8,53.32,230.6,48.44z"
|
||||
/>
|
||||
<path class="st0" d="M242.97,36.84h4.51l-3.87,23.19h-4.51L242.97,36.84z M243.44,30.53c0.06-1.5,1.41-2.7,2.98-2.7
|
||||
c1.55,0,2.79,1.21,2.72,2.7c-0.06,1.48-1.41,2.69-2.98,2.69C244.6,33.22,243.37,32.01,243.44,30.53z"/>
|
||||
<path class="st0" d="M250.19,48.35c1.13-7.13,6.1-11.81,12.56-11.81c6.61,0,10.13,4.88,8.96,12.2
|
||||
c-1.17,7.08-6.13,11.76-12.57,11.76C252.49,60.5,248.99,55.63,250.19,48.35z M267.25,48.35c0.68-4.36-0.62-8.03-4.76-8.03
|
||||
c-4.36,0-7.13,3.87-7.85,8.41c-0.71,4.35,0.59,7.99,4.76,7.99C263.73,56.71,266.5,52.89,267.25,48.35z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="st0" points="86.45,12.49 89.06,0.02 12.55,0.02 0,59.95 63.97,59.95 60.45,76.03 121.72,76.03 135.24,12.49
|
||||
"/>
|
||||
<path class="st1" d="M26.07,52.3c-1.73,0-3.37-0.28-4.91-0.85c-1.54-0.57-2.89-1.41-4.03-2.51c-1.15-1.11-2.05-2.47-2.72-4.09
|
||||
c-0.67-1.62-1-3.48-1-5.58c0-2.87,0.4-5.49,1.2-7.85c0.8-2.36,1.91-4.38,3.34-6.07c1.43-1.69,3.14-2.99,5.13-3.92
|
||||
c1.99-0.92,4.18-1.39,6.55-1.39c1.6,0,3.12,0.28,4.55,0.84c1.44,0.56,2.69,1.37,3.77,2.44c1.08,1.07,1.94,2.38,2.57,3.95
|
||||
c0.64,1.56,0.95,3.35,0.95,5.38c0,0.31-0.01,0.67-0.03,1.08c-0.02,0.42-0.05,0.84-0.09,1.27c-0.04,0.43-0.08,0.86-0.11,1.27
|
||||
c-0.04,0.41-0.09,0.77-0.14,1.05H18.64c-0.02,0.27-0.03,0.53-0.04,0.78c-0.01,0.25-0.01,0.51-0.01,0.78
|
||||
c0,1.6,0.23,2.96,0.69,4.09c0.46,1.13,1.06,2.05,1.81,2.76c0.74,0.71,1.58,1.23,2.5,1.55s1.86,0.48,2.8,0.48
|
||||
c2.06,0,3.72-0.36,4.97-1.07c1.25-0.71,2.21-1.68,2.86-2.89h5.29c-0.33,1.2-0.87,2.31-1.62,3.35c-0.75,1.04-1.7,1.94-2.85,2.7
|
||||
c-1.15,0.76-2.48,1.36-3.99,1.79C29.55,52.09,27.88,52.3,26.07,52.3z M36.22,33.31c0.02-0.1,0.03-0.27,0.04-0.54
|
||||
c0.01-0.26,0.01-0.52,0.01-0.77c0-1.02-0.15-1.99-0.45-2.91c-0.3-0.91-0.75-1.72-1.35-2.41c-0.6-0.69-1.34-1.24-2.23-1.65
|
||||
c-0.89-0.4-1.92-0.61-3.09-0.61c-1.2,0-2.31,0.21-3.35,0.64c-1.04,0.42-1.99,1.03-2.83,1.81c-0.85,0.78-1.58,1.71-2.2,2.8
|
||||
c-0.62,1.09-1.11,2.3-1.47,3.63L36.22,33.31L36.22,33.31z"/>
|
||||
<path class="st1" d="M55.67,52.3c-1.56,0-3.03-0.29-4.4-0.88c-1.37-0.59-2.57-1.43-3.6-2.53c-1.03-1.1-1.85-2.43-2.44-3.99
|
||||
c-0.6-1.56-0.9-3.3-0.9-5.23c0-1.87,0.19-3.66,0.56-5.36c0.38-1.7,0.91-3.29,1.6-4.74c0.69-1.46,1.53-2.77,2.5-3.95
|
||||
c0.97-1.18,2.05-2.18,3.24-3.01s2.47-1.47,3.85-1.91c1.38-0.44,2.82-0.67,4.32-0.67c1.12,0,2.18,0.15,3.19,0.46
|
||||
c1.01,0.31,1.93,0.74,2.75,1.29c0.82,0.55,1.52,1.21,2.11,1.99c0.59,0.78,1.02,1.63,1.29,2.56h0.46l3.85-18.13h5.06l-9.25,43.54
|
||||
h-4.8l0.9-4.25H65.5c-1.14,1.48-2.56,2.65-4.28,3.51C59.5,51.87,57.65,52.3,55.67,52.3z M57.26,47.82c1.62,0,3.12-0.38,4.5-1.14
|
||||
c1.38-0.76,2.58-1.8,3.6-3.12c1.02-1.32,1.82-2.87,2.4-4.65c0.58-1.78,0.87-3.71,0.87-5.77c0-1.33-0.18-2.52-0.55-3.59
|
||||
c-0.37-1.06-0.89-1.96-1.56-2.7c-0.68-0.74-1.49-1.32-2.46-1.72c-0.96-0.41-2.05-0.61-3.27-0.61c-1.6,0-3.08,0.36-4.45,1.07
|
||||
c-1.37,0.71-2.55,1.7-3.56,2.98c-1,1.27-1.79,2.79-2.37,4.55c-0.58,1.76-0.87,3.7-0.87,5.8c0,1.31,0.19,2.51,0.57,3.61
|
||||
s0.9,2.04,1.58,2.82c0.68,0.78,1.48,1.39,2.43,1.82C55.05,47.6,56.1,47.82,57.26,47.82z"/>
|
||||
<g>
|
||||
<polygon class="st1" points="124.49,20.68 113.2,20.68 100.9,35.92 100.29,35.92 93.78,20.68 82.37,20.68 92.15,42.82
|
||||
71.03,67.84 82.16,67.84 95.72,51.76 96.63,51.76 103.96,67.84 115.16,67.84 104.33,43.92 "/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M141.76,20.4c-0.55,0-1.07-0.1-1.55-0.31c-0.48-0.21-0.91-0.49-1.27-0.86c-0.37-0.37-0.65-0.79-0.86-1.27
|
||||
c-0.21-0.48-0.31-1-0.31-1.55c0-0.55,0.1-1.07,0.31-1.55c0.21-0.48,0.49-0.91,0.86-1.27c0.37-0.37,0.79-0.65,1.27-0.86
|
||||
c0.48-0.21,1-0.31,1.55-0.31c0.55,0,1.07,0.1,1.55,0.31c0.48,0.21,0.91,0.49,1.27,0.86c0.37,0.37,0.65,0.79,0.86,1.27
|
||||
c0.21,0.48,0.31,1,0.31,1.55c0,0.55-0.1,1.07-0.31,1.55c-0.21,0.48-0.49,0.91-0.86,1.27s-0.79,0.65-1.27,0.86
|
||||
C142.83,20.3,142.31,20.4,141.76,20.4z M141.76,19.66c0.6,0,1.15-0.15,1.64-0.44c0.49-0.29,0.88-0.69,1.18-1.18
|
||||
c0.29-0.49,0.44-1.04,0.44-1.64c0-0.6-0.15-1.15-0.44-1.64c-0.29-0.49-0.69-0.88-1.18-1.18c-0.49-0.29-1.04-0.44-1.64-0.44
|
||||
s-1.15,0.15-1.64,0.44c-0.49,0.29-0.88,0.69-1.18,1.18c-0.29,0.49-0.44,1.04-0.44,1.64c0,0.6,0.15,1.15,0.44,1.64
|
||||
c0.29,0.49,0.69,0.88,1.18,1.18C140.61,19.51,141.16,19.66,141.76,19.66z M140.4,18.2v-3.69h1.77c0.19,0,0.37,0.04,0.54,0.13
|
||||
c0.18,0.09,0.32,0.22,0.43,0.39c0.12,0.17,0.17,0.38,0.17,0.63c0,0.25-0.06,0.47-0.18,0.65c-0.12,0.18-0.27,0.32-0.45,0.42
|
||||
c-0.18,0.1-0.37,0.14-0.56,0.14h-1.37v-0.5h1.2c0.17,0,0.32-0.06,0.46-0.18c0.13-0.12,0.2-0.3,0.2-0.52c0-0.23-0.07-0.4-0.2-0.5
|
||||
s-0.28-0.15-0.44-0.15h-0.93v3.18H140.4z M142.55,16.49l0.92,1.71h-0.72l-0.89-1.71H142.55z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.2 KiB |
Reference in New Issue
Block a user