fix pages and resources content and styling issues (#158)
* fix: TNL-8457 Show field error state on blank discussion provider fields * fix: TNL-8474 Remove Resources section * fix: TNL-8473 change pages and resources background color to light-200 * fix: TNL-8476 app card descriptions should be bottom aligned * fix: TNL-8472 remove Disabled badge from tiles * fix: TNL-8487 use stateful button for configuration modal
This commit is contained in:
@@ -41,11 +41,11 @@ export default function CourseAuthoringPage({ courseId, children }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-light-200">
|
||||
{inProgress ? <Loading /> : AppHeader()}
|
||||
{children}
|
||||
{!inProgress && <Footer />}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Badge } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function StatusBadge({ intl, status, label }) {
|
||||
return (
|
||||
<>
|
||||
{label && `${label} `}
|
||||
{status
|
||||
? <Badge variant="success">{intl.formatMessage(messages.enabled)}</Badge>
|
||||
: <Badge variant="secondary">{intl.formatMessage(messages.disabled)}</Badge>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
StatusBadge.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
status: PropTypes.bool.isRequired,
|
||||
label: PropTypes.string,
|
||||
};
|
||||
|
||||
StatusBadge.defaultProps = {
|
||||
label: null,
|
||||
};
|
||||
|
||||
export default injectIntl(StatusBadge);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
enabled: {
|
||||
id: 'course-authoring.badge.enabled',
|
||||
defaultMessage: 'Enabled',
|
||||
},
|
||||
disabled: {
|
||||
id: 'course-authoring.badge.disabled',
|
||||
defaultMessage: 'Disabled',
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -11,8 +11,6 @@ import messages from './messages';
|
||||
import DiscussionsSettings from './discussions';
|
||||
|
||||
import PageGrid from './pages/PageGrid';
|
||||
import ResourceList from './resources/ResourcesList';
|
||||
|
||||
import { fetchCourseApps } from './data/thunks';
|
||||
import { useModels } from '../generic/model-store';
|
||||
import { getLoadingStatus } from './data/selectors';
|
||||
@@ -54,7 +52,6 @@ function PagesAndResources({ courseId, intl }) {
|
||||
</div>
|
||||
|
||||
<PageGrid pages={pages} />
|
||||
<ResourceList />
|
||||
<Switch>
|
||||
<PageRoute
|
||||
path={[
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form, Hyperlink, ModalLayer, Spinner, TransitionReplace,
|
||||
StatefulButton, Badge,
|
||||
} from '@edx/paragon';
|
||||
import { Formik } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useContext } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import * as Yup from 'yup';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import FormSwitchGroup from '../../generic/FormSwitchGroup';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import StatusBadge from '../../generic/status-badge/StatusBadge';
|
||||
import { getLoadingStatus } from '../data/selectors';
|
||||
import { getLoadingStatus, getSavingStatus } from '../data/selectors';
|
||||
import { updateAppStatus } from '../data/thunks';
|
||||
import { updateSavingStatus } from '../data/slice';
|
||||
import AppConfigFormDivider from '../discussions/app-config-form/apps/shared/AppConfigFormDivider';
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
import messages from './messages';
|
||||
@@ -59,8 +60,18 @@ function AppSettingsModal({
|
||||
}) {
|
||||
const { courseId } = useContext(PagesAndResourcesContext);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const updateSettingsRequestStatus = useSelector(getSavingStatus);
|
||||
const appInfo = useModel('courseApps', appId);
|
||||
const dispatch = useDispatch();
|
||||
const submitButtonState = updateSettingsRequestStatus === RequestStatus.IN_PROGRESS ? 'pending' : 'default';
|
||||
|
||||
useEffect(() => {
|
||||
if (updateSettingsRequestStatus === RequestStatus.SUCCESSFUL) {
|
||||
dispatch(updateSavingStatus({ status: '' }));
|
||||
onClose();
|
||||
}
|
||||
}, [updateSettingsRequestStatus]);
|
||||
|
||||
const handleFormSubmit = (values) => {
|
||||
// If the app's enabled/disabled loadingStatus has changed, set that first.
|
||||
if (appInfo.enabled !== values.enabled) {
|
||||
@@ -71,6 +82,7 @@ function AppSettingsModal({
|
||||
onSettingsSave();
|
||||
}
|
||||
};
|
||||
|
||||
const learnMoreLink = (
|
||||
<Hyperlink
|
||||
className="text-primary-500"
|
||||
@@ -121,10 +133,16 @@ function AppSettingsModal({
|
||||
label={(
|
||||
<>
|
||||
{enableAppLabel}
|
||||
<StatusBadge status={formikProps.values.enabled} />
|
||||
{
|
||||
formikProps.values.enabled && (
|
||||
<Badge className="py-1" variant="success">
|
||||
{intl.formatMessage(messages.enabled)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
helpText={(<p>{enableAppHelp}<br /> <div className="pt-3">{learnMoreLink}</div> </p>)}
|
||||
helpText={(<p>{enableAppHelp}<br /> <span className="pt-3">{learnMoreLink}</span> </p>)}
|
||||
/>
|
||||
<AppSettingsForm formikProps={formikProps}>
|
||||
{children}
|
||||
@@ -135,9 +153,15 @@ function AppSettingsModal({
|
||||
<Button variant="link" onClick={onClose}>
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" data-autofocus>
|
||||
{intl.formatMessage(messages.apply)}
|
||||
</Button>
|
||||
<StatefulButton
|
||||
labels={{
|
||||
default: intl.formatMessage(messages.apply),
|
||||
pending: intl.formatMessage(messages.applying),
|
||||
complete: intl.formatMessage(messages.applied),
|
||||
}}
|
||||
state={submitButtonState}
|
||||
onClick={formikProps.handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
@@ -29,7 +29,6 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.pages-resources.app-settings-modal.badge.disabled',
|
||||
defaultMessage: 'Disabled',
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7,7 +7,7 @@ const slice = createSlice({
|
||||
initialState: {
|
||||
courseAppIds: [],
|
||||
loadingStatus: RequestStatus.IN_PROGRESS,
|
||||
savingStatus: RequestStatus.SUCCESSFUL,
|
||||
savingStatus: '',
|
||||
courseAppsApiStatus: {},
|
||||
},
|
||||
reducers: {
|
||||
|
||||
@@ -18,6 +18,12 @@ import messages from './messages';
|
||||
function LtiConfigForm({
|
||||
appConfig, app, onSubmit, intl, formRef, title,
|
||||
}) {
|
||||
const ltiAppConfig = {
|
||||
consumerKey: appConfig.consumerKey || '',
|
||||
consumerSecret: appConfig.consumerSecret || '',
|
||||
launchUrl: appConfig.launchUrl || '',
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { externalLinks } = app;
|
||||
const {
|
||||
@@ -28,7 +34,7 @@ function LtiConfigForm({
|
||||
touched,
|
||||
errors,
|
||||
} = useFormik({
|
||||
initialValues: appConfig,
|
||||
initialValues: ltiAppConfig,
|
||||
validationSchema: Yup.object().shape({
|
||||
consumerKey: Yup.string().required(intl.formatMessage(messages.consumerKeyRequired)),
|
||||
consumerSecret: Yup.string().required(intl.formatMessage(messages.consumerSecretRequired)),
|
||||
@@ -37,9 +43,9 @@ function LtiConfigForm({
|
||||
onSubmit,
|
||||
});
|
||||
|
||||
const isInvalidConsumerKey = touched.consumerKey && errors.consumerKey;
|
||||
const isInvalidConsumerSecret = touched.consumerSecret && errors.consumerSecret;
|
||||
const isInvalidLaunchUrl = touched.launchUrl && errors.launchUrl;
|
||||
const isInvalidConsumerKey = Boolean(touched.consumerKey && errors.consumerKey);
|
||||
const isInvalidConsumerSecret = Boolean(touched.consumerSecret && errors.consumerSecret);
|
||||
const isInvalidLaunchUrl = Boolean(touched.launchUrl && errors.launchUrl);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(updateValidationStatus({ hasError: Object.keys(errors).length > 0 }));
|
||||
@@ -107,9 +113,9 @@ LtiConfigForm.propTypes = {
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
appConfig: PropTypes.shape({
|
||||
consumerKey: PropTypes.string.isRequired,
|
||||
consumerSecret: PropTypes.string.isRequired,
|
||||
launchUrl: PropTypes.string.isRequired,
|
||||
consumerKey: PropTypes.string,
|
||||
consumerSecret: PropTypes.string,
|
||||
launchUrl: PropTypes.string,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
|
||||
@@ -5,38 +5,6 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.pages-resources.heading',
|
||||
defaultMessage: 'Pages & Resources',
|
||||
},
|
||||
'enable.button': {
|
||||
id: 'course-authoring.pages-resources.enable.button',
|
||||
defaultMessage: 'Enable',
|
||||
},
|
||||
'pages.subheading': {
|
||||
id: 'course-authoring.pages-resources.pages.subheading',
|
||||
defaultMessage: 'Course pages',
|
||||
},
|
||||
'pageStatus.enabled': {
|
||||
id: 'course-authoring.pages-resources.pageStatus.enabled',
|
||||
defaultMessage: 'Enabled',
|
||||
},
|
||||
'pageStatus.disabled': {
|
||||
id: 'course-authoring.pages-resources.pageStatus.disabled',
|
||||
defaultMessage: 'Disabled',
|
||||
},
|
||||
'resources.subheading': {
|
||||
id: 'course-authoring.pages-resources.resources.subheading',
|
||||
defaultMessage: 'Resources',
|
||||
},
|
||||
'resources.custom.title': {
|
||||
id: 'course-authoring.pages-resources.resources.custom.title',
|
||||
defaultMessage: 'Custom',
|
||||
},
|
||||
'resources.custom.description': {
|
||||
id: 'course-authoring.pages-resources.resources.custom.description',
|
||||
defaultMessage: 'Create and edit custom pages to provide students with additional course content and resources. Pages are publicly visible. If users know the URL of a page, they can view the page even if they are not registered for or logged in to your course.',
|
||||
},
|
||||
'resources.newPage.button': {
|
||||
id: 'course-authoring.pages-resources.resources.newPage.button',
|
||||
defaultMessage: 'New Page',
|
||||
},
|
||||
settings: {
|
||||
id: 'course-authoring.pages-resources.resources.settings.button',
|
||||
defaultMessage: 'settings',
|
||||
@@ -45,6 +13,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.pages-resources.viewLive.button',
|
||||
defaultMessage: 'View Live',
|
||||
},
|
||||
enabled: {
|
||||
id: 'course-authoring.badge.enabled',
|
||||
defaultMessage: 'Enabled',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Card, Icon, IconButton,
|
||||
Card, Icon, IconButton, Badge,
|
||||
} from '@edx/paragon';
|
||||
import { Settings } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useContext } from 'react';
|
||||
import StatusBadge from '../../generic/status-badge/StatusBadge';
|
||||
import messages from '../messages';
|
||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||
|
||||
@@ -29,10 +28,6 @@ function PageCard({
|
||||
}) {
|
||||
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
|
||||
|
||||
const handleClick = () => {
|
||||
history.push(`${pagesAndResourcesPath}/${page.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="shadow card"
|
||||
@@ -41,10 +36,11 @@ function PageCard({
|
||||
height: '14rem',
|
||||
}}
|
||||
>
|
||||
<Card.Body className="d-flex flex-column">
|
||||
<Card.Title className="d-flex mb-0 align-items-center justify-content-between">
|
||||
<h4 className="m-0 p-0">{page.name}</h4>
|
||||
{(page.allowedOperations.configure || page.allowedOperations.enable)
|
||||
<Card.Body className="d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<Card.Title className="d-flex mb-0 align-items-center justify-content-between">
|
||||
<h4 className="m-0 p-0">{page.name}</h4>
|
||||
{(page.allowedOperations.configure || page.allowedOperations.enable)
|
||||
&& (
|
||||
<IconButton
|
||||
className="mb-0 mr-1"
|
||||
@@ -55,19 +51,20 @@ function PageCard({
|
||||
onClick={() => history.push(`${pagesAndResourcesPath}/${page.id}/settings`)}
|
||||
/>
|
||||
)}
|
||||
</Card.Title>
|
||||
</Card.Title>
|
||||
{
|
||||
page.enabled && (
|
||||
<Badge className="py-1" variant="success">
|
||||
{intl.formatMessage(messages.enabled)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="mb-2"><StatusBadge status={page.enabled} /></div>
|
||||
|
||||
<Card.Text className="flex-grow-1 m-0">
|
||||
<Card.Text className="m-0">
|
||||
{page.description}
|
||||
</Card.Text>
|
||||
|
||||
{(page.allowedOperations.enable && !page.enabled) && (
|
||||
<Button variant="outline-primary" size="sm" onClick={handleClick} className="align-self-end">
|
||||
{intl.formatMessage(messages['enable.button'])}
|
||||
</Button>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function ResourceList({ intl, resources }) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-info-500">{intl.formatMessage(messages['resources.subheading'])}</h3>
|
||||
<div className="row bg-light-100 text-info-500 border shadow justify-content-center align-items-center my-3 mx-1">
|
||||
<div className="col-1 font-weight-bold">{intl.formatMessage(messages['resources.custom.title'])}</div>
|
||||
<div className="col-8 my-3">
|
||||
{intl.formatMessage(messages['resources.custom.description'])}
|
||||
</div>
|
||||
<div className="col-2 text-right">
|
||||
<Button variant="outline-primary">
|
||||
{intl.formatMessage(messages['resources.newPage.button'])}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{resources}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const resourcesShape = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
// TODO: What is the shape of a resources model?
|
||||
});
|
||||
|
||||
ResourceList.propTypes = {
|
||||
resources: PropTypes.arrayOf(resourcesShape),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
ResourceList.defaultProps = {
|
||||
resources: [],
|
||||
};
|
||||
|
||||
export default injectIntl(ResourceList);
|
||||
Reference in New Issue
Block a user