style: 1.4 content and style fixes (#170)
* style: add spacing between footer and body content * fix: TNL-8567 prevent card selection when click on show features * style: TNL-8569 update page & resources margin for mobile view * fix: TNL-8571 correct error state for blockout date field * style: TNL-8569 set page&resources margins in mobile view * style: TNL-8573 feature list should intersect with column * style: TNL-8568 update font size and color for feature table * fix: move formInvalid logic to legacy form and make it centralize
This commit is contained in:
@@ -40,11 +40,17 @@ export default function CourseAuthoringPage({ courseId, children }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const AppFooter = () => (
|
||||||
|
<div className="mt-6">
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-light-200">
|
<div className="bg-light-200">
|
||||||
{inProgress ? <Loading /> : AppHeader()}
|
{inProgress ? <Loading /> : <AppHeader />}
|
||||||
{children}
|
{children}
|
||||||
{!inProgress && <Footer />}
|
{!inProgress && <AppFooter />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ function PagesAndResources({ courseId, intl }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PagesAndResourcesProvider courseId={courseId}>
|
<PagesAndResourcesProvider courseId={courseId}>
|
||||||
<main className="container container-mw-md">
|
<main className="container container-mw-md px-3">
|
||||||
<div className="d-flex justify-content-between my-5 align-items-center">
|
<div className="d-flex justify-content-between my-4 my-md-5 align-items-center">
|
||||||
<h3 className="m-0">{intl.formatMessage(messages.heading)}</h3>
|
<h3 className="m-0">{intl.formatMessage(messages.heading)}</h3>
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
destination={lmsCourseURL}
|
destination={lmsCourseURL}
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ function AppSettingsModal({
|
|||||||
const appInfo = useModel('courseApps', appId);
|
const appInfo = useModel('courseApps', appId);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const submitButtonState = updateSettingsRequestStatus === RequestStatus.IN_PROGRESS ? 'pending' : 'default';
|
const submitButtonState = updateSettingsRequestStatus === RequestStatus.IN_PROGRESS ? 'pending' : 'default';
|
||||||
const isTabletOrMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const modalVariant = isTabletOrMobile ? 'dark' : 'default';
|
const modalVariant = isMobile ? 'dark' : 'default';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (updateSettingsRequestStatus === RequestStatus.SUCCESSFUL) {
|
if (updateSettingsRequestStatus === RequestStatus.SUCCESSFUL) {
|
||||||
@@ -102,7 +102,7 @@ function AppSettingsModal({
|
|||||||
isOpen
|
isOpen
|
||||||
closeText={intl.formatMessage(messages.cancel)}
|
closeText={intl.formatMessage(messages.cancel)}
|
||||||
dialogClassName="modal-dialog-centered modal-lg"
|
dialogClassName="modal-dialog-centered modal-lg"
|
||||||
hasCloseButton={isTabletOrMobile}
|
hasCloseButton={isMobile}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
variant={modalVariant}
|
variant={modalVariant}
|
||||||
isFullscreenOnMobile
|
isFullscreenOnMobile
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Card, Form } from '@edx/paragon';
|
import { Card, Form } from '@edx/paragon';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
@@ -10,6 +11,7 @@ import AnonymousPostingFields from '../shared/AnonymousPostingFields';
|
|||||||
import DiscussionTopics from '../shared/discussion-topics/DiscussionTopics';
|
import DiscussionTopics from '../shared/discussion-topics/DiscussionTopics';
|
||||||
import BlackoutDatesField, { blackoutDatesRegex } from '../shared/BlackoutDatesField';
|
import BlackoutDatesField, { blackoutDatesRegex } from '../shared/BlackoutDatesField';
|
||||||
import LegacyConfigFormProvider from './LegacyConfigFormProvider';
|
import LegacyConfigFormProvider from './LegacyConfigFormProvider';
|
||||||
|
import { updateValidationStatus } from '../../../data/slice';
|
||||||
|
|
||||||
import messages from '../shared/messages';
|
import messages from '../shared/messages';
|
||||||
import AppConfigFormDivider from '../shared/AppConfigFormDivider';
|
import AppConfigFormDivider from '../shared/AppConfigFormDivider';
|
||||||
@@ -37,6 +39,13 @@ Yup.addMethod(Yup.object, 'uniqueProperty', function (propertyName, message) {
|
|||||||
function LegacyConfigForm({
|
function LegacyConfigForm({
|
||||||
appConfig, onSubmit, formRef, intl, title,
|
appConfig, onSubmit, formRef, intl, title,
|
||||||
}) {
|
}) {
|
||||||
|
const [isFormInvalid, setIsFormInvalid] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(updateValidationStatus({ hasError: isFormInvalid }));
|
||||||
|
}, [isFormInvalid]);
|
||||||
|
|
||||||
const [validDiscussionTopics, setValidDiscussionTopics] = useState(appConfig.discussionTopics);
|
const [validDiscussionTopics, setValidDiscussionTopics] = useState(appConfig.discussionTopics);
|
||||||
const legacyFormValidationSchema = Yup.object().shape({
|
const legacyFormValidationSchema = Yup.object().shape({
|
||||||
blackoutDates: Yup.string().matches(
|
blackoutDates: Yup.string().matches(
|
||||||
@@ -79,6 +88,8 @@ function LegacyConfigForm({
|
|||||||
setValidDiscussionTopics,
|
setValidDiscussionTopics,
|
||||||
discussionTopicErrors,
|
discussionTopicErrors,
|
||||||
};
|
};
|
||||||
|
setIsFormInvalid(discussionTopicErrors.some((error) => error === true)
|
||||||
|
|| Boolean(touched.blackoutDates && errors.blackoutDates));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LegacyConfigFormProvider value={contextValue}>
|
<LegacyConfigFormProvider value={contextValue}>
|
||||||
@@ -96,12 +107,7 @@ function LegacyConfigForm({
|
|||||||
<AppConfigFormDivider thick />
|
<AppConfigFormDivider thick />
|
||||||
<DivisionByGroupFields />
|
<DivisionByGroupFields />
|
||||||
<AppConfigFormDivider thick />
|
<AppConfigFormDivider thick />
|
||||||
<BlackoutDatesField
|
<BlackoutDatesField />
|
||||||
errors={errors}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onChange={handleChange}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</LegacyConfigFormProvider>
|
</LegacyConfigFormProvider>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Form } from '@edx/paragon';
|
import { Form, TransitionReplace } from '@edx/paragon';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,49 +68,57 @@ import messages from './messages';
|
|||||||
*/
|
*/
|
||||||
export const blackoutDatesRegex = /^\[(\[("[0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9](T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1}"),("[0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9](T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1}")\](,){0,1})*\]$/;
|
export const blackoutDatesRegex = /^\[(\[("[0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9](T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1}"),("[0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9](T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1}")\](,){0,1})*\]$/;
|
||||||
|
|
||||||
function BlackoutDatesField({
|
const BlackoutDatesField = ({ intl }) => {
|
||||||
onBlur,
|
const [inFocus, setInFocus] = useState(false);
|
||||||
onChange,
|
const {
|
||||||
intl,
|
handleChange, handleBlur, errors,
|
||||||
values,
|
touched, values: appConfig,
|
||||||
errors,
|
} = useFormikContext();
|
||||||
}) {
|
|
||||||
|
const hasError = Boolean(touched.blackoutDates && errors.blackoutDates);
|
||||||
|
|
||||||
|
const handleFocusOut = (event) => {
|
||||||
|
handleBlur(event);
|
||||||
|
setInFocus(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h5 className="my-4 text-gray-500">{intl.formatMessage(messages.blackoutDates)}</h5>
|
<h5 className="my-4 text-gray-500">{intl.formatMessage(messages.blackoutDates)}</h5>
|
||||||
<Form.Group
|
<Form.Group
|
||||||
controlId="blackoutDates"
|
controlId="blackoutDates"
|
||||||
|
isInvalid={hasError && !inFocus}
|
||||||
|
className="m-2"
|
||||||
>
|
>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
value={values.blackoutDates}
|
value={appConfig.blackoutDates}
|
||||||
onChange={onChange}
|
onChange={handleChange}
|
||||||
onBlur={onBlur}
|
onBlur={(event) => handleFocusOut(event)}
|
||||||
className="mb-3"
|
className="mb-1"
|
||||||
floatingLabel={intl.formatMessage(messages.blackoutDatesLabel)}
|
floatingLabel={intl.formatMessage(messages.blackoutDatesLabel)}
|
||||||
|
onFocus={() => setInFocus(true)}
|
||||||
/>
|
/>
|
||||||
{errors.blackoutDates && (
|
<TransitionReplace key="blackoutDates">
|
||||||
<Form.Control.Feedback type="invalid">
|
{hasError && !inFocus ? (
|
||||||
{errors.blackoutDates}
|
<React.Fragment key="open">
|
||||||
</Form.Control.Feedback>
|
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||||
)}
|
<div className="small">{errors.blackoutDates}</div>
|
||||||
<Form.Text muted>
|
</Form.Control.Feedback>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<React.Fragment key="closed" />
|
||||||
|
)}
|
||||||
|
</TransitionReplace>
|
||||||
|
<Form.Text muted className="mt-3">
|
||||||
{intl.formatMessage(messages.blackoutDatesHelp)}
|
{intl.formatMessage(messages.blackoutDatesHelp)}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
BlackoutDatesField.propTypes = {
|
BlackoutDatesField.propTypes = {
|
||||||
onBlur: PropTypes.func.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
values: PropTypes.shape({
|
|
||||||
blackoutDates: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
errors: PropTypes.shape({
|
|
||||||
blackoutDates: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(BlackoutDatesField);
|
export default injectIntl(BlackoutDatesField);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useContext, useCallback } from 'react';
|
import React, { useContext, useCallback } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { Add } from '@edx/paragon/icons';
|
import { Add } from '@edx/paragon/icons';
|
||||||
import { Button } from '@edx/paragon';
|
import { Button } from '@edx/paragon';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
@@ -8,7 +7,6 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import TopicItem from './TopicItem';
|
import TopicItem from './TopicItem';
|
||||||
import { updateValidationStatus } from '../../../../data/slice';
|
|
||||||
import { LegacyConfigFormContext } from '../../legacy/LegacyConfigFormProvider';
|
import { LegacyConfigFormContext } from '../../legacy/LegacyConfigFormProvider';
|
||||||
import filterItemFromObject from '../../../utils';
|
import filterItemFromObject from '../../../utils';
|
||||||
|
|
||||||
@@ -19,18 +17,12 @@ const DiscussionTopics = ({ intl }) => {
|
|||||||
setFieldValue,
|
setFieldValue,
|
||||||
} = useFormikContext();
|
} = useFormikContext();
|
||||||
const { discussionTopics, divideDiscussionIds } = appConfig;
|
const { discussionTopics, divideDiscussionIds } = appConfig;
|
||||||
const dispatch = useDispatch();
|
|
||||||
const {
|
const {
|
||||||
discussionTopicErrors,
|
discussionTopicErrors,
|
||||||
validDiscussionTopics,
|
validDiscussionTopics,
|
||||||
setValidDiscussionTopics,
|
setValidDiscussionTopics,
|
||||||
} = useContext(LegacyConfigFormContext);
|
} = useContext(LegacyConfigFormContext);
|
||||||
|
|
||||||
const isFormInvalid = discussionTopicErrors.some((error) => error === true);
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(updateValidationStatus({ hasError: isFormInvalid }));
|
|
||||||
}, [isFormInvalid]);
|
|
||||||
|
|
||||||
const handleTopicDelete = async (topicIndex, topicId, remove) => {
|
const handleTopicDelete = async (topicIndex, topicId, remove) => {
|
||||||
await remove(topicIndex);
|
await remove(topicIndex);
|
||||||
validateForm();
|
validateForm();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function AppCard({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Card.Body className="m-2">
|
<Card.Body>
|
||||||
<div className="h4 card-title">
|
<div className="h4 card-title">
|
||||||
{intl.formatMessage(messages[`appName-${app.id}`])}
|
{intl.formatMessage(messages[`appName-${app.id}`])}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const NonSupportedFeature = (
|
|||||||
function FeaturesList({ app, features, intl }) {
|
function FeaturesList({ app, features, intl }) {
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
title={(
|
title={(
|
||||||
<>
|
<>
|
||||||
<Collapsible.Visible whenClosed>
|
<Collapsible.Visible whenClosed>
|
||||||
@@ -32,7 +33,7 @@ function FeaturesList({ app, features, intl }) {
|
|||||||
styling="basic"
|
styling="basic"
|
||||||
>
|
>
|
||||||
{features && features.map((feature) => (
|
{features && features.map((feature) => (
|
||||||
<div key={`collapsible-${app.id}&${feature.id}`}>
|
<div key={`collapsible-${app.id}&${feature.id}`} className="d-flex mb-1">
|
||||||
{app.featureIds.includes(feature.id)
|
{app.featureIds.includes(feature.id)
|
||||||
? SupportedFeature
|
? SupportedFeature
|
||||||
: NonSupportedFeature}
|
: NonSupportedFeature}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Remove, Check } from '@edx/paragon/icons';
|
|||||||
import { DataTable } from '@edx/paragon';
|
import { DataTable } from '@edx/paragon';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import './FeaturesTable.scss';
|
||||||
|
|
||||||
function FeaturesTable({ apps, features, intl }) {
|
function FeaturesTable({ apps, features, intl }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
table {
|
||||||
|
font-size: 14px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { history } from '@edx/frontend-platform';
|
import { history } from '@edx/frontend-platform';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Badge, Card, Icon, IconButton, Hyperlink,
|
Badge, Card, Icon, IconButton, Hyperlink,
|
||||||
@@ -8,6 +9,8 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
|
||||||
|
import { useIsDesktop } from '../../utils';
|
||||||
|
import './PageCard.scss';
|
||||||
|
|
||||||
const CoursePageShape = PropTypes.shape({
|
const CoursePageShape = PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
@@ -27,6 +30,7 @@ function PageCard({
|
|||||||
page,
|
page,
|
||||||
}) {
|
}) {
|
||||||
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
|
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
|
||||||
|
const isDesktop = useIsDesktop();
|
||||||
|
|
||||||
const SettingsButton = () => {
|
const SettingsButton = () => {
|
||||||
if (page.legacyLink) {
|
if (page.legacyLink) {
|
||||||
@@ -55,11 +59,13 @@ function PageCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="shadow card"
|
className={classNames(
|
||||||
style={{
|
'shadow card',
|
||||||
width: '19rem',
|
{
|
||||||
height: '14rem',
|
'desktop-card': isDesktop,
|
||||||
}}
|
'mobile-card': !isDesktop,
|
||||||
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Card.Body className="d-flex flex-column justify-content-between">
|
<Card.Body className="d-flex flex-column justify-content-between">
|
||||||
<Card.Title className="d-flex mb-0 align-items-center justify-content-between">
|
<Card.Title className="d-flex mb-0 align-items-center justify-content-between">
|
||||||
|
|||||||
9
src/pages-and-resources/pages/PageCard.scss
Normal file
9
src/pages-and-resources/pages/PageCard.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.desktop-card {
|
||||||
|
width: 19rem;
|
||||||
|
height: 14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 14rem;
|
||||||
|
}
|
||||||
@@ -6,5 +6,9 @@ export const executeThunk = async (thunk, dispatch, getState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useIsMobile() {
|
export function useIsMobile() {
|
||||||
return useMediaQuery({ query: '(max-width: 768px)' });
|
return useMediaQuery({ query: '(max-width: 767.98px)' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIsDesktop() {
|
||||||
|
return useMediaQuery({ query: '(min-width: 992px)' });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user