Adds Legacy edX Discussions configuration UI (#55)
* feat: Implement edX forums advanced setting editor This change implements the UX for the advanced settings editor for the internal edX forums. * Stepper now intelligently shows/hides drop shadows on the header and footer Also organized the component into a more Paragon-like organization with the sub-components hanging off the main one. * Organizing full screen modal in a more Paragon-like way. Also fixing a few minor styling issues. * Massaging SettingsEditor into LegacyConfigForm - Reorganizing into apps/legacy directory from settings/base-forum - Using Formik and Yup for managing form data - Refactoring FormSwitchGroup a little and adding data-related properties - Also moving FormSwitchGroup into ‘generic’ - Some initial attempts at error validation in LegacyConfigForm (a blackout dates regex which doesn’t seem to work yet) - Sub-sections of config now fold and animate when their parent is toggled. * Minor naming and refactoring of Discussions component - Event handlers should be named handle*, not on*. Oops. Got over-zealous. - Organizing paths into some variables at the top of the component. The pages and resources path should probably be passed in. * Hooking up and organizing LTI and Legacy forms - The LTI form moves into app/lti - Adds in rendering of the legacy discussions form. - Splits up the messages file a bit (app/lti/messages.js exists now) - Removing unnecessary h1 in the LtiConfigForm. * Removing ‘info’ blue coloring from the pages & resources view. Co-authored-by: Kshitij Sobti <kshitij@sobti.in>
This commit is contained in:
40
src/generic/FormSwitchGroup.jsx
Normal file
40
src/generic/FormSwitchGroup.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
export default function FormSwitchGroup({
|
||||
id, label, helpText, className, onChange, onBlur, checked,
|
||||
}) {
|
||||
const helpTextId = `${id}HelpText`;
|
||||
return (
|
||||
<Form.Group className={className}>
|
||||
<div className="d-flex justify-content-between">
|
||||
<Form.Label>
|
||||
{label}
|
||||
</Form.Label>
|
||||
<Form.Switch
|
||||
id={id}
|
||||
aria-describedby={helpTextId}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
checked={checked}
|
||||
/>
|
||||
</div>
|
||||
<Form.Text id={helpTextId} muted>
|
||||
{helpText}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
FormSwitchGroup.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
helpText: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func.isRequired,
|
||||
checked: PropTypes.bool.isRequired,
|
||||
};
|
||||
FormSwitchGroup.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function FullScreenModalBody({ children }) {
|
||||
export default function Body({ children }) {
|
||||
return (
|
||||
<div className="flex-grow-1 overflow-auto">
|
||||
{children}
|
||||
@@ -9,6 +9,6 @@ export default function FullScreenModalBody({ children }) {
|
||||
);
|
||||
}
|
||||
|
||||
FullScreenModalBody.propTypes = {
|
||||
Body.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ModalLayer } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import Header from './Header';
|
||||
import Body from './Body';
|
||||
|
||||
export default function FullScreenModal({ children, title, onClose }) {
|
||||
return (
|
||||
@@ -28,3 +30,6 @@ FullScreenModal.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
FullScreenModal.Header = Header;
|
||||
FullScreenModal.Body = Body;
|
||||
|
||||
@@ -4,19 +4,18 @@ import { ModalCloseButton } from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function FullScreenModalHeader({ className, title }) {
|
||||
export default function Header({ className, title }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-primary',
|
||||
'text-white',
|
||||
'd-flex',
|
||||
'justify-content-between',
|
||||
'align-items-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<h2 className="pl-3 h6 mb-0">{title}</h2>
|
||||
<h3 className="text-white pl-3 mb-0">{title}</h3>
|
||||
<ModalCloseButton variant="outline-link" className="text-white">
|
||||
<Close />
|
||||
</ModalCloseButton>
|
||||
@@ -24,11 +23,11 @@ export default function FullScreenModalHeader({ className, title }) {
|
||||
);
|
||||
}
|
||||
|
||||
FullScreenModalHeader.propTypes = {
|
||||
Header.propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
FullScreenModalHeader.defaultProps = {
|
||||
Header.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
1
src/generic/full-screen-modal/index.js
Normal file
1
src/generic/full-screen-modal/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './FullScreenModal';
|
||||
47
src/generic/stepper/Body.jsx
Normal file
47
src/generic/stepper/Body.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useRef,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { StepperContext } from './StepperContext';
|
||||
|
||||
export default function Body({ children, className }) {
|
||||
const { setIsAtTop, setIsAtBottom } = useContext(StepperContext);
|
||||
|
||||
const ref = useRef();
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
setIsAtTop(ref.current.scrollTop === 0);
|
||||
setIsAtBottom(ref.current.offsetHeight + ref.current.scrollTop === ref.current.scrollHeight);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref) {
|
||||
setIsAtTop(ref.current.scrollTop === 0);
|
||||
setIsAtBottom(ref.current.offsetHeight + ref.current.scrollTop === ref.current.scrollHeight);
|
||||
}
|
||||
}, [ref.current ? ref.current.scrollHeight : 0]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'overflow-auto',
|
||||
'flex-grow-1',
|
||||
className,
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Body.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
Body.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { StepperContext } from './StepperContext';
|
||||
|
||||
export default function StepperFooter({ children, className }) {
|
||||
export default function Footer({ children, className }) {
|
||||
const { isAtBottom } = useContext(StepperContext);
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -12,10 +14,12 @@ export default function StepperFooter({ children, className }) {
|
||||
// above the content in the div with our content.
|
||||
'position-relative',
|
||||
'w-100',
|
||||
'border-top',
|
||||
'border-light',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
boxShadow: '0 -0.25rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||
boxShadow: isAtBottom ? null : '0 -0.25rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -23,11 +27,11 @@ export default function StepperFooter({ children, className }) {
|
||||
);
|
||||
}
|
||||
|
||||
StepperFooter.propTypes = {
|
||||
Footer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
StepperFooter.defaultProps = {
|
||||
Footer.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
69
src/generic/stepper/Header.jsx
Normal file
69
src/generic/stepper/Header.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Icon from './Icon';
|
||||
import Line from './Line';
|
||||
import { StepperContext } from './StepperContext';
|
||||
|
||||
export default function Header({ steps, className }) {
|
||||
const { isAtTop } = useContext(StepperContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-2',
|
||||
'd-flex',
|
||||
'border-bottom',
|
||||
'border-light',
|
||||
'justify-content-center',
|
||||
'align-items-center',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
// zIndex raises this div to a higher 'layer' so that its drop shadow falls
|
||||
// above the content in the Body component. Without this, any backgrounds in the Body
|
||||
// cut off the drop shadow as if they're 'higher' than it.
|
||||
zIndex: '1',
|
||||
boxShadow: isAtTop ? null : '0 0.25rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
{steps.map(({ iconLabel, label, incomplete }, index) => {
|
||||
const isNotLastStep = index < steps.length - 1;
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<React.Fragment key={`${index}-${label}`}>
|
||||
<Icon className={incomplete ? 'bg-light-900' : 'bg-primary'}>
|
||||
{iconLabel || index + 1}
|
||||
</Icon>
|
||||
<span className={classNames(
|
||||
'font-weight-bold',
|
||||
{ 'text-light-900': incomplete },
|
||||
)}
|
||||
>{label}
|
||||
</span>
|
||||
{isNotLastStep && (
|
||||
<Line />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
steps: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
iconLabel: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
]),
|
||||
label: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
@@ -1,10 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function StepIcon({ children, size }) {
|
||||
export default function Icon({ children, size, className }) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-circle small mr-2 bg-primary text-white d-flex justify-content-center align-items-center"
|
||||
className={classNames(
|
||||
'rounded-circle',
|
||||
'small',
|
||||
'mr-2',
|
||||
'text-white',
|
||||
'd-flex',
|
||||
'justify-content-center',
|
||||
'align-items-center',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
// TODO: Is there a better way to lock the shape of this thing?
|
||||
width: size,
|
||||
@@ -20,11 +30,13 @@ export default function StepIcon({ children, size }) {
|
||||
);
|
||||
}
|
||||
|
||||
StepIcon.propTypes = {
|
||||
Icon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
size: PropTypes.string,
|
||||
};
|
||||
|
||||
StepIcon.defaultProps = {
|
||||
Icon.defaultProps = {
|
||||
className: null,
|
||||
size: '1.3rem',
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function StepLine() {
|
||||
export default function Line() {
|
||||
return (
|
||||
<div className="border-bottom mx-2" style={{ width: '5rem' }} />
|
||||
);
|
||||
@@ -1,6 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Icon from './Icon';
|
||||
import Line from './Line';
|
||||
import Body from './Body';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
import { StepperContextProvider } from './StepperContext';
|
||||
|
||||
export default function Stepper({ children, className }) {
|
||||
return (
|
||||
@@ -11,7 +17,9 @@ export default function Stepper({ children, className }) {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<StepperContextProvider>
|
||||
{children}
|
||||
</StepperContextProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,3 +32,9 @@ Stepper.propTypes = {
|
||||
Stepper.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
|
||||
Stepper.Icon = Icon;
|
||||
Stepper.Line = Line;
|
||||
Stepper.Body = Body;
|
||||
Stepper.Footer = Footer;
|
||||
Stepper.Header = Header;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function StepperBody({ children, className }) {
|
||||
return (
|
||||
<div className={classNames(
|
||||
'overflow-auto',
|
||||
'flex-grow-1',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StepperBody.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
StepperBody.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
28
src/generic/stepper/StepperContext.jsx
Normal file
28
src/generic/stepper/StepperContext.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const StepperContext = React.createContext({
|
||||
isAtTop: true,
|
||||
isAtBottom: false,
|
||||
});
|
||||
|
||||
export function StepperContextProvider({ children }) {
|
||||
const [isAtTop, setIsAtTop] = useState(true);
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
return (
|
||||
<StepperContext.Provider
|
||||
value={{
|
||||
isAtTop,
|
||||
isAtBottom,
|
||||
setIsAtTop,
|
||||
setIsAtBottom,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StepperContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
StepperContextProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import StepIcon from './StepIcon';
|
||||
import StepLine from './StepLine';
|
||||
|
||||
export default function StepperHeader({ steps, className }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-2',
|
||||
'd-flex',
|
||||
'border-bottom',
|
||||
'border-light',
|
||||
'justify-content-center',
|
||||
'align-items-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{steps.map(({ iconLabel, label }, index) => {
|
||||
const isNotLastStep = index < steps.length - 1;
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<React.Fragment key={`${index}-${label}`}>
|
||||
<StepIcon>
|
||||
{iconLabel || index + 1}
|
||||
</StepIcon>
|
||||
<span className="font-weight-bold">{label}</span>
|
||||
{isNotLastStep && (
|
||||
<StepLine />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StepperHeader.propTypes = {
|
||||
steps: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
iconLabel: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
]),
|
||||
label: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
StepperHeader.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
1
src/generic/stepper/index.js
Normal file
1
src/generic/stepper/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Stepper';
|
||||
@@ -30,9 +30,9 @@ function PagesAndResources({ courseId, intl }) {
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="container-fluid bg-info-100 pb-3">
|
||||
<div className="container-fluid pb-3">
|
||||
<div className="d-flex justify-content-between align-items-center border-bottom">
|
||||
<h1 className="mt-3 text-info-500">{intl.formatMessage(messages.heading)}</h1>
|
||||
<h1 className="mt-3">{intl.formatMessage(messages.heading)}</h1>
|
||||
<a className="btn btn-primary" href={lmsCourseURL} role="button">
|
||||
{intl.formatMessage(messages['viewLive.button'])}
|
||||
</a>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useRouteMatch } from 'react-router';
|
||||
import { Card, Container } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
import LtiConfigForm from './LtiConfigForm';
|
||||
import { fetchAppConfig } from './data/thunks';
|
||||
import LegacyConfigForm from './apps/legacy';
|
||||
import LtiConfigForm from './apps/lti';
|
||||
import messages from './messages';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export default function ConfigFormContainer({
|
||||
courseId, onSubmit, formRef,
|
||||
function ConfigFormContainer({
|
||||
courseId, onSubmit, formRef, intl,
|
||||
}) {
|
||||
const { params: { appId: routeAppId } } = useRouteMatch();
|
||||
|
||||
@@ -28,13 +30,35 @@ export default function ConfigFormContainer({
|
||||
return null;
|
||||
}
|
||||
|
||||
let form = null;
|
||||
|
||||
if (app.id === 'edx-discussions') {
|
||||
form = (
|
||||
<LegacyConfigForm
|
||||
formRef={formRef}
|
||||
appConfig={appConfig}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
form = (
|
||||
<LtiConfigForm
|
||||
formRef={formRef}
|
||||
app={app}
|
||||
appConfig={appConfig}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LtiConfigForm
|
||||
formRef={formRef}
|
||||
app={app}
|
||||
appConfig={appConfig}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
<Container size="xs" className="px-sm-0">
|
||||
<h3 className="my-4">
|
||||
{intl.formatMessage(messages.configureApp, { name: app.name })}
|
||||
</h3>
|
||||
<Card className="mb-5 p-5">
|
||||
{form}
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,4 +67,7 @@ ConfigFormContainer.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
formRef: PropTypes.object.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ConfigFormContainer);
|
||||
|
||||
@@ -14,35 +14,52 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, StatefulButton } from '@edx/paragon';
|
||||
import { Check } from '@edx/paragon/icons';
|
||||
|
||||
import FullScreenModal from '../../generic/full-screen-modal/FullScreenModal';
|
||||
import Stepper from '../../generic/stepper/Stepper';
|
||||
import FullScreenModal from '../../generic/full-screen-modal';
|
||||
import Stepper from '../../generic/stepper';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
import messages from './messages';
|
||||
import AppList from './AppList';
|
||||
import ConfigFormContainer from './ConfigFormContainer';
|
||||
import messages from './messages';
|
||||
import { fetchApps, saveAppConfig } from './data/thunks';
|
||||
import StepperFooter from '../../generic/stepper/StepperFooter';
|
||||
import StepperHeader from '../../generic/stepper/StepperHeader';
|
||||
import StepperBody from '../../generic/stepper/StepperBody';
|
||||
import FullScreenModalHeader from '../../generic/full-screen-modal/FullScreenModalHeader';
|
||||
import FullScreenModalBody from '../../generic/full-screen-modal/FullScreenModalBody';
|
||||
|
||||
function Discussions({ courseId, intl }) {
|
||||
const discussionsPath = `/course/${courseId}/pages-and-resources/discussions`;
|
||||
|
||||
const [selectedAppId, setSelectedAppId] = useState(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const formRef = useRef();
|
||||
|
||||
const { path } = useRouteMatch();
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const app = useModel('apps', selectedAppId);
|
||||
|
||||
// Route paths
|
||||
// TODO: pagesAndResourcesPath should probably be passed in.
|
||||
const pagesAndResourcesPath = `/course/${courseId}/pages-and-resources`;
|
||||
const discussionsPath = `${pagesAndResourcesPath}/discussions`;
|
||||
const selectedAppConfigPath = `${discussionsPath}/configure/${selectedAppId}`;
|
||||
|
||||
const isFirstStep = pathname === discussionsPath;
|
||||
const submitButtonState = isSubmitting ? 'pending' : 'default';
|
||||
|
||||
const steps = [{
|
||||
label: intl.formatMessage(messages.selectDiscussionTool),
|
||||
iconLabel: isFirstStep ? undefined : (
|
||||
<Check style={{ width: '1rem', height: '1rem' }} />
|
||||
),
|
||||
incomplete: false,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(messages.configureApp, {
|
||||
name: app ? app.name : 'discussions',
|
||||
}),
|
||||
incomplete: isFirstStep,
|
||||
}];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchApps(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
const onSelectApp = useCallback((appId) => {
|
||||
const handleSelectApp = useCallback((appId) => {
|
||||
if (selectedAppId === appId) {
|
||||
setSelectedAppId(null);
|
||||
} else {
|
||||
@@ -50,57 +67,48 @@ function Discussions({ courseId, intl }) {
|
||||
}
|
||||
}, [selectedAppId]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
history.push(`/course/${courseId}/pages-and-resources`);
|
||||
const handleClose = useCallback(() => {
|
||||
history.push(pagesAndResourcesPath);
|
||||
}, [courseId]);
|
||||
|
||||
const onStartConfig = useCallback(() => {
|
||||
history.push(`${discussionsPath}/configure/${selectedAppId}`);
|
||||
const handleStartConfig = useCallback(() => {
|
||||
history.push(selectedAppConfigPath);
|
||||
}, [discussionsPath, selectedAppId]);
|
||||
|
||||
// This causes the form to be submitted from a button outside the form.
|
||||
const onApply = () => {
|
||||
const handleApply = () => {
|
||||
setIsSubmitting(true);
|
||||
formRef.current.requestSubmit();
|
||||
};
|
||||
|
||||
// This is a callback that gets called after the form has been submitted successfully.
|
||||
const onSubmit = useCallback((values) => {
|
||||
const handleSubmit = useCallback((values) => {
|
||||
console.log(values);
|
||||
dispatch(saveAppConfig(courseId, selectedAppId, values)).then(() => {
|
||||
history.push(`/course/${courseId}/pages-and-resources`);
|
||||
history.push(pagesAndResourcesPath);
|
||||
});
|
||||
}, [courseId, selectedAppId, courseId]);
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
history.push(discussionsPath);
|
||||
setSelectedAppId(null);
|
||||
const handleBack = useCallback(() => {
|
||||
if (isFirstStep) {
|
||||
history.push(pagesAndResourcesPath);
|
||||
} else {
|
||||
history.push(discussionsPath);
|
||||
setSelectedAppId(null);
|
||||
}
|
||||
}, [discussionsPath]);
|
||||
|
||||
const isFirstStep = pathname === discussionsPath;
|
||||
|
||||
const steps = [{
|
||||
label: 'Select discussion tool',
|
||||
iconLabel: isFirstStep ? undefined : (
|
||||
<Check style={{ width: '1rem', height: '1rem' }} />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Configure discussions',
|
||||
}];
|
||||
|
||||
const submitButtonState = isSubmitting ? 'pending' : 'default';
|
||||
|
||||
return (
|
||||
<FullScreenModal title={intl.formatMessage(messages.configure)} onClose={onClose}>
|
||||
<FullScreenModalHeader title={intl.formatMessage(messages.configure)} />
|
||||
<FullScreenModalBody className="d-flex flex-column">
|
||||
<FullScreenModal title={intl.formatMessage(messages.configure)} onClose={handleClose}>
|
||||
<FullScreenModal.Header title={intl.formatMessage(messages.configure)} />
|
||||
<FullScreenModal.Body className="d-flex flex-column">
|
||||
<Stepper className="h-100">
|
||||
<StepperHeader steps={steps} />
|
||||
<StepperBody>
|
||||
<Stepper.Header steps={steps} />
|
||||
<Stepper.Body className="bg-light-200">
|
||||
<Switch>
|
||||
<PageRoute exact path={`${path}`}>
|
||||
<AppList
|
||||
onSelectApp={onSelectApp}
|
||||
onSelectApp={handleSelectApp}
|
||||
selectedAppId={selectedAppId}
|
||||
/>
|
||||
</PageRoute>
|
||||
@@ -108,18 +116,18 @@ function Discussions({ courseId, intl }) {
|
||||
<ConfigFormContainer
|
||||
courseId={courseId}
|
||||
selectedAppId={selectedAppId}
|
||||
onSubmit={onSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
formRef={formRef}
|
||||
/>
|
||||
</PageRoute>
|
||||
</Switch>
|
||||
</StepperBody>
|
||||
<StepperFooter className="d-flex justify-content-between align-items-center">
|
||||
<Button variant="outline-primary" onClick={onBack} disabled={isFirstStep}>
|
||||
</Stepper.Body>
|
||||
<Stepper.Footer className="d-flex justify-content-end align-items-center">
|
||||
<Button variant="outline-primary" className="mr-2" onClick={handleBack}>
|
||||
{intl.formatMessage(messages.backButton)}
|
||||
</Button>
|
||||
{isFirstStep && (
|
||||
<Button variant="primary" onClick={onStartConfig} disabled={!selectedAppId}>
|
||||
<Button variant="primary" onClick={handleStartConfig} disabled={!selectedAppId}>
|
||||
{intl.formatMessage(messages.nextButton)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -132,12 +140,12 @@ function Discussions({ courseId, intl }) {
|
||||
}}
|
||||
state={submitButtonState}
|
||||
className="mr-3"
|
||||
onClick={onApply}
|
||||
onClick={handleApply}
|
||||
/>
|
||||
)}
|
||||
</StepperFooter>
|
||||
</Stepper.Footer>
|
||||
</Stepper>
|
||||
</FullScreenModalBody>
|
||||
</FullScreenModal.Body>
|
||||
</FullScreenModal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form, TransitionReplace } from '@edx/paragon';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import FormSwitchGroup from '../../../../generic/FormSwitchGroup';
|
||||
import messages from './messages';
|
||||
|
||||
function LegacyConfigForm({
|
||||
appConfig, onSubmit, intl, formRef,
|
||||
}) {
|
||||
// TODO: This regex doesn't seem to be working yet/validation isn't displaying. Unclear why.
|
||||
const blackoutDateRegex = /(\[\]|\[(\["\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}){0,1}"\])*\])/;
|
||||
const {
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
handleBlur,
|
||||
values,
|
||||
errors,
|
||||
} = useFormik({
|
||||
initialValues: appConfig,
|
||||
validationSchema: Yup.object().shape({
|
||||
blackoutDates: Yup.string().matches(blackoutDateRegex, 'bad regex'),
|
||||
|
||||
}),
|
||||
onSubmit,
|
||||
});
|
||||
|
||||
const divider = <hr className="my-2" />;
|
||||
|
||||
return (
|
||||
<Form ref={formRef} onSubmit={handleSubmit}>
|
||||
<h5>{intl.formatMessage(messages.divisionByGroup)}</h5>
|
||||
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
id="divideByCohorts"
|
||||
checked={values.divideByCohorts}
|
||||
label={intl.formatMessage(messages.divideByCohortsLabel)}
|
||||
helpText={intl.formatMessage(messages.divideByCohortsHelp)}
|
||||
/>
|
||||
<TransitionReplace>
|
||||
{values.divideByCohorts ? (
|
||||
<React.Fragment key="open">
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
className="pl-4"
|
||||
id="allowDivisionByUnit"
|
||||
checked={values.allowDivisionByUnit}
|
||||
label={intl.formatMessage(messages.allowDivisionByUnitLabel)}
|
||||
helpText={intl.formatMessage(messages.allowDivisionByUnitHelp)}
|
||||
/>
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
id="divideCourseWideTopics"
|
||||
checked={values.divideCourseWideTopics}
|
||||
label={intl.formatMessage(messages.divideCourseWideTopicsLabel)}
|
||||
helpText={intl.formatMessage(messages.divideCourseWideTopicsHelp)}
|
||||
/>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
checked={values.divideGeneralTopic}
|
||||
label="General"
|
||||
/>
|
||||
<Form.Check
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
checked={values.divideQuestionsForTAs}
|
||||
label="Questions for the TAs"
|
||||
/>
|
||||
</Form.Group>
|
||||
</React.Fragment>
|
||||
) : <React.Fragment key="closed" />}
|
||||
</TransitionReplace>
|
||||
|
||||
{divider}
|
||||
|
||||
<h5>{intl.formatMessage(messages.visibilityInContext)}</h5>
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
id="inContextDiscussion"
|
||||
checked={values.inContextDiscussion}
|
||||
label={intl.formatMessage(messages.inContextDiscussionLabel)}
|
||||
helpText={intl.formatMessage(messages.inContextDiscussionHelp)}
|
||||
/>
|
||||
<TransitionReplace>
|
||||
{values.inContextDiscussion ? (
|
||||
<React.Fragment key="open">
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
className="pl-4"
|
||||
id="gradedUnitPages"
|
||||
checked={values.gradedUnitPages}
|
||||
label={intl.formatMessage(messages.gradedUnitPagesLabel)}
|
||||
helpText={intl.formatMessage(messages.gradedUnitPagesHelp)}
|
||||
/>
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
className="pl-4"
|
||||
id="groupInContextSubsection"
|
||||
checked={values.groupInContextSubsection}
|
||||
label={intl.formatMessage(messages.groupInContextSubsectionLabel)}
|
||||
helpText={intl.formatMessage(messages.groupInContextSubsectionHelp)}
|
||||
/>
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
className="pl-4"
|
||||
id="allowUnitLevelVisibility"
|
||||
checked={values.allowUnitLevelVisibility}
|
||||
label={intl.formatMessage(messages.allowUnitLevelVisibilityLabel)}
|
||||
helpText={intl.formatMessage(messages.allowUnitLevelVisibilityHelp)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : <React.Fragment key="closed" />}
|
||||
|
||||
</TransitionReplace>
|
||||
|
||||
{divider}
|
||||
<h5>{intl.formatMessage(messages.anonymousPosting)}</h5>
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
id="allowAnonymousPosts"
|
||||
checked={values.allowAnonymousPosts}
|
||||
label={intl.formatMessage(messages.allowAnonymousPostsLabel)}
|
||||
helpText={intl.formatMessage(messages.allowAnonymousPostsHelp)}
|
||||
/>
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
id="allowAnonymousPostsPeers"
|
||||
checked={values.allowAnonymousPostsPeers}
|
||||
label={intl.formatMessage(messages.allowAnonymousPostsPeersLabel)}
|
||||
helpText={intl.formatMessage(messages.allowAnonymousPostsPeersHelp)}
|
||||
/>
|
||||
<Form.Group>
|
||||
<Form.Label>{intl.formatMessage(messages.blackoutDatesLabel)}</Form.Label>
|
||||
<Form.Control
|
||||
id="blackoutDates"
|
||||
aria-describedby="blackoutDatesHelpText"
|
||||
value={values.blackoutDates}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
className={{ 'is-invalid': !!errors.launchUrl }}
|
||||
/>
|
||||
<Form.Control.Feedback id="blackoutDatesFeedback" type="invalid">
|
||||
Bad blackout date
|
||||
</Form.Control.Feedback>
|
||||
<Form.Text id="blackoutDatesHelpText" muted>
|
||||
{intl.formatMessage(messages.blackoutDatesHelp)}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
LegacyConfigForm.propTypes = {
|
||||
appConfig: PropTypes.shape({
|
||||
divideByCohorts: PropTypes.bool.isRequired,
|
||||
allowDivisionByUnit: PropTypes.bool.isRequired,
|
||||
divideCourseWideTopics: PropTypes.bool.isRequired,
|
||||
divideGeneralTopic: PropTypes.bool.isRequired,
|
||||
divideQuestionsForTAs: PropTypes.bool.isRequired,
|
||||
inContextDiscussion: PropTypes.bool.isRequired,
|
||||
gradedUnitPages: PropTypes.bool.isRequired,
|
||||
groupInContextSubsection: PropTypes.bool.isRequired,
|
||||
allowUnitLevelVisibility: PropTypes.bool.isRequired,
|
||||
allowAnonymousPosts: PropTypes.bool.isRequired,
|
||||
allowAnonymousPostsPeers: PropTypes.bool.isRequired,
|
||||
blackoutDates: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
formRef: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LegacyConfigForm);
|
||||
1
src/pages-and-resources/discussions/apps/legacy/index.js
Normal file
1
src/pages-and-resources/discussions/apps/legacy/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './LegacyConfigForm';
|
||||
104
src/pages-and-resources/discussions/apps/legacy/messages.js
Normal file
104
src/pages-and-resources/discussions/apps/legacy/messages.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
divisionByGroup: {
|
||||
id: 'authoring.discussions.builtIn.divisionByGroup',
|
||||
defaultMessage: 'Division by group',
|
||||
},
|
||||
divideByCohortsLabel: {
|
||||
id: 'authoring.discussions.builtIn.divideByCohorts.label',
|
||||
defaultMessage: 'Divide discussions by cohorts',
|
||||
},
|
||||
divideByCohortsHelp: {
|
||||
id: 'authoring.discussions.builtIn.divideByCohorts.help',
|
||||
defaultMessage: 'Learners will only be able to view and respond to discussions posted by members of their cohort.',
|
||||
},
|
||||
allowDivisionByUnitLabel: {
|
||||
id: 'authoring.discussions.builtIn.allowDivisionByUnit.label',
|
||||
defaultMessage: 'Allow cohort division for each course unit',
|
||||
},
|
||||
allowDivisionByUnitHelp: {
|
||||
id: 'authoring.discussions.builtIn.allowDivisionByUnit.help',
|
||||
defaultMessage: 'With this advanced setting enabled, you will be able to override the global visibility, and turn the division of cohorts on or off for each unit from the course outline view.',
|
||||
},
|
||||
divideCourseWideTopicsLabel: {
|
||||
id: 'authoring.discussions.builtIn.divideCourseWideTopics.label',
|
||||
defaultMessage: 'Divide course wide discussion topics',
|
||||
},
|
||||
divideCourseWideTopicsHelp: {
|
||||
id: 'authoring.discussions.builtIn.divideCourseWideTopics.help',
|
||||
defaultMessage: 'Choose which of your general course wide discussion topics you would like to divide.',
|
||||
},
|
||||
visibilityInContext: {
|
||||
id: 'authoring.discussions.builtIn.visibilityInContext',
|
||||
defaultMessage: 'Visibility of in-context discussions',
|
||||
},
|
||||
inContextDiscussionLabel: {
|
||||
id: 'authoring.discussions.builtIn.inContextDiscussion.label',
|
||||
defaultMessage: 'In-context discussion',
|
||||
},
|
||||
inContextDiscussionHelp: {
|
||||
id: 'authoring.discussions.builtIn.inContextDiscussion.help',
|
||||
defaultMessage: 'Learners will eb able to view or hide a discussion side panel to engage with discussion on te course unit page.',
|
||||
},
|
||||
gradedUnitPagesLabel: {
|
||||
id: 'authoring.discussions.builtIn.gradedUnitPages.label',
|
||||
defaultMessage: 'Graded unit pages',
|
||||
},
|
||||
gradedUnitPagesHelp: {
|
||||
id: 'authoring.discussions.builtIn.gradedUnitPages.help',
|
||||
defaultMessage: 'Allow learners to engage with discussion on all graded unit pages except timed exams.',
|
||||
},
|
||||
groupInContextSubsectionLabel: {
|
||||
id: 'authoring.discussions.builtIn.groupInContextSubsection.label',
|
||||
defaultMessage: 'Group in context discussion at the subsection level',
|
||||
},
|
||||
groupInContextSubsectionHelp: {
|
||||
id: 'authoring.discussions.builtIn.groupInContextSubsection.help',
|
||||
defaultMessage: 'Learners will be able to view any post in the sub-section no matter which unit page they are viewing. While this is not recommended, if your course has short learning sequences or low enrollment grouping may increase engagement.',
|
||||
},
|
||||
allowUnitLevelVisibilityLabel: {
|
||||
id: 'authoring.discussions.builtIn.allowUnitLevelVisibility.label',
|
||||
defaultMessage: 'Allow visibility configuration for each course unit',
|
||||
},
|
||||
allowUnitLevelVisibilityHelp: {
|
||||
id: 'authoring.discussions.builtIn.allowUnitLevelVisibility.help',
|
||||
defaultMessage: 'With this advanced setting enabled you will be able to override the global visibility setting and turn discussions on or off for each unit from the course outline view..',
|
||||
},
|
||||
anonymousPosting: {
|
||||
id: 'authoring.discussions.builtIn.anonymousPosting',
|
||||
defaultMessage: 'Anonymous posting',
|
||||
},
|
||||
allowAnonymousPostsLabel: {
|
||||
id: 'authoring.discussions.builtIn.allowAnonymous.label',
|
||||
defaultMessage: 'Allow Anonymous Discussion Posts',
|
||||
},
|
||||
allowAnonymousPostsHelp: {
|
||||
id: 'authoring.discussions.builtIn.allowAnonymous.help',
|
||||
defaultMessage: 'Enter true or false. If true, students can create discussion posts that are anonymous to all users.',
|
||||
},
|
||||
allowAnonymousPostsPeersLabel: {
|
||||
id: 'authoring.discussions.builtIn.allowAnonymousPeers.label',
|
||||
defaultMessage: 'Allow Anonymous Discussion Posts to Peers',
|
||||
},
|
||||
allowAnonymousPostsPeersHelp: {
|
||||
id: 'authoring.discussions.builtIn.allowAnonymousPeers.help',
|
||||
defaultMessage: 'Enter true or false. If true, students can create discussion posts that are anonymous to other students. This setting does not make posts anonymous to course staff.',
|
||||
},
|
||||
blackoutDatesLabel: {
|
||||
id: 'authoring.discussions.builtIn.blackoutDates.label',
|
||||
defaultMessage: 'Discussion Blackout Dates',
|
||||
},
|
||||
blackoutDatesHelp: {
|
||||
id: 'authoring.discussions.builtIn.blackoutDates.help',
|
||||
defaultMessage:
|
||||
`Enter pairs of dates between which students cannot post to discussion forums. Inside the provided
|
||||
brackets, enter an additional set of square brackets surrounding each pair of dates you add.
|
||||
Format each pair of dates as ["YYYY-MM-DD", "YYYY-MM-DD"]. To specify times as well as dates,
|
||||
format each pair as ["YYYY-MM-DDTHH:MM", "YYYY-MM-DDTHH:MM"]. Be sure to include the "T" between
|
||||
the date and time. For example, an entry defining two blackout periods looks like this, including
|
||||
the outer pair of square brackets: [["2015-09-15", "2015-09-21"], ["2015-10-01", "2015-10-08"]]`,
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Hyperlink,
|
||||
} from '@edx/paragon';
|
||||
import { Form, Hyperlink } from '@edx/paragon';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
@@ -29,8 +27,7 @@ function LtiConfigForm({
|
||||
});
|
||||
|
||||
return (
|
||||
<Form ref={formRef} className="m-5" onSubmit={handleSubmit}>
|
||||
<h1>{intl.formatMessage(messages.configureApp, { name: app.name })}</h1>
|
||||
<Form ref={formRef} onSubmit={handleSubmit}>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="authoring.discussions.appDocInstructions"
|
||||
1
src/pages-and-resources/discussions/apps/lti/index.js
Normal file
1
src/pages-and-resources/discussions/apps/lti/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './LtiConfigForm';
|
||||
40
src/pages-and-resources/discussions/apps/lti/messages.js
Normal file
40
src/pages-and-resources/discussions/apps/lti/messages.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
documentationPage: {
|
||||
id: 'authoring.discussions.documentationPage',
|
||||
defaultMessage: 'Visit the {name} documentation page',
|
||||
},
|
||||
consumerKey: {
|
||||
id: 'authoring.discussions.consumerKey',
|
||||
defaultMessage: 'Consumer Key',
|
||||
description: 'Label for the Consumer Key field.',
|
||||
},
|
||||
consumerKeyRequired: {
|
||||
id: 'authoring.discussions.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.discussions.consumerSecret',
|
||||
defaultMessage: 'Consumer Secret',
|
||||
description: 'Label for the Consumer Secret field.',
|
||||
},
|
||||
consumerSecretRequired: {
|
||||
id: 'authoring.discussions.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.discussions.launchUrl',
|
||||
defaultMessage: 'Launch URL',
|
||||
description: 'Label for the Launch URL field.',
|
||||
},
|
||||
launchUrlRequired: {
|
||||
id: 'authoring.discussions.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.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,6 +1,6 @@
|
||||
const edXForumsApp = {
|
||||
id: 'edx-forums',
|
||||
name: 'edX Forum',
|
||||
const legacyEdXDiscussions = {
|
||||
id: 'edx-discussions',
|
||||
name: 'edX Discussions',
|
||||
logo: 'https://cdn-blog.lawrencemcdaniel.com/wp-content/uploads/2018/01/22125436/edx-logo.png',
|
||||
description: 'Start conversations with other learners, ask questions, and interact with other learners in the course.',
|
||||
supportLevel: 'Full support',
|
||||
@@ -72,7 +72,7 @@ export function getApps() {
|
||||
},
|
||||
],
|
||||
apps: [
|
||||
edXForumsApp,
|
||||
legacyEdXDiscussions,
|
||||
piazzaApp,
|
||||
yellowdigApp,
|
||||
],
|
||||
@@ -90,17 +90,37 @@ export function getAppConfig(courseId, appId) {
|
||||
app = yellowdigApp;
|
||||
break;
|
||||
default:
|
||||
app = edXForumsApp;
|
||||
app = legacyEdXDiscussions;
|
||||
}
|
||||
|
||||
let appConfig = {
|
||||
id: 'appConfig1',
|
||||
consumerSecret: 'its-a-secret-to-everybody',
|
||||
consumerKey: 'abc123',
|
||||
launchUrl: 'https://localhost/launch',
|
||||
};
|
||||
|
||||
if (appId === 'edx-discussions') {
|
||||
appConfig = {
|
||||
id: 'appConfig2',
|
||||
divideByCohorts: false,
|
||||
allowDivisionByUnit: false,
|
||||
divideCourseWideTopics: false,
|
||||
divideGeneralTopic: false,
|
||||
divideQuestionsForTAs: false,
|
||||
inContextDiscussion: false,
|
||||
gradedUnitPages: false,
|
||||
groupInContextSubsection: false,
|
||||
allowUnitLevelVisibility: false,
|
||||
allowAnonymousPosts: false,
|
||||
allowAnonymousPostsPeers: false,
|
||||
blackoutDates: '[]',
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
app,
|
||||
appConfig: {
|
||||
id: 'appConfig1',
|
||||
consumerSecret: 'its-a-secret-to-everybody',
|
||||
consumerKey: 'abc123',
|
||||
launchUrl: 'https://localhost/launch',
|
||||
},
|
||||
appConfig,
|
||||
features: [
|
||||
{
|
||||
id: 'lti',
|
||||
@@ -136,7 +156,7 @@ export function postAppConfig(courseId, appId, drafts) {
|
||||
app = yellowdigApp;
|
||||
break;
|
||||
default:
|
||||
app = edXForumsApp;
|
||||
app = legacyEdXDiscussions;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -40,40 +40,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Saved',
|
||||
description: 'Button text shown once a discussion config has been saved to the server.',
|
||||
},
|
||||
documentationPage: {
|
||||
id: 'authoring.discussions.documentationPage',
|
||||
defaultMessage: 'Visit the {name} documentation page',
|
||||
},
|
||||
consumerKey: {
|
||||
id: 'authoring.discussions.consumerKey',
|
||||
defaultMessage: 'Consumer Key',
|
||||
description: 'Label for the Consumer Key field.',
|
||||
},
|
||||
consumerSecret: {
|
||||
id: 'authoring.discussions.consumerSecret',
|
||||
defaultMessage: 'Consumer Secret',
|
||||
description: 'Label for the Consumer Secret field.',
|
||||
},
|
||||
launchUrl: {
|
||||
id: 'authoring.discussions.launchUrl',
|
||||
defaultMessage: 'Launch URL',
|
||||
description: 'Label for the Launch URL field.',
|
||||
},
|
||||
consumerKeyRequired: {
|
||||
id: 'authoring.discussions.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.',
|
||||
},
|
||||
consumerSecretRequired: {
|
||||
id: 'authoring.discussions.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.',
|
||||
},
|
||||
launchUrlRequired: {
|
||||
id: 'authoring.discussions.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.',
|
||||
},
|
||||
backButton: {
|
||||
id: 'authoring.discussions.backButton',
|
||||
defaultMessage: 'Back',
|
||||
@@ -89,6 +55,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Apply',
|
||||
description: 'Button allowing the user to submit their discussion configuration.',
|
||||
},
|
||||
selectDiscussionTool: {
|
||||
id: 'authoring.discussions.selectDiscussionTool',
|
||||
defaultMessage: 'Select discussion tool',
|
||||
description: 'A label for the first step of a wizard where the user chooses a discussion tool to configure.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -29,7 +29,7 @@ function PageCard({ intl, page }) {
|
||||
const componentClasses = classNames(
|
||||
'd-flex flex-column align-content-stretch',
|
||||
'bg-white p-3 border shadow',
|
||||
{ 'border-info-300': page.isEnabled, 'border-gray-100': !page.isEnabled },
|
||||
{ 'border-gray-500': page.isEnabled, 'border-gray-100': !page.isEnabled },
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import messages from '../messages';
|
||||
|
||||
function PageGrid({ intl, pages }) {
|
||||
return (
|
||||
<div className="text-info-500">
|
||||
<div>
|
||||
<h3 className="mt-3">
|
||||
{intl.formatMessage(messages['pages.subheading'])}
|
||||
</h3>
|
||||
|
||||
Reference in New Issue
Block a user