Merge pull request #11545 from strapi/accordion-error-state

CM/ Accordion Error state
This commit is contained in:
cyril lopez 2021-11-15 17:46:30 +01:00 committed by GitHub
commit baae2f0252
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 166 additions and 223 deletions

View File

@ -23,45 +23,64 @@ const IconWrapper = styled.span`
}
`;
const ComponentInitializer = ({ isReadOnly, onClick }) => {
const ComponentInitializer = ({ error, isReadOnly, onClick }) => {
const { formatMessage } = useIntl();
return (
<Box
as="button"
background="neutral100"
borderColor="neutral200"
disabled={isReadOnly}
hasRadius
onClick={onClick}
paddingTop={9}
paddingBottom={9}
type="button"
>
<Stack size={2}>
<Flex justifyContent="center" style={{ cursor: isReadOnly ? 'not-allowed' : 'inherit' }}>
<IconWrapper>
<PlusCircle />
</IconWrapper>
</Flex>
<Flex justifyContent="center">
<Text textColor="primary600" small bold>
{formatMessage({
id: getTrad('components.empty-repeatable'),
defaultMessage: 'No entry yet. Click on the button below to add one.',
})}
</Text>
</Flex>
</Stack>
</Box>
<>
<Box
as="button"
background="neutral100"
borderColor={error ? 'danger600' : 'neutral200'}
disabled={isReadOnly}
hasRadius
onClick={onClick}
paddingTop={9}
paddingBottom={9}
type="button"
>
<Stack size={2}>
<Flex justifyContent="center" style={{ cursor: isReadOnly ? 'not-allowed' : 'inherit' }}>
<IconWrapper>
<PlusCircle />
</IconWrapper>
</Flex>
<Flex justifyContent="center">
<Text textColor="primary600" small bold>
{formatMessage({
id: getTrad('components.empty-repeatable'),
defaultMessage: 'No entry yet. Click on the button below to add one.',
})}
</Text>
</Flex>
</Stack>
</Box>
{error?.id && (
<Text textColor="danger600" small>
{formatMessage(
{
id: error.id,
defaultMessage: error.defaultMessage,
},
error.values
)}
</Text>
)}
</>
);
};
ComponentInitializer.defaultProps = {
error: undefined,
isReadOnly: false,
};
ComponentInitializer.propTypes = {
error: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
values: PropTypes.object,
}),
isReadOnly: PropTypes.bool,
onClick: PropTypes.func.isRequired,
};

View File

@ -22,15 +22,18 @@ const IconButtonCustom = styled(IconButton)`
`;
const StyledBox = styled(Box)`
> div {
> div:not(:first-of-type) {
overflow: visible;
}
> div:first-child {
box-shadow: ${({ theme }) => theme.shadows.tableShadow};
}
`;
const AccordionContentRadius = styled(Box)`
border-radius: 0 0 ${({ theme }) => theme.spaces[1]} ${({ theme }) => theme.spaces[1]};
`;
const Component = ({
componentUid,
formErrors,
index,
isOpen,
isFieldAllowed,
@ -74,11 +77,32 @@ const Component = ({
{ name: friendlyName }
);
const formErrorsKeys = Object.keys(formErrors);
const fieldsErrors = formErrorsKeys.filter(errorKey => {
const errorKeysArray = errorKey.split('.');
if (`${errorKeysArray[0]}.${errorKeysArray[1]}` === `${name}.${index}`) {
return true;
}
return false;
});
let errorMessage;
if (fieldsErrors.length > 0) {
errorMessage = formatMessage({
id: getTrad('components.DynamicZone.error-message'),
defaultMessage: 'The component contains error(s)',
});
}
return (
<Box>
<Rectangle />
<StyledBox shadow="tableShadow" hasRadius>
<Accordion expanded={isOpen} toggle={() => onToggle(index)} size="S">
<StyledBox hasRadius>
<Accordion expanded={isOpen} toggle={() => onToggle(index)} size="S" error={errorMessage}>
<AccordionToggle
startIcon={<FontAwesomeIcon icon={icon} />}
action={
@ -113,14 +137,16 @@ const Component = ({
togglePosition="left"
/>
<AccordionContent>
<FocusTrap onEscape={() => onToggle(index)}>
<FieldComponent
componentUid={componentUid}
icon={icon}
name={`${name}.${index}`}
isFromDynamicZone
/>
</FocusTrap>
<AccordionContentRadius background="neutral0">
<FocusTrap onEscape={() => onToggle(index)}>
<FieldComponent
componentUid={componentUid}
icon={icon}
name={`${name}.${index}`}
isFromDynamicZone
/>
</FocusTrap>
</AccordionContentRadius>
</AccordionContent>
</Accordion>
</StyledBox>
@ -130,6 +156,7 @@ const Component = ({
Component.propTypes = {
componentUid: PropTypes.string.isRequired,
formErrors: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
isFieldAllowed: PropTypes.bool.isRequired,
isOpen: PropTypes.bool.isRequired,

View File

@ -188,6 +188,7 @@ const DynamicZone = ({
return (
<Component
componentUid={componentUid}
formErrors={formErrors}
key={index}
index={index}
isOpen={isOpen}

View File

@ -300,8 +300,8 @@ const EditViewDataManagerProvider = ({
onPut(formData, trackerProperty);
}
} catch (err) {
console.error('ValidationError');
console.error(err);
console.log('ValidationError');
console.log(err);
errors = getYupInnerErrors(err);

View File

@ -20,7 +20,7 @@ const NonRepeatableComponent = ({ componentUid, isFromDynamicZone, isNested, nam
return (
<Box
background={isFromDynamicZone ? 'neutral0' : 'neutral100'}
background={isFromDynamicZone ? '' : 'neutral100'}
paddingLeft={6}
paddingRight={6}
paddingTop={6}

View File

@ -1,8 +1,10 @@
import React from 'react';
import React, { Children, cloneElement } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import { Box } from '@strapi/design-system/Box';
import { Text } from '@strapi/design-system/Text';
import { Typography } from '@strapi/design-system/Typography';
import { Flex } from '@strapi/design-system/Flex';
import { KeyboardNavigable } from '@strapi/design-system/KeyboardNavigable';
@ -61,7 +63,12 @@ const LabelAction = styled(Box)`
}
`;
const AccordionGroupCustom = ({ children, footer, label, labelAction }) => {
const AccordionGroupCustom = ({ children, footer, label, labelAction, error }) => {
const { formatMessage } = useIntl();
const childrenArray = Children.toArray(children).map(child => {
return cloneElement(child, { hasErrorMessage: false });
});
return (
<KeyboardNavigable attributeName="data-strapi-accordion-toggle">
{label && (
@ -72,13 +79,21 @@ const AccordionGroupCustom = ({ children, footer, label, labelAction }) => {
{labelAction && <LabelAction paddingLeft={1}>{labelAction}</LabelAction>}
</Flex>
)}
<EnhancedGroup footer={footer}>{children}</EnhancedGroup>
<EnhancedGroup footer={footer}>{childrenArray}</EnhancedGroup>
{footer && <AccordionFooter>{footer}</AccordionFooter>}
{error && (
<Box paddingTop={1}>
<Typography variant="pi" textColor="danger600">
{formatMessage({ id: error.id, defaultMessage: error.defaultMessage }, error.values)}
</Typography>
</Box>
)}
</KeyboardNavigable>
);
};
AccordionGroupCustom.defaultProps = {
error: undefined,
footer: null,
label: null,
labelAction: undefined,
@ -86,6 +101,11 @@ AccordionGroupCustom.defaultProps = {
AccordionGroupCustom.propTypes = {
children: PropTypes.node.isRequired,
error: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
values: PropTypes.object,
}),
footer: PropTypes.node,
label: PropTypes.string,
labelAction: PropTypes.node,

View File

@ -5,7 +5,6 @@ import { useDrag, useDrop } from 'react-dnd';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { useIntl } from 'react-intl';
import toString from 'lodash/toString';
import styled from 'styled-components';
import { Accordion, AccordionToggle, AccordionContent } from '@strapi/design-system/Accordion';
import { Grid, GridItem } from '@strapi/design-system/Grid';
import { Stack } from '@strapi/design-system/Stack';
@ -21,14 +20,6 @@ import DraggingSibling from './DraggingSibling';
import { CustomIconButton, DragHandleWrapper } from './IconButtonCustoms';
import { connect, select } from './utils';
const StyledBox = styled(Box)`
> div {
> div:not(:first-of-type) {
overflow: visible;
}
}
`;
/* eslint-disable react/no-array-index-key */
// Issues:
@ -37,11 +28,9 @@ const StyledBox = styled(Box)`
const DraggedItem = ({
componentFieldName,
// FIXME: errors
// doesPreviousFieldContainErrorsAndIsOpen,
// hasErrors,
// hasMinError,
// isFirst,
// Errors are retrieved from the AccordionGroupCustom cloneElement
hasErrorMessage,
hasErrors,
isDraggingSibling,
isOpen,
isReadOnly,
@ -171,16 +160,24 @@ const DraggedItem = ({
};
const accordionTitle = toString(displayedValue);
const accordionHasError = hasErrors ? 'error' : undefined;
return (
<StyledBox ref={refs ? refs.dropRef : null}>
<Box ref={refs ? refs.dropRef : null}>
{isDragging && <Preview />}
{!isDragging && isDraggingSibling && (
<DraggingSibling displayedValue={accordionTitle} componentFieldName={componentFieldName} />
)}
{!isDragging && !isDraggingSibling && (
<Accordion expanded={isOpen} toggle={onClickToggle} id={componentFieldName} size="S">
<Accordion
error={accordionHasError}
hasErrorMessage={hasErrorMessage}
expanded={isOpen}
toggle={onClickToggle}
id={componentFieldName}
size="S"
>
<AccordionToggle
action={
isReadOnly ? null : (
@ -264,15 +261,11 @@ const DraggedItem = ({
</AccordionContent>
</Accordion>
)}
</StyledBox>
</Box>
);
};
DraggedItem.defaultProps = {
// doesPreviousFieldContainErrorsAndIsOpen: false,
// hasErrors: false,
// hasMinError: false,
// isFirst: false,
isDraggingSibling: false,
isOpen: false,
setIsDraggingSiblig: () => {},
@ -281,10 +274,8 @@ DraggedItem.defaultProps = {
DraggedItem.propTypes = {
componentFieldName: PropTypes.string.isRequired,
// doesPreviousFieldContainErrorsAndIsOpen: PropTypes.bool,
// hasErrors: PropTypes.bool,
// hasMinError: PropTypes.bool,
// isFirst: PropTypes.bool,
hasErrorMessage: PropTypes.bool.isRequired,
hasErrors: PropTypes.bool.isRequired,
isDraggingSibling: PropTypes.bool,
isOpen: PropTypes.bool,
isReadOnly: PropTypes.bool.isRequired,

View File

@ -43,7 +43,7 @@ const RepeatableComponent = ({
isNested,
isReadOnly,
max,
// min,
min,
name,
}) => {
const toggleNotification = useNotification();
@ -75,11 +75,10 @@ const RepeatableComponent = ({
const toggleCollapses = () => {
setCollapseToOpen('');
};
// TODO
// const missingComponentsValue = min - componentValueLength;
const errorsArray = componentErrorKeys.map(key => get(formErrors, [key, 'id'], ''));
const hasMinError = get(errorsArray, [0], '').includes('min');
const missingComponentsValue = min - componentValueLength;
const hasMinError = get(formErrors, name, { id: '' }).id.includes('min');
const handleClick = useCallback(() => {
if (!isReadOnly) {
@ -108,13 +107,38 @@ const RepeatableComponent = ({
toggleNotification,
]);
let errorMessage = formErrors[name];
if (hasMinError) {
errorMessage = {
id: getTrad('components.DynamicZone.missing-components'),
defaultMessage:
'There {number, plural, =0 {are # missing components} one {is # missing component} other {are # missing components}}',
values: { number: missingComponentsValue },
};
}
if (componentValueLength === 0) {
return <ComponentInitializer isReadOnly={isReadOnly} onClick={handleClick} />;
return (
<ComponentInitializer error={errorMessage} isReadOnly={isReadOnly} onClick={handleClick} />
);
}
const doesRepComponentHasChildError = componentErrorKeys.some(
error => error.split('.').length > 1
);
if (doesRepComponentHasChildError && !hasMinError) {
errorMessage = {
id: getTrad('components.RepeatableComponent.error-message'),
defaultMessage: 'The component(s) contain error(s)',
};
}
return (
<Box hasRadius background="neutral0" shadow="tableShadow" ref={drop}>
<Box hasRadius ref={drop}>
<AccordionGroupCustom
error={errorMessage}
footer={
<Flex justifyContent="center" height="48px" background="neutral0" hasRadius>
<TextButtonCustom disabled={isReadOnly} onClick={handleClick} startIcon={<Plus />}>
@ -130,24 +154,15 @@ const RepeatableComponent = ({
const key = data.__temp_key__;
const isOpen = collapseToOpen === key;
const componentFieldName = `${name}.${index}`;
const previousComponentTempKey = get(componentValue, [index - 1, '__temp_key__']);
const doesPreviousFieldContainErrorsAndIsOpen =
componentErrorKeys.includes(`${name}.${index - 1}`) &&
index !== 0 &&
collapseToOpen === previousComponentTempKey;
const hasErrors = componentErrorKeys.includes(componentFieldName);
return (
<DraggedItem
componentFieldName={componentFieldName}
componentUid={componentUid}
// TODO
doesPreviousFieldContainErrorsAndIsOpen={doesPreviousFieldContainErrorsAndIsOpen}
hasErrors={hasErrors}
hasMinError={hasMinError}
isDraggingSibling={isDraggingSibling}
isFirst={index === 0}
isOpen={isOpen}
isReadOnly={isReadOnly}
key={key}
@ -167,139 +182,7 @@ const RepeatableComponent = ({
})}
</AccordionGroupCustom>
</Box>
// <Box hasRadius borderColor="neutral200">
// <Box ref={drop}>
// {componentValue.map((data, index) => {
// const key = data.__temp_key__;
// const isOpen = collapseToOpen === key;
// const componentFieldName = `${name}.${index}`;
// const previousComponentTempKey = get(componentValue, [index - 1, '__temp_key__']);
// const doesPreviousFieldContainErrorsAndIsOpen =
// componentErrorKeys.includes(`${name}.${index - 1}`) &&
// index !== 0 &&
// collapseToOpen === previousComponentTempKey;
// const hasErrors = componentErrorKeys.includes(componentFieldName);
// return (
// <DraggedItem
// componentFieldName={componentFieldName}
// componentUid={componentUid}
// // TODO
// doesPreviousFieldContainErrorsAndIsOpen={doesPreviousFieldContainErrorsAndIsOpen}
// hasErrors={hasErrors}
// hasMinError={hasMinError}
// isFirst={index === 0}
// isOdd={index % 2 === 1}
// isOpen={isOpen}
// isReadOnly={isReadOnly}
// key={key}
// onClickToggle={() => {
// if (isOpen) {
// setCollapseToOpen('');
// } else {
// setCollapseToOpen(key);
// }
// }}
// parentName={name}
// schema={componentLayoutData}
// toggleCollapses={toggleCollapses}
// />
// );
// })}
// </Box>
// <Button
// // TODO
// // hasMinError={hasMinError}
// disabled={isReadOnly}
// // TODO
// // doesPreviousFieldContainErrorsAndIsClosed={
// // componentValueLength > 0 &&
// // componentErrorKeys.includes(`${name}.${componentValueLength - 1}`) &&
// // componentValue[componentValueLength - 1].__temp_key__ !== collapseToOpen
// // }
// onClick={handleClick}
// />
// </Box>
);
// return (
// <div>
// {componentValueLength === 0 && (
// <EmptyComponent hasMinError={hasMinError}>
// <FormattedMessage id={getTrad('components.empty-repeatable')}>
// {msg => <p>{msg}</p>}
// </FormattedMessage>
// </EmptyComponent>
// )}
// <div ref={drop}>
// {componentValueLength > 0 &&
// componentValue.map((data, index) => {
// const key = data.__temp_key__;
// const isOpen = collapseToOpen === key;
// const componentFieldName = `${name}.${index}`;
// const previousComponentTempKey = get(componentValue, [index - 1, '__temp_key__']);
// const doesPreviousFieldContainErrorsAndIsOpen =
// componentErrorKeys.includes(`${name}.${index - 1}`) &&
// index !== 0 &&
// collapseToOpen === previousComponentTempKey;
// const hasErrors = componentErrorKeys.includes(componentFieldName);
// return (
// <DraggedItem
// componentFieldName={componentFieldName}
// componentUid={componentUid}
// doesPreviousFieldContainErrorsAndIsOpen={doesPreviousFieldContainErrorsAndIsOpen}
// hasErrors={hasErrors}
// hasMinError={hasMinError}
// isFirst={index === 0}
// isReadOnly={isReadOnly}
// isOpen={isOpen}
// key={key}
// onClickToggle={() => {
// if (isOpen) {
// setCollapseToOpen('');
// } else {
// setCollapseToOpen(key);
// }
// }}
// parentName={name}
// schema={componentLayoutData}
// toggleCollapses={toggleCollapses}
// />
// );
// })}
// </div>
// <Button
// hasMinError={hasMinError}
// disabled={isReadOnly}
// withBorderRadius={false}
// doesPreviousFieldContainErrorsAndIsClosed={
// componentValueLength > 0 &&
// componentErrorKeys.includes(`${name}.${componentValueLength - 1}`) &&
// componentValue[componentValueLength - 1].__temp_key__ !== collapseToOpen
// }
// type="button"
// onClick={handleClick}
// >
// <i className="fa fa-plus" />
// <FormattedMessage id={getTrad('containers.EditView.add.new')} />
// </Button>
// {hasMinError && (
// <ErrorMessage>
// <FormattedMessage
// id={getTrad(
// `components.DynamicZone.missing${
// missingComponentsValue > 1 ? '.plural' : '.singular'
// }`
// )}
// values={{ count: missingComponentsValue }}
// />
// </ErrorMessage>
// )}
// </div>
// );
};
RepeatableComponent.defaultProps = {
@ -308,7 +191,7 @@ RepeatableComponent.defaultProps = {
formErrors: {},
isNested: false,
max: Infinity,
// min: -Infinity,
min: 0,
};
RepeatableComponent.propTypes = {
@ -320,7 +203,7 @@ RepeatableComponent.propTypes = {
isNested: PropTypes.bool,
isReadOnly: PropTypes.bool.isRequired,
max: PropTypes.number,
// min: PropTypes.number,
min: PropTypes.number,
name: PropTypes.string.isRequired,
};

View File

@ -461,6 +461,7 @@
"content-manager.components.DynamicZone.ComponentPicker-label": "Pick one component",
"content-manager.components.DynamicZone.add-component": "Add a component to {componentName}",
"content-manager.components.DynamicZone.delete-label": "Delete {name}",
"content-manager.components.DynamicZone.error-message": "The component contains error(s)",
"content-manager.components.DynamicZone.missing-components": "There {number, plural, =0 {are # missing components} one {is # missing component} other {are # missing components}}",
"content-manager.components.DynamicZone.move-down-label": "Move component down",
"content-manager.components.DynamicZone.move-up-label": "Move component up",
@ -481,6 +482,7 @@
"content-manager.components.LeftMenu.single-types": "{number, plural, =0 {Single Types} one {Single Type } other {Single Types}}",
"content-manager.components.LimitSelect.itemsPerPage": "Items per page",
"content-manager.components.NotAllowedInput.text": "No permissions to see this field",
"content-manager.components.RepeatableComponent.error-message": "The component(s) contain error(s)",
"content-manager.components.Search.placeholder": "Search for an entry...",
"content-manager.components.Select.draft-info-title": "State: Draft",
"content-manager.components.Select.publish-info-title": "State: Published",