diff --git a/src/generic/FieldFeedback.jsx b/src/generic/FieldFeedback.jsx
index 131814e6e..9776d8212 100644
--- a/src/generic/FieldFeedback.jsx
+++ b/src/generic/FieldFeedback.jsx
@@ -36,10 +36,10 @@ function FieldFeedback({
}
FieldFeedback.propTypes = {
- errorMessage: PropTypes.string.isRequired,
- feedbackMessage: PropTypes.string.isRequired,
errorCondition: PropTypes.bool.isRequired,
- feedbackCondition: PropTypes.bool.isRequired,
+ errorMessage: PropTypes.string,
+ feedbackMessage: PropTypes.string,
+ feedbackCondition: PropTypes.bool,
feedbackClasses: PropTypes.string,
transitionClasses: PropTypes.string,
};
@@ -47,6 +47,9 @@ FieldFeedback.propTypes = {
FieldFeedback.defaultProps = {
feedbackClasses: '',
transitionClasses: '',
+ feedbackMessage: '',
+ feedbackCondition: false,
+ errorMessage: '',
};
export default FieldFeedback;
diff --git a/src/generic/FormikControl.jsx b/src/generic/FormikControl.jsx
index 606f1989b..5fd33b5a0 100644
--- a/src/generic/FormikControl.jsx
+++ b/src/generic/FormikControl.jsx
@@ -39,13 +39,19 @@ function FormikControl({
FormikControl.propTypes = {
name: PropTypes.element.isRequired,
- label: PropTypes.element.isRequired,
- help: PropTypes.element.isRequired,
- className: PropTypes.string.isRequired,
+ label: PropTypes.element,
+ help: PropTypes.element,
+ className: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
};
+FormikControl.defaultProps = {
+ help: <>>,
+ label: <>>,
+ className: '',
+};
+
export default FormikControl;
diff --git a/src/pages-and-resources/data/thunks.js b/src/pages-and-resources/data/thunks.js
index 34c2436b9..b6fa27404 100644
--- a/src/pages-and-resources/data/thunks.js
+++ b/src/pages-and-resources/data/thunks.js
@@ -21,6 +21,7 @@ const COURSE_APPS_ORDER = [
'wiki',
'calculator',
'proctoring',
+ 'live',
'textbooks',
'custom_pages',
];
diff --git a/src/pages-and-resources/discussions/app-config-form/apps/lti/LtiConfigForm.jsx b/src/pages-and-resources/discussions/app-config-form/apps/lti/LtiConfigForm.jsx
index 35ca6038e..fb5cee6d9 100644
--- a/src/pages-and-resources/discussions/app-config-form/apps/lti/LtiConfigForm.jsx
+++ b/src/pages-and-resources/discussions/app-config-form/apps/lti/LtiConfigForm.jsx
@@ -153,7 +153,11 @@ function LtiConfigForm({ onSubmit, intl, formRef }) {
)}
-
+
);
}
diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/AppExternalLinks.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/AppExternalLinks.jsx
index 2074a8420..855224511 100644
--- a/src/pages-and-resources/discussions/app-config-form/apps/shared/AppExternalLinks.jsx
+++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/AppExternalLinks.jsx
@@ -13,6 +13,8 @@ function AppExternalLinks({
externalLinks,
intl,
providerName,
+ showLaunchIcon,
+ customClasses,
}) {
const { contactEmail, ...links } = externalLinks;
const linkTypes = Object.keys(links).filter(key => links[key]);
@@ -24,12 +26,13 @@ function AppExternalLinks({
{intl.formatMessage(messages.linkTextHeading)}
{linkTypes.map((type) => (
-
+
{ intl.formatMessage(messages[type], { providerName }) }
@@ -38,7 +41,7 @@ function AppExternalLinks({
>
) : null}
{contactEmail && (
-
+
- { contactEmail }
+ { contactEmail }
),
}}
@@ -69,6 +72,13 @@ AppExternalLinks.propTypes = {
}).isRequired,
providerName: PropTypes.string.isRequired,
intl: intlShape.isRequired,
+ showLaunchIcon: PropTypes.bool,
+ customClasses: PropTypes.string,
+};
+
+AppExternalLinks.defaultProps = {
+ showLaunchIcon: false,
+ customClasses: '',
};
export default injectIntl(AppExternalLinks);
diff --git a/src/pages-and-resources/discussions/app-config-form/utils.js b/src/pages-and-resources/discussions/app-config-form/utils.js
index e521aab98..6e33efdf4 100644
--- a/src/pages-and-resources/discussions/app-config-form/utils.js
+++ b/src/pages-and-resources/discussions/app-config-form/utils.js
@@ -7,9 +7,10 @@ export const filterItemFromObject = (array, key, value) => (
array.filter(item => item[key] !== value)
);
-export const checkFieldErrors = (touched, errors, fieldPath, propertyName) => Boolean(
- getIn(errors, `${fieldPath}.${propertyName}`) && getIn(touched, `${fieldPath}.${propertyName}`),
-);
+export const checkFieldErrors = (touched, errors, fieldPath, propertyName) => {
+ const path = fieldPath ? `${fieldPath}.${propertyName}` : propertyName;
+ return Boolean(getIn(errors, path) && getIn(touched, path));
+};
export const errorExists = (errors, fieldPath, propertyName) => getIn(errors, `${fieldPath}.${propertyName}`);
diff --git a/src/pages-and-resources/live/Settings.jsx b/src/pages-and-resources/live/Settings.jsx
new file mode 100644
index 000000000..986fa59d5
--- /dev/null
+++ b/src/pages-and-resources/live/Settings.jsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import * as Yup from 'yup';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { SelectableBox, Icon } from '@edx/paragon';
+import { camelCase } from 'lodash';
+import FormikControl from '../../generic/FormikControl';
+import { useAppSetting } from '../../utils';
+import AppExternalLinks from '../discussions/app-config-form/apps/shared/AppExternalLinks';
+import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
+import iconsSrc from './constants';
+import messages from './messages';
+
+function LiveSettings({
+ intl,
+ onClose,
+}) {
+ const [liveConfiguration, saveSettings] = useAppSetting('liveConfiguration');
+ const liveData = {
+ consumerKey: liveConfiguration?.consumerKey || '',
+ consumerSecret: liveConfiguration?.consumerSecret || '',
+ launchUrl: liveConfiguration?.launchUrl || '',
+ launchEmail: liveConfiguration?.launchEmail || '',
+ provider: liveConfiguration?.provider || 'zoom',
+ piiSharingEnable: liveConfiguration?.piiSharing
+ ? liveConfiguration.piiShareUsername && liveConfiguration.piiShareEmail
+ : false,
+ };
+ const validationSchema = {
+ enabled: Yup.boolean(),
+ consumerKey: Yup.string().required(intl.formatMessage(messages.consumerKeyRequired)),
+ consumerSecret: Yup.string().required(intl.formatMessage(messages.consumerSecretRequired)),
+ launchUrl: Yup.string().required(intl.formatMessage(messages.launchUrlRequired)),
+ launchEmail: Yup.string().required(intl.formatMessage(messages.launchEmailRequired)),
+ };
+
+ const handleSettingsSave = async (values) => saveSettings(values);
+ const handleProviderChange = (selectedProvider, setFieldValue) => {
+ setFieldValue('provider', selectedProvider);
+ };
+
+ return (
+
+ {
+ ({ values, setFieldValue }) => (
+ <>
+ {intl.formatMessage(messages.selectProvider)}
+ handleProviderChange(event.target.value, setFieldValue)}
+ name="provider"
+ columns={3}
+ className="mb-3"
+ >
+ {[{ id: 'zoom' }, { id: 'google meet' }, { id: 'microsoft teams' }].map((app) => (
+
+
+
+ {intl.formatMessage(messages[`appName-${camelCase(app.id)}`])}
+
+
+ ))}
+
+ {intl.formatMessage(messages.providerHelperText, { providerName: 'Zoom' })}
+ {liveData.piiSharingEnable ? (
+ <>
+ {intl.formatMessage(messages.formInstructions)}
+
+
+
+
+
+ >
+ ) : (
+ {intl.formatMessage(messages.requestPiiSharingEnable)}
+ )}
+ >
+ )
+ }
+
+ );
+}
+
+LiveSettings.propTypes = {
+ intl: intlShape.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+export default injectIntl(LiveSettings);
diff --git a/src/pages-and-resources/live/constants.js b/src/pages-and-resources/live/constants.js
new file mode 100644
index 000000000..cf8a2094b
--- /dev/null
+++ b/src/pages-and-resources/live/constants.js
@@ -0,0 +1,9 @@
+import { GoogleMeet, MicrosoftTeams, Zoom } from '@edx/paragon/icons';
+
+const iconsSrc = {
+ googleMeet: GoogleMeet,
+ microsoftTeams: MicrosoftTeams,
+ zoom: Zoom,
+};
+
+export { iconsSrc as default };
diff --git a/src/pages-and-resources/live/messages.js b/src/pages-and-resources/live/messages.js
new file mode 100644
index 000000000..636a8e7de
--- /dev/null
+++ b/src/pages-and-resources/live/messages.js
@@ -0,0 +1,146 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ heading: {
+ id: 'authoring.pagesAndResources.live.enableLive.heading',
+ defaultMessage: 'Configure Live',
+ description: 'Heading for live configuration',
+ },
+ enableLiveLabel: {
+ id: 'authoring.pagesAndResources.live.enableLive.label',
+ defaultMessage: 'Live',
+ description: 'Title for configuration',
+ },
+ enableLiveHelp: {
+ id: 'authoring.pagesAndResources.live.enableLive.help',
+ defaultMessage: 'Schedule meetings and conduct live course sessions with learners.',
+ description: 'Tells the purpose of live configuration',
+ },
+ enableLiveLink: {
+ id: 'authoring.pagesAndResources.live.enableLive.link',
+ defaultMessage: 'Learn more about live',
+ description: 'Link text that tells the user to learn about the live',
+ },
+ saveButton: {
+ id: 'authoring.discussions.saveButton',
+ defaultMessage: 'Save',
+ description: 'Button allowing the user to submit their discussion configuration.',
+ },
+ savingButton: {
+ id: 'authoring.discussions.savingButton',
+ defaultMessage: 'Saving',
+ description: 'Button label when the discussion configuration is being submitted.',
+ },
+ savedButton: {
+ id: 'authoring.discussions.savedButton',
+ defaultMessage: 'Saved',
+ description: 'Button label when the discussion configuration has been successfully submitted.',
+ },
+ selectProvider: {
+ id: 'authoring.live.selectProvider',
+ defaultMessage: 'Select a video conferencing tool',
+ description: '',
+ },
+ formInstructions: {
+ id: 'authoring.live.formInstructions',
+ defaultMessage: 'Complete the fields below to set up your video conferencing tool.',
+ description: 'Instruction for configure the video conferencing tool.',
+ },
+ consumerKey: {
+ id: 'authoring.live.consumerKey',
+ defaultMessage: 'Consumer Key',
+ description: 'Label for the Consumer Key field.',
+ },
+ consumerKeyRequired: {
+ id: 'authoring.live.consumerKey.required',
+ defaultMessage: 'Consumer key is a required field',
+ description: 'Tells the user that the Consumer Key field is required and must have a value.',
+ },
+ consumerSecret: {
+ id: 'authoring.live.consumerSecret',
+ defaultMessage: 'Consumer Secret',
+ description: 'Label for the Consumer Secret field.',
+ },
+ consumerSecretRequired: {
+ id: 'authoring.live.consumerSecret.required',
+ defaultMessage: 'Consumer secret is a required field',
+ description: 'Tells the user that the Consumer Secret field is required and must have a value.',
+ },
+ launchUrl: {
+ id: 'authoring.live.launchUrl',
+ defaultMessage: 'Launch URL',
+ description: 'Label for the Launch URL field.',
+ },
+ launchUrlRequired: {
+ id: 'authoring.live.launchUrl.required',
+ defaultMessage: 'Launch URL is a required field',
+ description: 'Tells the user that the Launch URL field is required and must have a value.',
+ },
+ launchEmail: {
+ id: 'authoring.live.launchEmail',
+ defaultMessage: 'Launch Email',
+ description: 'Label for the Launch Email field.',
+ },
+ launchEmailRequired: {
+ id: 'authoring.live.launchEmail.required',
+ defaultMessage: 'Launch Email is a required field',
+ description: 'Tells the user that the Launch Email field is required and must have a value.',
+ },
+ providerHelperText: {
+ id: 'authoring.live.provider.helpText',
+ defaultMessage: 'This configuration will require sharing username and emails of learners and the course team with {providerName}',
+ description: 'Tells the user that sharing username and email is required for configuration',
+ },
+ requestPiiSharingEnable: {
+ id: 'authoring.live.requestPiiSharingEnable',
+ defaultMessage: 'Request your edX support team to enable the PII sharing for this course, in order to access the LTI configurations for a provider',
+ description: 'Tells the user that request edx support team to enable the PII sharing to access the LTO configuration for a provider.',
+ },
+ general: {
+ id: 'authoring.live.appDocInstructions.documentationLink',
+ defaultMessage: 'General documentation',
+ description: 'Application Document Instructions message for documentation link',
+ },
+ accessibility: {
+ id: 'authoring.live.appDocInstructions.accessibilityDocumentationLink',
+ defaultMessage: 'Accessibility documentation',
+ description: 'Application Document Instructions message for accessibility link',
+ },
+ configuration: {
+ id: 'authoring.live.appDocInstructions.configurationLink',
+ defaultMessage: 'Configuration documentation',
+ description: 'Application Document Instructions message for configurations link',
+ },
+ learnMore: {
+ id: 'authoring.live.appDocInstructions.learnMoreLink',
+ defaultMessage: 'Learn more about {providerName}',
+ description: 'Application Document Instructions message for learn more links',
+ },
+ linkTextHeading: {
+ id: 'authoring.live.appDocInstructions.linkTextHeading',
+ defaultMessage: 'External help and documentation',
+ description: 'External help and documentation heading',
+ },
+ linkText: {
+ id: 'authoring.live.appDocInstructions.linkText',
+ defaultMessage: '{link}',
+ description: 'link',
+ },
+ 'appName-zoom': {
+ id: 'authoring.live.appName-yellowdig',
+ defaultMessage: 'Zoom',
+ description: 'The name of the Zoom app.',
+ },
+ 'appName-googleMeet': {
+ id: 'authoring.live.appName-googleMeet',
+ defaultMessage: 'Google Meet',
+ description: 'The name of the Google Meet app.',
+ },
+ 'appName-microsoftTeams': {
+ id: 'authoring.live.appName-microsoftTeams',
+ defaultMessage: 'Microsoft Teams',
+ description: 'The name of the Microsoft Teams app.',
+ },
+});
+
+export default messages;