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:
Leangseu Kim
2024-04-24 11:55:46 -04:00
committed by GitHub
parent 98138181f7
commit e55f031c39
10 changed files with 395 additions and 209 deletions

View File

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

View File

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

View File

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

View File

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

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