chore: add slot to allow additional course app plugin (#941)
* chore: add @openedx/frontend-plugin-framework chore: move plugin page setting button to a props chore: split out app setting modal for reusability chore: add implementation of WTC plugin chore: update app setting form chore: implement the plugin form with mock chore: follow the UI design chore: remove translation plugin and move it into frontend-plugin instead * chore: add eslint ignore for env.config.jsx * chore: update package-lock.json
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Alert,
|
||||
Badge,
|
||||
Form,
|
||||
Hyperlink,
|
||||
ModalDialog,
|
||||
StatefulButton,
|
||||
TransitionReplace,
|
||||
} from '@openedx/paragon';
|
||||
@@ -31,81 +29,10 @@ import { updateSavingStatus } from '../data/slice';
|
||||
import { updateAppStatus } from '../data/thunks';
|
||||
import AppConfigFormDivider from '../discussions/app-config-form/apps/shared/AppConfigFormDivider';
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
import AppSettingsModalBase from './AppSettingsModalBase';
|
||||
import messages from './messages';
|
||||
|
||||
const AppSettingsForm = ({
|
||||
formikProps, children, showForm,
|
||||
}) => children && (
|
||||
<TransitionReplace>
|
||||
{showForm ? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="app-disabled" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
|
||||
AppSettingsForm.propTypes = {
|
||||
// Ignore the warning here since we're just passing along the props as-is and the child component should validate
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
formikProps: PropTypes.object.isRequired,
|
||||
showForm: PropTypes.bool.isRequired,
|
||||
children: PropTypes.func,
|
||||
};
|
||||
|
||||
AppSettingsForm.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
const AppSettingsModalBase = ({
|
||||
intl, title, onClose, variant, isMobile, children, footer,
|
||||
}) => (
|
||||
<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,
|
||||
title: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
variant: PropTypes.oneOf(['default', 'dark']).isRequired,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
footer: PropTypes.node,
|
||||
};
|
||||
|
||||
AppSettingsModalBase.defaultProps = {
|
||||
footer: null,
|
||||
};
|
||||
|
||||
const AppSettingsModal = ({
|
||||
intl,
|
||||
appId,
|
||||
title,
|
||||
children,
|
||||
@@ -121,6 +48,7 @@ const AppSettingsModal = ({
|
||||
enableReinitialize,
|
||||
hideAppToggle,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseId } = useContext(PagesAndResourcesContext);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const updateSettingsRequestStatus = useSelector(getSavingStatus);
|
||||
@@ -199,14 +127,12 @@ const AppSettingsModal = ({
|
||||
onClose={onClose}
|
||||
variant={modalVariant}
|
||||
isMobile={isMobile}
|
||||
isFullscreenOnMobile
|
||||
intl={intl}
|
||||
footer={(
|
||||
<StatefulButton
|
||||
labels={{
|
||||
default: intl.formatMessage(messages.save),
|
||||
pending: intl.formatMessage(messages.saving),
|
||||
complete: intl.formatMessage(messages.saved),
|
||||
default: formatMessage(messages.save),
|
||||
pending: formatMessage(messages.saving),
|
||||
complete: formatMessage(messages.saved),
|
||||
}}
|
||||
state={submitButtonState}
|
||||
onClick={handleFormikSubmit(formikProps)}
|
||||
@@ -216,9 +142,9 @@ const AppSettingsModal = ({
|
||||
{saveError && (
|
||||
<Alert variant="danger" icon={Info} ref={alertRef}>
|
||||
<Alert.Heading>
|
||||
{formikProps.errors.enabled?.title || intl.formatMessage(messages.errorSavingTitle)}
|
||||
{formikProps.errors.enabled?.title || formatMessage(messages.errorSavingTitle)}
|
||||
</Alert.Heading>
|
||||
{formikProps.errors.enabled?.message || intl.formatMessage(messages.errorSavingMessage)}
|
||||
{formikProps.errors.enabled?.message || formatMessage(messages.errorSavingMessage)}
|
||||
</Alert>
|
||||
)}
|
||||
{!hideAppToggle && (
|
||||
@@ -233,7 +159,7 @@ const AppSettingsModal = ({
|
||||
{enableAppLabel}
|
||||
{formikProps.values.enabled && (
|
||||
<Badge className="ml-2" variant="success" data-testid="enable-badge">
|
||||
{intl.formatMessage(messages.enabled)}
|
||||
{formatMessage(messages.enabled)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -249,9 +175,19 @@ const AppSettingsModal = ({
|
||||
{bodyChildren}
|
||||
{(formikProps.values.enabled || configureBeforeEnable) && children
|
||||
&& <AppConfigFormDivider marginAdj={{ default: 0, sm: 0 }} />}
|
||||
<AppSettingsForm formikProps={formikProps} showForm={formikProps.values.enabled || configureBeforeEnable}>
|
||||
{children}
|
||||
</AppSettingsForm>
|
||||
{
|
||||
children && (
|
||||
<TransitionReplace>
|
||||
{formikProps.values.enabled || configureBeforeEnable ? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="app-disabled" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
)
|
||||
}
|
||||
</AppSettingsModalBase>
|
||||
</Form>
|
||||
)}
|
||||
@@ -260,7 +196,6 @@ const AppSettingsModal = ({
|
||||
}
|
||||
return (
|
||||
<AppSettingsModalBase
|
||||
intl={intl}
|
||||
title={title}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
@@ -277,7 +212,6 @@ const AppSettingsModal = ({
|
||||
};
|
||||
|
||||
AppSettingsModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
appId: PropTypes.string.isRequired,
|
||||
children: PropTypes.func,
|
||||
@@ -306,4 +240,4 @@ AppSettingsModal.defaultProps = {
|
||||
hideAppToggle: false,
|
||||
};
|
||||
|
||||
export default injectIntl(AppSettingsModal);
|
||||
export default AppSettingsModal;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, ModalDialog } from '@openedx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AppSettingsModalBase = ({
|
||||
title,
|
||||
onClose,
|
||||
variant,
|
||||
isMobile,
|
||||
children,
|
||||
footer,
|
||||
isOpen,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<ModalDialog
|
||||
title={title}
|
||||
isOpen={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">
|
||||
{formatMessage(messages.cancel)}
|
||||
</ModalDialog.CloseButton>
|
||||
{footer}
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
AppSettingsModalBase.defaultProps = {
|
||||
isOpen: true,
|
||||
};
|
||||
|
||||
AppSettingsModalBase.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
variant: PropTypes.oneOf(['default', 'dark']).isRequired,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
footer: PropTypes.node,
|
||||
isOpen: PropTypes.bool,
|
||||
};
|
||||
|
||||
AppSettingsModalBase.defaultProps = {
|
||||
footer: null,
|
||||
};
|
||||
|
||||
export default AppSettingsModalBase;
|
||||
@@ -1,15 +1,11 @@
|
||||
import classNames from 'classnames';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Badge, Card, Icon, IconButton, Hyperlink,
|
||||
} from '@openedx/paragon';
|
||||
import { ArrowForward, Settings } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Badge, Card } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import messages from '../messages';
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
import { useIsDesktop } from '../../utils';
|
||||
import PageSettingButton from './PageSettingButton';
|
||||
import './PageCard.scss';
|
||||
|
||||
const CoursePageShape = PropTypes.shape({
|
||||
@@ -19,45 +15,21 @@ const CoursePageShape = PropTypes.shape({
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
legacyLink: PropTypes.string,
|
||||
allowedOperations: PropTypes.shape({
|
||||
enable: PropTypes.bool.isRequired,
|
||||
configure: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
enable: PropTypes.bool,
|
||||
configure: PropTypes.bool,
|
||||
}),
|
||||
});
|
||||
|
||||
export { CoursePageShape };
|
||||
|
||||
const PageCard = ({
|
||||
intl,
|
||||
page,
|
||||
settingButton,
|
||||
}) => {
|
||||
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
|
||||
const { formatMessage } = useIntl();
|
||||
const isDesktop = useIsDesktop();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
const SettingsButton = () => {
|
||||
if (page.legacyLink) {
|
||||
return (
|
||||
<Hyperlink destination={page.legacyLink}>
|
||||
<IconButton
|
||||
src={ArrowForward}
|
||||
iconAs={Icon}
|
||||
size="inline"
|
||||
alt={intl.formatMessage(messages.settings)}
|
||||
/>
|
||||
</Hyperlink>
|
||||
);
|
||||
}
|
||||
return (page.allowedOperations.configure || page.allowedOperations.enable) && (
|
||||
<IconButton
|
||||
src={Settings}
|
||||
iconAs={Icon}
|
||||
size="inline"
|
||||
alt={intl.formatMessage(messages.settings)}
|
||||
onClick={() => navigate(`${pagesAndResourcesPath}/${page.id}/settings`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const SettingButton = settingButton || <PageSettingButton {...page} />;
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -70,10 +42,10 @@ const PageCard = ({
|
||||
title={page.name}
|
||||
subtitle={page.enabled && (
|
||||
<Badge variant="success" className="mt-1">
|
||||
{intl.formatMessage(messages.enabled)}
|
||||
{formatMessage(messages.enabled)}
|
||||
</Badge>
|
||||
)}
|
||||
actions={<div className="mt-1"><SettingsButton /></div>}
|
||||
actions={<div className="mt-1">{SettingButton}</div>}
|
||||
size="sm"
|
||||
/>
|
||||
<Card.Body>
|
||||
@@ -85,9 +57,13 @@ const PageCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
PageCard.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
page: CoursePageShape.isRequired,
|
||||
PageCard.defaultProps = {
|
||||
settingButton: null,
|
||||
};
|
||||
|
||||
export default injectIntl(PageCard);
|
||||
PageCard.propTypes = {
|
||||
page: CoursePageShape.isRequired,
|
||||
settingButton: PropTypes.node,
|
||||
};
|
||||
|
||||
export default PageCard;
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { CardGrid } from '@openedx/paragon';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import PageCard, { CoursePageShape } from './PageCard';
|
||||
|
||||
const PageGrid = ({ pages }) => (
|
||||
@@ -15,6 +16,7 @@ const PageGrid = ({ pages }) => (
|
||||
{pages.map((page) => (
|
||||
<PageCard page={page} key={page.id} />
|
||||
))}
|
||||
<PluginSlot id="additional_course_plugin" />
|
||||
</CardGrid>
|
||||
);
|
||||
|
||||
|
||||
59
src/pages-and-resources/pages/PageSettingButton.jsx
Normal file
59
src/pages-and-resources/pages/PageSettingButton.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton, Hyperlink } from '@openedx/paragon';
|
||||
import { ArrowForward, Settings } from '@openedx/paragon/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import messages from '../messages';
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
|
||||
const PageSettingButton = ({
|
||||
id,
|
||||
legacyLink,
|
||||
allowedOperations,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (legacyLink) {
|
||||
return (
|
||||
<Hyperlink destination={legacyLink}>
|
||||
<IconButton
|
||||
src={ArrowForward}
|
||||
iconAs={Icon}
|
||||
size="inline"
|
||||
alt={formatMessage(messages.settings)}
|
||||
/>
|
||||
</Hyperlink>
|
||||
);
|
||||
} if (!(allowedOperations?.configure || allowedOperations?.enable)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<IconButton
|
||||
src={Settings}
|
||||
iconAs={Icon}
|
||||
size="inline"
|
||||
alt={formatMessage(messages.settings)}
|
||||
onClick={() => navigate(`${pagesAndResourcesPath}/${id}/settings`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
PageSettingButton.defaultProps = {
|
||||
legacyLink: null,
|
||||
allowedOperations: null,
|
||||
};
|
||||
|
||||
PageSettingButton.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
legacyLink: PropTypes.string,
|
||||
allowedOperations: PropTypes.shape({
|
||||
configure: PropTypes.bool,
|
||||
enable: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default PageSettingButton;
|
||||
Reference in New Issue
Block a user