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:
Awais Ansari
2021-07-16 10:47:22 +05:00
committed by GitHub
parent 12ae7a6874
commit 8eee09a2d0
11 changed files with 69 additions and 160 deletions

View File

@@ -41,11 +41,11 @@ export default function CourseAuthoringPage({ courseId, children }) {
);
return (
<>
<div className="bg-light-200">
{inProgress ? <Loading /> : AppHeader()}
{children}
{!inProgress && <Footer />}
</>
</div>
);
}

View File

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

View File

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

View File

@@ -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={[

View File

@@ -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}&nbsp;
<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>
)}

View File

@@ -29,7 +29,6 @@ const messages = defineMessages({
id: 'course-authoring.pages-resources.app-settings-modal.badge.disabled',
defaultMessage: 'Disabled',
},
});
export default messages;

View File

@@ -7,7 +7,7 @@ const slice = createSlice({
initialState: {
courseAppIds: [],
loadingStatus: RequestStatus.IN_PROGRESS,
savingStatus: RequestStatus.SUCCESSFUL,
savingStatus: '',
courseAppsApiStatus: {},
},
reducers: {

View File

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

View File

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

View File

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

View File

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