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:
Awais Ansari
2021-08-02 14:24:38 +05:00
committed by GitHub
parent ca5e283159
commit aa3420f870
13 changed files with 97 additions and 60 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
table {
font-size: 14px;
color: black;
}

View File

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

View File

@@ -0,0 +1,9 @@
.desktop-card {
width: 19rem;
height: 14rem;
}
.mobile-card {
width: 100%;
height: 14rem;
}

View File

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