Compare commits

...

39 Commits

Author SHA1 Message Date
Maria Grimaldi
7424b60a90 fix: cast progress graph configuration to string (#495) (#516)
(cherry picked from commit 51e5e7126c)
2023-06-12 11:35:48 -04:00
Mashal Malik
1c0e6fd4b5 feat: upgraded to node v18, added .nvmrc and updated workflows (#464)
* feat: upgraded to node v18, added .nvmrc and updated workflows

* feat: upfate validate workflow

* feat: update validate workflow

* fix: update lock file

* refactor: update validate file

* build: update pkg

* refactor: updated packages

* build: updated frontend-build, frontend-platform, component-footer & component-header packages

* refactor: updated workflow

* refactor: updated workflow

* refactor: updated workflow

* build: update commit file

* build: update lock file

* refactor: update workflow

* refactor: update workflow

* refactor: update workflow

* refactor: update workflow

* build: update pkg

* build: update pkgs

* build: update lock file

---------

Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com>
2023-06-09 09:21:59 +02:00
Dmytro
bc3faa4105 fix: disable invalid link Video Uploads for Palm (#513) 2023-06-08 16:14:35 -04:00
connorhaugh
a94942a36e Feat update flcc for cat 2 part deux (#473)
* feat: update flcc for cat-2

* feat: updte flcc for cat-2 part deux
2023-04-10 15:55:59 -04:00
connorhaugh
ab7c51994c feat: update flcc for cat-2 (#472) 2023-04-10 10:58:59 -04:00
Raymond Zhou
67967a92cf feat: update FLCC to 1.129.0 (#471) 2023-04-06 14:12:09 -04:00
connorhaugh
6efa8c5356 feat: upgrade flcc to 125 (#470) 2023-04-05 10:24:15 -04:00
Kristin Aoki
c28669f5b2 feat: upgrade flcc package to 1.126.1 (#469) 2023-04-04 09:54:05 -04:00
kenclary
270f4a8a12 chore: upgrade frontend-lib-content-components to 1.124.0. (#468) 2023-03-27 13:57:44 -04:00
Kristin Aoki
641a169e6f feat: upgrade flcc to 1.11.8.0 (#466) 2023-03-22 14:52:22 -04:00
kenclary
25e254bbfb chore: update frontend-lib-content-components to 1.117.1. (#465) 2023-03-16 14:46:56 -04:00
Jenkins
af0ddf532a chore(i18n): update translations 2023-03-12 17:32:39 -04:00
kenclary
eaf76c8dee chore: update frontend-lib-content-components to 1.113.0. (#459) 2023-03-10 10:51:50 -05:00
Ihor Romaniuk
5c0ca7b706 feat: replace hardcoded edx string with site_name from configs (#425)
* feat: replace hardcoded edx string with site_name from configs

* feat: add ability to obtain site name dynamically

* fix: localize overriding createPortal method
2023-03-09 13:38:22 -05:00
Mashal Malik
530b247c33 refactor: remove unused tranisfex v2 url (#457) 2023-03-06 12:18:02 +05:00
Ihor Romaniuk
a5bc86e948 feat: replace hardcoded logo with logo from configs (#426) 2023-03-03 08:29:12 -05:00
connorhaugh
9910937269 feat: flcc 1.109.2 (#456) 2023-03-02 10:48:29 -05:00
connorhaugh
1344c289df feat: update flcc (#453) 2023-02-24 11:41:11 -05:00
Feanil Patel
7f4111c12c Update standard workflow files. (#452)
* build: Creating a missing workflow file `self-assign-issue.yml`.

The .github/workflows/self-assign-issue.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.

* build: Creating a missing workflow file `add-remove-label-on-comment.yml`.

The .github/workflows/add-remove-label-on-comment.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.

* build: Updating a missing workflow file `add-depr-ticket-to-depr-board.yml`.

The .github/workflows/add-depr-ticket-to-depr-board.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-24 10:44:48 -05:00
connorhaugh
105fdea8ef feat: upgrade FLCC to 1.101.3 (#451) 2023-02-15 08:41:05 -05:00
connorhaugh
9d91e3f242 feat: update flcc to 1.99.3 (#449) 2023-02-13 12:58:00 -05:00
connorhaugh
fdcb3a5e7f feat: update FLCC to 1.99.0 (#447) 2023-02-10 10:22:48 -05:00
Bilal Qamar
86974b76a9 Fixed broken "Pages and Resources" page (#446)
* fix: updated AppHeader call

* fix: AppHeader function signature changed to react component
2023-02-09 06:45:37 -05:00
Kristin Aoki
835915750c feat: update frontend-lib-content-component 1.95.0 (#445) 2023-02-07 16:13:51 -05:00
Jenkins
fe8a125d1a chore(i18n): update translations 2023-02-05 16:32:36 -05:00
Kristin Aoki
f82e572ad2 feat: update frontend-lib-content-components (#439) 2023-02-01 15:46:13 -05:00
Kristin Aoki
8aa03496fb feat: update frontend-content-components & paragon (#438) 2023-01-31 15:21:13 -05:00
Muhammad Abdullah Waheed
3c2c347bb9 Automate Browserlist DB Update (#357)
* feat: added cron github action to auto update brwoserlist DB periodically

* refactor: used a shared script to update broswerslist DB, create PR and automerge it
2023-01-31 17:40:26 +05:00
Jenkins
0d166288cc chore(i18n): update translations 2023-01-29 16:32:35 -05:00
Bilal Qamar
f8954ef870 refactor: upgraded frontend-build version to v12
PR #322
2023-01-26 09:02:14 -03:00
Muhammad Adeel Tajamul
66afd4ddac fix: updated openedx discussion provider help text (#431)
Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
2023-01-25 07:42:29 +05:00
Kristin Aoki
a99eb8a44a feat: upgrade frontend-lib-content-components (#436) 2023-01-24 13:51:06 -05:00
kenclary
b2981318b0 chore: update version of frontend-lib-content-components to 1.85.0. (#435) 2023-01-24 10:16:03 -05:00
Jenkins
5142f3afd4 chore(i18n): update translations 2023-01-22 16:32:35 -05:00
Raymond Zhou
b7b3601337 feat: update FLCC to 1.81.0 (#432) 2023-01-20 11:55:58 -05:00
Kristin Aoki
50e5ca86c6 feat: upgrade paragon and lib-content-components (#430) 2023-01-17 16:26:11 -05:00
connorhaugh
fe9a9a37e7 feat: upgrade to 1.76.0 (#429) 2023-01-12 15:43:22 -05:00
connorhaugh
1c5ab42ea6 feat: update flcc to 1.74.0 (#428) 2023-01-12 10:53:46 -05:00
Kristin Aoki
74fcbe426d feat: upgrade lib-content-components to 1.73.0 (#419) 2023-01-06 10:41:27 -05:00
96 changed files with 5784 additions and 28669 deletions

1
.env
View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

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

View File

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

@@ -1 +1 @@
v16
18

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ const ConfirmationPopup = ({
</Card.Footer>
</Card.Body>
</Card>
);
);
ConfirmationPopup.propTypes = {
label: PropTypes.string.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 يقدم للمعلمين حلاً رقميًا تعليميًا ممتعًا لتحسين مشاركة الطلاب من خلال بناء مجتمعات متعلمين لأي نمط مساق. ",

View File

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

View File

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

View File

@@ -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&#39;e-mail",
"authoring.live.launchEmail.required": "L&#39;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",

View File

@@ -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&#39;e-mail",
"authoring.live.launchEmail.required": "L&#39;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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-constructed-context-values */
import React from 'react';
import { initializeMockApp } from '@edx/frontend-platform';

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ const SupportedFeature = ({ name }) => (
</span>
{name}
</>
);
);
SupportedFeature.propTypes = {
name: PropTypes.string.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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