diff --git a/packages/core/admin/admin/src/content-manager/components/ComponentInitializer/index.js b/packages/core/admin/admin/src/content-manager/components/ComponentInitializer/index.js index ba4f5758f3..03d76c7b6c 100644 --- a/packages/core/admin/admin/src/content-manager/components/ComponentInitializer/index.js +++ b/packages/core/admin/admin/src/content-manager/components/ComponentInitializer/index.js @@ -23,45 +23,64 @@ const IconWrapper = styled.span` } `; -const ComponentInitializer = ({ isReadOnly, onClick }) => { +const ComponentInitializer = ({ error, isReadOnly, onClick }) => { const { formatMessage } = useIntl(); return ( - - - - - - - - - - {formatMessage({ - id: getTrad('components.empty-repeatable'), - defaultMessage: 'No entry yet. Click on the button below to add one.', - })} - - - - + <> + + + + + + + + + + {formatMessage({ + id: getTrad('components.empty-repeatable'), + defaultMessage: 'No entry yet. Click on the button below to add one.', + })} + + + + + {error?.id && ( + + {formatMessage( + { + id: error.id, + defaultMessage: error.defaultMessage, + }, + error.values + )} + + )} + ); }; 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, }; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/Component/index.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/Component/index.js index c8e2d1662e..071f7f305f 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/Component/index.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/Component/index.js @@ -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 ( - - onToggle(index)} size="S"> + + onToggle(index)} size="S" error={errorMessage}> } action={ @@ -113,14 +137,16 @@ const Component = ({ togglePosition="left" /> - onToggle(index)}> - - + + onToggle(index)}> + + + @@ -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, diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/index.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/index.js index f283c63ff0..b1becbe911 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/index.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/index.js @@ -188,6 +188,7 @@ const DynamicZone = ({ return ( { +const AccordionGroupCustom = ({ children, footer, label, labelAction, error }) => { + const { formatMessage } = useIntl(); + const childrenArray = Children.toArray(children).map(child => { + return cloneElement(child, { hasErrorMessage: false }); + }); + return ( {label && ( @@ -72,13 +79,21 @@ const AccordionGroupCustom = ({ children, footer, label, labelAction }) => { {labelAction && {labelAction}} )} - {children} + {childrenArray} {footer && {footer}} + {error && ( + + + {formatMessage({ id: error.id, defaultMessage: error.defaultMessage }, error.values)} + + + )} ); }; 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, diff --git a/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/DraggedItem/index.js b/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/DraggedItem/index.js index bffc572bb6..e596c4195c 100644 --- a/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/DraggedItem/index.js +++ b/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/DraggedItem/index.js @@ -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 ( - + {isDragging && } {!isDragging && isDraggingSibling && ( )} {!isDragging && !isDraggingSibling && ( - + )} - + ); }; 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, diff --git a/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/index.js b/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/index.js index fd42a9936f..6ed82525a7 100644 --- a/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/index.js +++ b/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/index.js @@ -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 ; + return ( + + ); + } + + 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 ( - + }> @@ -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 ( - // - // - // {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 ( - // { - // if (isOpen) { - // setCollapseToOpen(''); - // } else { - // setCollapseToOpen(key); - // } - // }} - // parentName={name} - // schema={componentLayoutData} - // toggleCollapses={toggleCollapses} - // /> - // ); - // })} - // - // - // {hasMinError && ( - // - // 1 ? '.plural' : '.singular' - // }` - // )} - // values={{ count: missingComponentsValue }} - // /> - // - // )} - // - // ); }; 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, }; diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index a031c480db..5eefcca086 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -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",