diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/AddComponentButton.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/AddComponentButton.js index b0caf3f7e9..9f0860eaba 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/AddComponentButton.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/AddComponentButton.js @@ -6,14 +6,41 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useIntl } from 'react-intl'; import styled from 'styled-components'; import { PlusCircle } from '@strapi/icons'; -import { BaseButton, Box, Flex, Typography } from '@strapi/design-system'; +import { BaseButton, Flex, Typography } from '@strapi/design-system'; -import { getTrad } from '../../../utils'; +export const AddComponentButton = ({ hasError, isDisabled, isOpen, children, onClick }) => { + return ( + + + + + {children} + + + + ); +}; const StyledAddIcon = styled(PlusCircle)` + height: ${({ theme }) => theme.spaces[6]}; + width: ${({ theme }) => theme.spaces[6]}; transform: ${({ $isOpen }) => ($isOpen ? 'rotate(45deg)' : 'rotate(0deg)')}; > circle { fill: ${({ theme, $hasError }) => @@ -28,25 +55,11 @@ const StyledAddIcon = styled(PlusCircle)` const StyledButton = styled(BaseButton)` border-radius: 26px; border-color: ${({ theme }) => theme.colors.neutral150}; - background: ${({ theme }) => theme.colors.neutral0}; - padding-top: ${({ theme }) => theme.spaces[3]}; - padding-right: ${({ theme }) => theme.spaces[4]}; - padding-bottom: ${({ theme }) => theme.spaces[3]}; - padding-left: ${({ theme }) => theme.spaces[4]}; - box-shadow: ${({ theme }) => theme.shadows.filterShadow}; - svg { - height: ${({ theme }) => theme.spaces[6]}; - width: ${({ theme }) => theme.spaces[6]}; - > path { - fill: ${({ theme }) => theme.colors.neutral600}; - } - } &:hover { - color: ${({ theme }) => theme.colors.primary600} !important; ${Typography} { - color: ${({ theme }) => theme.colors.primary600} !important; + color: ${({ theme }) => theme.colors.primary600}; } ${StyledAddIcon} { @@ -73,92 +86,16 @@ const StyledButton = styled(BaseButton)` } `; -const BoxFullHeight = styled(Box)` - height: 100%; -`; - -const AddComponentButton = ({ - hasError, - hasMaxError, - hasMinError, - isDisabled, - isOpen, - label, - missingComponentNumber, - name, - onClick, -}) => { - const { formatMessage } = useIntl(); - const addLabel = formatMessage( - { - id: getTrad('components.DynamicZone.add-component'), - defaultMessage: 'Add a component to {componentName}', - }, - { componentName: label || name } - ); - const closeLabel = formatMessage({ id: 'app.utils.close-label', defaultMessage: 'Close' }); - let buttonLabel = isOpen ? closeLabel : addLabel; - - if (hasMaxError && !isOpen) { - buttonLabel = formatMessage({ - id: 'components.Input.error.validation.max', - defaultMessage: 'The value is too high.', - }); - } - - if (hasMinError && !isOpen) { - buttonLabel = formatMessage( - { - id: getTrad(`components.DynamicZone.missing-components`), - defaultMessage: - 'There {number, plural, =0 {are # missing components} one {is # missing component} other {are # missing components}}', - }, - { number: missingComponentNumber } - ); - } - - return ( - - - - - - - - - {buttonLabel} - - - - - - ); -}; - AddComponentButton.defaultProps = { hasError: false, - hasMaxError: false, - hasMinError: false, isDisabled: false, isOpen: false, - label: '', - missingComponentNumber: 0, }; AddComponentButton.propTypes = { - label: PropTypes.string, + children: PropTypes.node.isRequired, hasError: PropTypes.bool, - hasMaxError: PropTypes.bool, - hasMinError: PropTypes.bool, isDisabled: PropTypes.bool, isOpen: PropTypes.bool, - missingComponentNumber: PropTypes.number, - name: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, }; - -export default AddComponentButton; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCard.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCard.js deleted file mode 100644 index 9ee654ad98..0000000000 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCard.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * - * ComponentCard - * - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; - -import { Box, Typography, Flex } from '@strapi/design-system'; -import { pxToRem } from '@strapi/helper-plugin'; - -import { ComponentIcon } from '../../ComponentIcon'; - -const ComponentBox = styled(Box)` - flex-shrink: 0; - height: ${pxToRem(84)}; - border: 1px solid ${({ theme }) => theme.colors.neutral200}; - background: ${({ theme }) => theme.colors.neutral100}; - border-radius: ${({ theme }) => theme.borderRadius}; - display: flex; - justify-content: center; - align-items: center; - - &:focus, - &:hover { - border: 1px solid ${({ theme }) => theme.colors.primary200}; - background: ${({ theme }) => theme.colors.primary100}; - - ${Typography} { - color: ${({ theme }) => theme.colors.primary600}; - } - - /* > Flex > ComponentIcon */ - > div > div:first-child { - background: ${({ theme }) => theme.colors.primary200}; - color: ${({ theme }) => theme.colors.primary600}; - } - } -`; - -export default function ComponentCard({ children, onClick }) { - return ( - - - - - - {children} - - - - ); -} - -ComponentCard.defaultProps = { - onClick() {}, -}; - -ComponentCard.propTypes = { - children: PropTypes.node.isRequired, - onClick: PropTypes.func, -}; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCategory.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCategory.js index 9d2634ca1e..811cee33c3 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCategory.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentCategory.js @@ -1,18 +1,27 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Accordion, AccordionToggle, AccordionContent, Box } from '@strapi/design-system'; +import { + Accordion, + AccordionToggle, + AccordionContent, + Box, + Flex, + Typography, +} from '@strapi/design-system'; +import { pxToRem } from '@strapi/helper-plugin'; import styled from 'styled-components'; import { useIntl } from 'react-intl'; -import ComponentCard from './ComponentCard'; +import { ComponentIcon } from '../../ComponentIcon'; -const Grid = styled.div` - display: grid; - grid-template-columns: repeat(auto-fit, ${140 / 16}rem); - grid-gap: ${({ theme }) => theme.spaces[1]}; -`; - -const ComponentCategory = ({ category, components, variant, isOpen, onAddComponent, onToggle }) => { +export const ComponentCategory = ({ + category, + components, + variant, + isOpen, + onAddComponent, + onToggle, +}) => { const { formatMessage } = useIntl(); const handleToggle = () => { @@ -30,9 +39,26 @@ const ComponentCategory = ({ category, components, variant, isOpen, onAddCompone {components.map(({ componentUid, info: { displayName } }) => ( - - {formatMessage({ id: displayName, defaultMessage: displayName })} - + + + + + + {formatMessage({ id: displayName, defaultMessage: displayName })} + + + ))} @@ -41,6 +67,30 @@ const ComponentCategory = ({ category, components, variant, isOpen, onAddCompone ); }; +const Grid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, ${140 / 16}rem); + grid-gap: ${({ theme }) => theme.spaces[1]}; +`; + +const ComponentBox = styled(Flex)` + &:focus, + &:hover { + border: 1px solid ${({ theme }) => theme.colors.primary200}; + background: ${({ theme }) => theme.colors.primary100}; + + ${Typography} { + color: ${({ theme }) => theme.colors.primary600}; + } + + /* > Flex > ComponentIcon */ + > div > div:first-child { + background: ${({ theme }) => theme.colors.primary200}; + color: ${({ theme }) => theme.colors.primary600}; + } + } +`; + ComponentCategory.defaultProps = { components: [], isOpen: false, @@ -55,5 +105,3 @@ ComponentCategory.propTypes = { onToggle: PropTypes.func.isRequired, variant: PropTypes.oneOf(['primary', 'secondary']), }; - -export default ComponentCategory; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentPicker.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentPicker.js index 8504dea90d..1eff918632 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentPicker.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/ComponentPicker.js @@ -1,40 +1,24 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import groupBy from 'lodash/groupBy'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; import { KeyboardNavigable, Box, Flex, Typography } from '@strapi/design-system'; import { getTrad } from '../../../utils'; -import { useContentTypeLayout } from '../../../hooks'; -import ComponentCategory from './ComponentCategory'; +import { ComponentCategory } from './ComponentCategory'; -const ComponentPicker = ({ components, isOpen, onClickAddComponent }) => { +export const ComponentPicker = ({ dynamicComponentsByCategory, isOpen, onClickAddComponent }) => { const { formatMessage } = useIntl(); - const { getComponentLayout } = useContentTypeLayout(); + const [categoryToOpen, setCategoryToOpen] = useState(''); - const dynamicComponentCategories = useMemo(() => { - const componentsWithInfo = components.map((componentUid) => { - const { category, info } = getComponentLayout(componentUid); - - return { componentUid, category, info }; - }); - - const categories = groupBy(componentsWithInfo, 'category'); - - return Object.keys(categories).reduce((acc, current) => { - acc.push({ category: current, components: categories[current] }); - - return acc; - }, []); - }, [components, getComponentLayout]); - useEffect(() => { - if (isOpen && dynamicComponentCategories.length > 0) { - setCategoryToOpen(dynamicComponentCategories[0].category); + const categoryKeys = Object.keys(dynamicComponentsByCategory); + + if (isOpen && categoryKeys.length > 0) { + setCategoryToOpen(categoryKeys[0]); } - }, [isOpen, dynamicComponentCategories]); + }, [isOpen, dynamicComponentsByCategory]); const handleAddComponentToDz = (componentUid) => () => { onClickAddComponent(componentUid); @@ -53,54 +37,57 @@ const ComponentPicker = ({ components, isOpen, onClickAddComponent }) => { } return ( - - - - - {formatMessage({ - id: getTrad('components.DynamicZone.ComponentPicker-label'), - defaultMessage: 'Pick one component', - })} - - - - - {dynamicComponentCategories.map(({ category, components }, index) => ( - - ))} - - + + + + {formatMessage({ + id: getTrad('components.DynamicZone.ComponentPicker-label'), + defaultMessage: 'Pick one component', + })} + + + + + {Object.entries(dynamicComponentsByCategory).map(([category, components], index) => ( + + ))} + ); }; ComponentPicker.defaultProps = { - components: [], + dynamicComponentsByCategory: {}, isOpen: false, }; ComponentPicker.propTypes = { - components: PropTypes.array, + dynamicComponentsByCategory: PropTypes.shape({ + components: PropTypes.arrayOf( + PropTypes.shape({ + componentUid: PropTypes.string.isRequired, + info: PropTypes.object, + }) + ), + }), isOpen: PropTypes.bool, onClickAddComponent: PropTypes.func.isRequired, }; - -export default ComponentPicker; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js index 21e81e9447..3f921356e9 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicComponent.js @@ -12,67 +12,18 @@ import { IconButton, Box, Flex, + VisuallyHidden, } from '@strapi/design-system'; +import { Menu, MenuItem } from '@strapi/design-system/v2'; import { useCMEditViewDataManager } from '@strapi/helper-plugin'; -import { Trash, Drag } from '@strapi/icons'; +import { Trash, Drag, More } from '@strapi/icons'; import { useContentTypeLayout, useDragAndDrop } from '../../../hooks'; import { composeRefs, getTrad, ItemTypes } from '../../../utils'; import FieldComponent from '../../FieldComponent'; -const ActionsFlex = styled(Flex)` - /* - we need to remove the background from the button but we can't - wrap the element in styled because it breaks the forwardedAs which - we need for drag handler to work on firefox - */ - div[role='button'] { - background: transparent; - } -`; - -const IconButtonCustom = styled(IconButton)` - background-color: transparent; - - svg path { - fill: ${({ theme, expanded }) => - expanded ? theme.colors.primary600 : theme.colors.neutral600}; - } -`; - -// TODO: Delete once https://github.com/strapi/design-system/pull/858 -// is merged and released. -const StyledBox = styled(Box)` - > 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 Rectangle = styled(Box)` - width: ${({ theme }) => theme.spaces[2]}; - height: ${({ theme }) => theme.spaces[4]}; -`; - -const Preview = styled.span` - display: block; - background-color: ${({ theme }) => theme.colors.primary100}; - outline: 1px dashed ${({ theme }) => theme.colors.primary500}; - outline-offset: -1px; - padding: ${({ theme }) => theme.spaces[6]}; -`; - -const ComponentContainer = styled(Box)` - list-style: none; - padding: 0; - margin: 0; -`; - -const DynamicZoneComponent = ({ +export const DynamicComponent = ({ componentUid, formErrors, index, @@ -83,6 +34,8 @@ const DynamicZoneComponent = ({ onGrabItem, onDropItem, onCancel, + dynamicComponentsByCategory, + onAddComponent, }) => { const [isOpen, setIsOpen] = useState(true); const { formatMessage } = useIntl(); @@ -180,11 +133,70 @@ const DynamicZoneComponent = ({ > + + + + + {formatMessage({ + id: getTrad('components.DynamicZone.more-actions'), + defaultMessage: 'More actions', + })} + + + + + + {formatMessage({ + id: getTrad('components.DynamicZone.add-item-above'), + defaultMessage: 'Add component above', + })} + + + {Object.entries(dynamicComponentsByCategory).map(([category, components]) => ( + + {category} + {components.map(({ componentUid, info: { displayName } }) => ( + onAddComponent(componentUid, index)} + > + {displayName} + + ))} + + ))} + + + + + {formatMessage({ + id: getTrad('components.DynamicZone.add-item-below'), + defaultMessage: 'Add component below', + })} + + + {Object.entries(dynamicComponentsByCategory).map(([category, components]) => ( + + {category} + {components.map(({ componentUid, info: { displayName } }) => ( + onAddComponent(componentUid, index + 1)} + > + {displayName} + + ))} + + ))} + + + + ); return ( - + @@ -215,26 +227,86 @@ const DynamicZoneComponent = ({ ); }; -DynamicZoneComponent.defaultProps = { +const ActionsFlex = styled(Flex)` + /* + we need to remove the background from the button but we can't + wrap the element in styled because it breaks the forwardedAs which + we need for drag handler to work on firefox + */ + div[role='button'] { + background: transparent; + } +`; + +const IconButtonCustom = styled(IconButton)` + background-color: transparent; + + svg path { + fill: ${({ theme, expanded }) => + expanded ? theme.colors.primary600 : theme.colors.neutral600}; + } +`; + +// TODO: Delete once https://github.com/strapi/design-system/pull/858 +// is merged and released. +const StyledBox = styled(Box)` + > 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 Rectangle = styled(Box)` + width: ${({ theme }) => theme.spaces[2]}; + height: ${({ theme }) => theme.spaces[4]}; +`; + +const Preview = styled.span` + display: block; + background-color: ${({ theme }) => theme.colors.primary100}; + outline: 1px dashed ${({ theme }) => theme.colors.primary500}; + outline-offset: -1px; + padding: ${({ theme }) => theme.spaces[6]}; +`; + +const ComponentContainer = styled(Box)` + list-style: none; + padding: 0; + margin: 0; +`; + +DynamicComponent.defaultProps = { + dynamicComponentsByCategory: {}, formErrors: {}, index: 0, isFieldAllowed: true, + onAddComponent: undefined, onGrabItem: undefined, onDropItem: undefined, onCancel: undefined, }; -DynamicZoneComponent.propTypes = { +DynamicComponent.propTypes = { componentUid: PropTypes.string.isRequired, + dynamicComponentsByCategory: PropTypes.shape({ + components: PropTypes.arrayOf( + PropTypes.shape({ + componentUid: PropTypes.string.isRequired, + info: PropTypes.object, + }) + ), + }), formErrors: PropTypes.object, index: PropTypes.number, isFieldAllowed: PropTypes.bool, name: PropTypes.string.isRequired, + onAddComponent: PropTypes.func, onGrabItem: PropTypes.func, onDropItem: PropTypes.func, onCancel: PropTypes.func, onMoveComponent: PropTypes.func.isRequired, onRemoveComponentClick: PropTypes.func.isRequired, }; - -export default DynamicZoneComponent; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicZoneLabel.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicZoneLabel.js index 9b8ffcee20..4df303ef28 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicZoneLabel.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/DynamicZoneLabel.js @@ -7,15 +7,10 @@ import React from 'react'; import { useIntl } from 'react-intl'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; import { pxToRem } from '@strapi/helper-plugin'; import { Box, Flex, Typography } from '@strapi/design-system'; -const StyledBox = styled(Box)` - border-radius: ${pxToRem(26)}; -`; - -const DynamicZoneLabel = ({ +export const DynamicZoneLabel = ({ label, labelAction, name, @@ -28,36 +23,35 @@ const DynamicZoneLabel = ({ return ( - - - - - - {intlLabel}  - - - ({numberOfComponents}) - - {required && *} - {labelAction && {labelAction}} - - {intlDescription && ( - - - {formatMessage(intlDescription)} - - - )} + + + + + {intlLabel}  + + + ({numberOfComponents}) + + {required && *} + {labelAction && {labelAction}} - + {intlDescription && ( + + + {formatMessage(intlDescription)} + + + )} + ); @@ -82,5 +76,3 @@ DynamicZoneLabel.propTypes = { numberOfComponents: PropTypes.number, required: PropTypes.bool, }; - -export default DynamicZoneLabel; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/AddComponentButton.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/AddComponentButton.test.js index acb2d154ad..87d6b50b98 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/AddComponentButton.test.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/AddComponentButton.test.js @@ -1,66 +1,54 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render as renderRTL } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { IntlProvider } from 'react-intl'; -import AddComponentButton from '../AddComponentButton'; +import { AddComponentButton } from '../AddComponentButton'; describe('', () => { - const setup = (props) => - render( - - - - - - ); + const render = (props) => ({ + ...renderRTL( + + test + , + { + wrapper: ({ children }) => ( + + + {children} + + + ), + } + ), + user: userEvent.setup(), + }); it('should render the label by default', () => { - setup(); + const { getByRole } = render(); - expect(screen.getByText(/test/)).toBeInTheDocument(); + expect(getByRole('button', { name: 'test' })).toBeInTheDocument(); }); - it('should render the close label if the isOpen prop is true', () => { - setup({ isOpen: true }); - - expect(screen.getByText(/Close/)).toBeInTheDocument(); - }); - - it('should render the name of the field when the label is an empty string', () => { - setup({ label: '' }); - - expect(screen.getByText(/name/)).toBeInTheDocument(); - }); - - it('should render a too high error if there is hasMaxError is true and the component is not open', () => { - setup({ hasMaxError: true }); - - expect(screen.getByText(/The value is too high./)).toBeInTheDocument(); - }); - - it('should render a label telling the user there are X missing components if hasMinError is true and the component is not open', () => { - setup({ hasMinError: true }); - - expect(screen.getByText(/missing components/)).toBeInTheDocument(); - }); - - it('should call the onClick handler when the button is clicked', () => { + it('should call the onClick handler when the button is clicked', async () => { const onClick = jest.fn(); - setup({ onClick }); + const { getByRole, user } = render({ onClick }); - screen.getByText(/test/).click(); + await user.click(getByRole('button', { name: 'test' })); expect(onClick).toHaveBeenCalled(); }); - it('should not call the onClick handler when the button is disabled', () => { + it('should not call the onClick handler when the button is disabled', async () => { const onClick = jest.fn(); - setup({ onClick, isDisabled: true }); + const { getByRole, user } = render({ onClick, isDisabled: true }); - screen.getByText(/test/).click(); + await expect(() => user.click(getByRole('button', { name: 'test' }))).rejects.toThrow( + /pointer-events: none/ + ); expect(onClick).not.toHaveBeenCalled(); }); diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCard.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCard.test.js deleted file mode 100644 index d50b0feeea..0000000000 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCard.test.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; - -import { ThemeProvider, lightTheme } from '@strapi/design-system'; - -import GlobalStyle from '../../../../../components/GlobalStyle'; - -import ComponentCard from '../ComponentCard'; - -describe('ComponentCard', () => { - const setup = (props) => - render( - - test - - - ); - - it('should call the onClick handler when passed', () => { - const onClick = jest.fn(); - const { getByText } = setup({ onClick }); - fireEvent.click(getByText('test')); - expect(onClick).toHaveBeenCalled(); - }); -}); diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCategory.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCategory.test.js index 239a0b9c15..4f9ad0b022 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCategory.test.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentCategory.test.js @@ -1,27 +1,35 @@ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render as renderRTL } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { IntlProvider } from 'react-intl'; -import ComponentCategory from '../ComponentCategory'; +import { ComponentCategory } from '../ComponentCategory'; describe('ComponentCategory', () => { - const setup = (props) => - render( - - - - - - ); + const render = (props) => ({ + ...renderRTL( + , + { + wrapper: ({ children }) => ( + + + {children} + + + ), + } + ), + user: userEvent.setup(), + }); it('should render my array of components when passed and the accordion is open', () => { - setup({ + const { getByRole } = render({ isOpen: true, components: [ { @@ -34,31 +42,31 @@ describe('ComponentCategory', () => { ], }); - expect(screen.getByText(/myComponent/)).toBeInTheDocument(); + expect(getByRole('button', { name: /myComponent/ })).toBeInTheDocument(); }); it('should render the category as the accordion buttons label', () => { - setup({ + const { getByText } = render({ category: 'myCategory', }); - expect(screen.getByText(/myCategory/)).toBeInTheDocument(); + expect(getByText(/myCategory/)).toBeInTheDocument(); }); - it('should call the onToggle callback when the accordion trigger is pressed', () => { + it('should call the onToggle callback when the accordion trigger is pressed', async () => { const onToggle = jest.fn(); - setup({ + const { getByRole, user } = render({ onToggle, }); - fireEvent.click(screen.getByText(/testing/)); + await user.click(getByRole('button', { name: /testing/ })); expect(onToggle).toHaveBeenCalledWith('testing'); }); - it('should call onAddComponent with the componentUid when a ComponentCard is clicked', () => { + it('should call onAddComponent with the componentUid when a ComponentCard is clicked', async () => { const onAddComponent = jest.fn(); - setup({ + const { getByRole, user } = render({ isOpen: true, onAddComponent, components: [ @@ -72,7 +80,7 @@ describe('ComponentCategory', () => { ], }); - fireEvent.click(screen.getByText(/myComponent/)); + await user.click(getByRole('button', { name: /myComponent/ })); expect(onAddComponent).toHaveBeenCalledWith('test'); }); diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentPicker.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentPicker.test.js index 4e67cbf0d9..4078d91d97 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentPicker.test.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/ComponentPicker.test.js @@ -1,70 +1,73 @@ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render as renderRTL } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { IntlProvider } from 'react-intl'; -import ComponentPicker from '../ComponentPicker'; +import { ComponentPicker } from '../ComponentPicker'; -import { layoutData } from './fixtures'; - -jest.mock('../../../../hooks', () => ({ - useContentTypeLayout: jest.fn().mockReturnValue({ - getComponentLayout: jest.fn().mockImplementation((componentUid) => layoutData[componentUid]), - }), -})); +import { dynamicComponentsByCategory } from './fixtures'; describe('ComponentPicker', () => { - afterEach(() => { - jest.restoreAllMocks(); - }); - const Component = (props) => ( - - - - - + ); - const setup = (props) => render(); + const render = (props) => ({ + ...renderRTL(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }), + user: userEvent.setup(), + }); it('should by default give me the instruction to Pick one Component', () => { - setup(); + const { getByText } = render(); - expect(screen.getByText(/Pick one component/)).toBeInTheDocument(); + expect(getByText(/Pick one component/)).toBeInTheDocument(); }); it('should render null if isOpen is false', () => { - setup({ isOpen: false }); + const { queryByText } = render({ isOpen: false }); - expect(screen.queryByText(/Pick one component/)).not.toBeInTheDocument(); + expect(queryByText(/Pick one component/)).not.toBeInTheDocument(); }); it('should render the category names by default', () => { - setup({ components: ['component1', 'component2'] }); + const { getByText } = render({ components: ['component1', 'component2'] }); - expect(screen.getByText(/myComponents/)).toBeInTheDocument(); + expect(getByText(/myComponents/)).toBeInTheDocument(); }); it('should open the first category of components when isOpen changes to true from false', () => { - const { rerender } = setup({ + const { rerender, getByRole, queryByRole } = render({ isOpen: false, }); rerender(); - expect(screen.getByText(/component1/)).toBeInTheDocument(); - expect(screen.queryByText(/component3/)).not.toBeInTheDocument(); + expect(getByRole('button', { name: /component1/ })).toBeInTheDocument(); + expect(queryByRole('button', { name: /component3/ })).not.toBeInTheDocument(); }); - it('should call onClickAddComponent with the componentUid when a Component is clicked', () => { + it('should call onClickAddComponent with the componentUid when a Component is clicked', async () => { const onClickAddComponent = jest.fn(); - setup({ + const { user, getByRole } = render({ components: ['component1', 'component2'], onClickAddComponent, }); - fireEvent.click(screen.getByText(/component1/)); + await user.click(getByRole('button', { name: /component1/ })); expect(onClickAddComponent).toHaveBeenCalledWith('component1'); }); diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/DynamicComponent.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/DynamicComponent.test.js index b4ab46d92a..b002fa95d8 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/DynamicComponent.test.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/DynamicComponent.test.js @@ -1,13 +1,14 @@ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render as renderRTL, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { IntlProvider } from 'react-intl'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import DynamicComponent from '../DynamicComponent'; +import { DynamicComponent } from '../DynamicComponent'; -import { layoutData } from './fixtures'; +import { layoutData, dynamicComponentsByCategory } from './fixtures'; jest.mock('../../../../hooks', () => ({ ...jest.requireActual('../../../../hooks'), @@ -42,65 +43,72 @@ describe('DynamicComponent', () => { // eslint-disable-next-line react/prop-types const TestComponent = ({ testingDnd, ...restProps }) => ( - - - - - {testingDnd ? : null} - - - + <> + + {testingDnd ? : null} + ); - const setup = (props) => render(); - - it('should by default render the name of the component in the accordion trigger', () => { - setup(); - - expect(screen.getByRole('button', { name: 'component1' })).toBeInTheDocument(); + const render = (props) => ({ + ...renderRTL(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }), + user: userEvent.setup(), }); - it('should allow removal of the component & call the onRemoveComponentClick callback when the field isAllowed', () => { - const onRemoveComponentClick = jest.fn(); - setup({ isFieldAllowed: true, onRemoveComponentClick }); + it('should by default render the name of the component in the accordion trigger', () => { + const { getByRole } = render(); - fireEvent.click(screen.getByRole('button', { name: 'Delete component1' })); + expect(getByRole('button', { name: 'component1' })).toBeInTheDocument(); + }); + + it('should allow removal of the component & call the onRemoveComponentClick callback when the field isAllowed', async () => { + const onRemoveComponentClick = jest.fn(); + const { getByRole, user } = render({ isFieldAllowed: true, onRemoveComponentClick }); + + await user.click(getByRole('button', { name: 'Delete component1' })); expect(onRemoveComponentClick).toHaveBeenCalled(); }); it('should not show you the delete component button if isFieldAllowed is false', () => { - setup({ isFieldAllowed: false }); + const { queryByRole } = render({ isFieldAllowed: false }); - expect(screen.queryByRole('button', { name: 'Delete component1' })).not.toBeInTheDocument(); + expect(queryByRole('button', { name: 'Delete component1' })).not.toBeInTheDocument(); }); - it('should hide the field component when you close the accordion', () => { - setup(); + it('should hide the field component when you close the accordion', async () => { + const { queryByText, user, getByRole } = render(); - expect(screen.queryByText("I'm a field component")).toBeInTheDocument(); + expect(queryByText("I'm a field component")).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: 'component1' })); + await user.click(getByRole('button', { name: 'component1' })); - expect(screen.queryByText("I'm a field component")).not.toBeInTheDocument(); + expect(queryByText("I'm a field component")).not.toBeInTheDocument(); }); describe('Keyboard drag and drop', () => { it('should not move with arrow keys if the button is not pressed first', () => { const onMoveComponent = jest.fn(); - setup({ + const { getAllByText } = render({ onMoveComponent, testingDnd: true, }); - const [draggedItem] = screen.getAllByText('Drag'); + const [draggedItem] = getAllByText('Drag'); fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' }); expect(onMoveComponent).not.toBeCalled(); }); it('should move with the arrow keys if the button has been activated first', () => { const onMoveComponent = jest.fn(); - setup({ onMoveComponent, testingDnd: true }); - const [draggedItem] = screen.getAllByText('Drag'); + const { getAllByText } = render({ onMoveComponent, testingDnd: true }); + const [draggedItem] = getAllByText('Drag'); fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' }); expect(onMoveComponent).toBeCalledWith(1, 0); @@ -108,8 +116,8 @@ describe('DynamicComponent', () => { it('should move with the arrow keys if the button has been activated and then not move after the button has been deactivated', () => { const onMoveComponent = jest.fn(); - setup({ onMoveComponent, testingDnd: true }); - const [draggedItem] = screen.getAllByText('Drag'); + const { getAllByText } = render({ onMoveComponent, testingDnd: true }); + const [draggedItem] = getAllByText('Drag'); fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' }); fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); @@ -119,8 +127,8 @@ describe('DynamicComponent', () => { it('should exit drag and drop mode when the escape key is pressed', () => { const onMoveComponent = jest.fn(); - setup({ onMoveComponent, testingDnd: true }); - const [draggedItem] = screen.getAllByText('Drag'); + const { getAllByText } = render({ onMoveComponent, testingDnd: true }); + const [draggedItem] = getAllByText('Drag'); fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); fireEvent.keyDown(draggedItem, { key: 'Escape', code: 'Escape' }); fireEvent.keyDown(draggedItem, { key: 'ArrowUp', code: 'ArrowUp' }); @@ -128,5 +136,66 @@ describe('DynamicComponent', () => { }); }); + describe('adding above and below components', () => { + it('should render a menu button with two items that have submenus that list the components grouped by categories', async () => { + const { getByRole, getByText, user } = render({ dynamicComponentsByCategory }); + + expect(getByRole('button', { name: 'More actions' })).toBeInTheDocument(); + + await user.click(getByRole('button', { name: 'More actions' })); + + expect(getByRole('menuitem', { name: 'Add component above' })).toBeInTheDocument(); + expect(getByRole('menuitem', { name: 'Add component below' })).toBeInTheDocument(); + + await user.click(getByRole('menuitem', { name: 'Add component above' })); + + expect(getByText('myComponents')).toBeInTheDocument(); + expect(getByText('otherComponents')).toBeInTheDocument(); + + expect(getByRole('menuitem', { name: 'component1' })).toBeInTheDocument(); + expect(getByRole('menuitem', { name: 'component2' })).toBeInTheDocument(); + expect(getByRole('menuitem', { name: 'component3' })).toBeInTheDocument(); + + await user.click(getByRole('menuitem', { name: 'Add component below' })); + + expect(getByText('myComponents')).toBeInTheDocument(); + expect(getByText('otherComponents')).toBeInTheDocument(); + + expect(getByRole('menuitem', { name: 'component1' })).toBeInTheDocument(); + expect(getByRole('menuitem', { name: 'component2' })).toBeInTheDocument(); + expect(getByRole('menuitem', { name: 'component3' })).toBeInTheDocument(); + }); + + it('should call the onAddComponent callback with the correct index when adding above', async () => { + const onAddComponent = jest.fn(); + const { getByRole, user } = render({ dynamicComponentsByCategory, onAddComponent, index: 0 }); + + await user.click(getByRole('button', { name: 'More actions' })); + await user.click(getByRole('menuitem', { name: 'Add component above' })); + + /** + * @note – for some reason, user.click() doesn't work here + */ + fireEvent.click(getByRole('menuitem', { name: 'component1' })); + + expect(onAddComponent).toHaveBeenCalledWith('component1', 0); + }); + + it('should call the onAddComponent callback with the correct index when adding below', async () => { + const onAddComponent = jest.fn(); + const { getByRole, user } = render({ dynamicComponentsByCategory, onAddComponent, index: 0 }); + + await user.click(getByRole('button', { name: 'More actions' })); + await user.click(getByRole('menuitem', { name: 'Add component below' })); + + /** + * @note – for some reason, user.click() doesn't work here + */ + fireEvent.click(getByRole('menuitem', { name: 'component1' })); + + expect(onAddComponent).toHaveBeenCalledWith('component1', 1); + }); + }); + it.todo('should handle errors in the fields'); }); diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/DynamicZoneLabel.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/DynamicZoneLabel.test.js index 970ca04ced..dc211ebabd 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/DynamicZoneLabel.test.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/DynamicZoneLabel.test.js @@ -1,11 +1,11 @@ import React from 'react'; import { IntlProvider } from 'react-intl'; -import { render, screen } from '@testing-library/react'; +import { render as renderRTL } from '@testing-library/react'; import { ThemeProvider, lightTheme, Tooltip } from '@strapi/design-system'; import { Earth } from '@strapi/icons'; -import DynamicZoneLabel from '../DynamicZoneLabel'; +import { DynamicZoneLabel } from '../DynamicZoneLabel'; const LabelAction = () => { return ( @@ -26,50 +26,50 @@ describe('DynamicZoneLabel', () => { ); - const setup = (props) => render(); + const render = (props) => renderRTL(); it('should render the label by default', () => { - setup(); + const { getByText } = render(); - expect(screen.getByText(/dynamic zone/)).toBeInTheDocument(); + expect(getByText(/dynamic zone/)).toBeInTheDocument(); }); it('should render the name of the zone when there is no label', () => { - setup({ label: '' }); + const { getByText } = render({ label: '' }); - expect(screen.getByText(/test/)).toBeInTheDocument(); + expect(getByText(/test/)).toBeInTheDocument(); }); it('should always render the amount of components no matter the value', () => { - const { rerender } = setup({ numberOfComponents: 0 }); + const { rerender, getByText } = render({ numberOfComponents: 0 }); - expect(screen.getByText(/0/)).toBeInTheDocument(); + expect(getByText(/0/)).toBeInTheDocument(); rerender(); - expect(screen.getByText(/2/)).toBeInTheDocument(); + expect(getByText(/2/)).toBeInTheDocument(); }); it('should render an asteriks when the required prop is true', () => { - setup({ required: true }); + const { getByText } = render({ required: true }); - expect(screen.getByText(/\*/)).toBeInTheDocument(); + expect(getByText(/\*/)).toBeInTheDocument(); }); it('should render the labelAction when it is provided', () => { - setup({ labelAction: }); + const { getByLabelText } = render({ labelAction: }); - expect(screen.getByLabelText(/i18n/)).toBeInTheDocument(); + expect(getByLabelText(/i18n/)).toBeInTheDocument(); }); it('should render a description if passed as a prop', () => { - setup({ + const { getByText } = render({ intlDescription: { id: 'description', defaultMessage: 'description', }, }); - expect(screen.getByText(/description/)).toBeInTheDocument(); + expect(getByText(/description/)).toBeInTheDocument(); }); }); diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/fixtures.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/fixtures.js index 49f3b6e0fc..233748d70c 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/fixtures.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/components/tests/fixtures.js @@ -21,3 +21,31 @@ export const layoutData = { }, }, }; + +export const dynamicComponentsByCategory = { + myComponents: [ + { + componentUid: 'component1', + info: { + displayName: 'component1', + icon: undefined, + }, + }, + { + componentUid: 'component2', + info: { + displayName: 'component2', + icon: undefined, + }, + }, + ], + otherComponents: [ + { + componentUid: 'component3', + info: { + displayName: 'component3', + icon: undefined, + }, + }, + ], +}; 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 9c1b928af2..9de9043c10 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 @@ -1,76 +1,113 @@ -import React, { memo, useMemo, useState } from 'react'; -import get from 'lodash/get'; -import isEqual from 'lodash/isEqual'; +import React, { useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { Box, Flex, VisuallyHidden } from '@strapi/design-system'; -import { NotAllowedInput, useNotification } from '@strapi/helper-plugin'; +import { NotAllowedInput, useNotification, useCMEditViewDataManager } from '@strapi/helper-plugin'; import { useIntl } from 'react-intl'; import { getTrad } from '../../utils'; -import connect from './utils/connect'; -import select from './utils/select'; - -import DynamicZoneComponent from './components/DynamicComponent'; -import AddComponentButton from './components/AddComponentButton'; -import DynamicZoneLabel from './components/DynamicZoneLabel'; -import ComponentPicker from './components/ComponentPicker'; +import { DynamicComponent } from './components/DynamicComponent'; +import { AddComponentButton } from './components/AddComponentButton'; +import { DynamicZoneLabel } from './components/DynamicZoneLabel'; +import { ComponentPicker } from './components/ComponentPicker'; import { useContentTypeLayout } from '../../hooks'; -const DynamicZone = ({ - name, - // Passed with the select function - addComponentToDynamicZone, - formErrors, - isCreatingEntry, - isFieldAllowed, - isFieldReadable, - labelAction, - moveComponentField, - removeComponentFromDynamicZone, - dynamicDisplayedComponents, - fieldSchema, - metadatas, -}) => { +const DynamicZone = ({ name, labelAction, fieldSchema, metadatas }) => { + // We cannot use the default props here + const { max = Infinity, min = -Infinity, components = [], required = false } = fieldSchema; + const [addComponentIsOpen, setAddComponentIsOpen] = useState(false); const [liveText, setLiveText] = useState(''); + const { + addComponentToDynamicZone, + createActionAllowedFields, + isCreatingEntry, + formErrors, + modifiedData, + moveComponentField, + removeComponentFromDynamicZone, + readActionAllowedFields, + updateActionAllowedFields, + } = useCMEditViewDataManager(); + + const dynamicDisplayedComponents = useMemo( + () => + (modifiedData?.[name] ?? []).map((data) => { + return { + componentUid: data.__component, + id: data.id ?? data.__temp_key__, + }; + }), + [modifiedData, name] + ); + + const { getComponentLayout } = useContentTypeLayout(); + + /** + * @type {Record}>>} + */ + const dynamicComponentsByCategory = useMemo(() => { + return components.reduce((acc, componentUid) => { + const { category, info, attributes } = getComponentLayout(componentUid); + const component = { componentUid, info, attributes }; + + if (!acc[category]) { + acc[category] = []; + } + + acc[category] = [...acc[category], component]; + + return acc; + }, {}); + }, [components, getComponentLayout]); + const { formatMessage } = useIntl(); const toggleNotification = useNotification(); - const { getComponentLayout, components } = useContentTypeLayout(); + + const isFieldAllowed = useMemo(() => { + const allowedFields = isCreatingEntry ? createActionAllowedFields : updateActionAllowedFields; + + return allowedFields.includes(name); + }, [name, isCreatingEntry, createActionAllowedFields, updateActionAllowedFields]); + + const isFieldReadable = useMemo(() => { + const allowedFields = isCreatingEntry ? [] : readActionAllowedFields; + + return allowedFields.includes(name); + }, [name, isCreatingEntry, readActionAllowedFields]); const dynamicDisplayedComponentsLength = dynamicDisplayedComponents.length; const intlDescription = metadatas.description ? { id: metadatas.description, defaultMessage: metadatas.description } : null; - // We cannot use the default props here - const { max = Infinity, min = -Infinity } = fieldSchema; - const dynamicZoneErrors = useMemo(() => { - return Object.keys(formErrors) - .filter((key) => { - return key === name; - }) - .map((key) => formErrors[key]); - }, [formErrors, name]); + const dynamicZoneError = formErrors[name]; const missingComponentNumber = min - dynamicDisplayedComponentsLength; - const hasError = dynamicZoneErrors.length > 0; + const hasError = !!dynamicZoneError; - const hasMinError = - dynamicZoneErrors.length > 0 && get(dynamicZoneErrors, [0, 'id'], '').includes('min'); - - const hasMaxError = - hasError && get(dynamicZoneErrors, [0, 'id'], '') === 'components.Input.error.validation.max'; - - const handleAddComponent = (componentUid) => { + const handleAddComponent = (componentUid, position) => { setAddComponentIsOpen(false); const componentLayoutData = getComponentLayout(componentUid); - addComponentToDynamicZone(name, componentLayoutData, components, hasError); + const allComponents = Object.values(dynamicComponentsByCategory).reduce((acc, components) => { + const componentObjects = components.reduce((acc, { componentUid, attributes }) => { + acc[componentUid] = { + attributes, + uid: componentUid, + }; + + return acc; + }, {}); + + return { ...acc, ...componentObjects }; + }, {}); + + addComponentToDynamicZone(name, componentLayoutData, allComponents, hasError, position); }; const handleClickOpenPicker = () => { @@ -160,6 +197,38 @@ const DynamicZone = ({ removeComponentFromDynamicZone(name, currentIndex); }; + const renderButtonLabel = () => { + if (addComponentIsOpen) { + return formatMessage({ id: 'app.utils.close-label', defaultMessage: 'Close' }); + } + + if (hasError && dynamicZoneError.id.includes('max')) { + return formatMessage({ + id: 'components.Input.error.validation.max', + defaultMessage: 'The value is too high.', + }); + } + + if (hasError && dynamicZoneError.id.includes('min')) { + return formatMessage( + { + id: getTrad(`components.DynamicZone.missing-components`), + defaultMessage: + 'There {number, plural, =0 {are # missing components} one {is # missing component} other {are # missing components}}', + }, + { number: missingComponentNumber } + ); + } + + return formatMessage( + { + id: getTrad('components.DynamicZone.add-component'), + defaultMessage: 'Add a component to {componentName}', + }, + { componentName: metadatas.label || name } + ); + }; + if (!isFieldAllowed && (isCreatingEntry || (!isFieldReadable && !isCreatingEntry))) { return ( {formatMessage({ @@ -194,7 +263,7 @@ const DynamicZone = ({ {liveText}
    {dynamicDisplayedComponents.map(({ componentUid, id }, index) => ( - ))}
)} - - + + + {renderButtonLabel()} + + @@ -233,44 +302,23 @@ const DynamicZone = ({ }; DynamicZone.defaultProps = { - dynamicDisplayedComponents: [], - fieldSchema: { - max: Infinity, - min: -Infinity, - }, + fieldSchema: {}, labelAction: null, }; DynamicZone.propTypes = { - addComponentToDynamicZone: PropTypes.func.isRequired, - dynamicDisplayedComponents: PropTypes.arrayOf( - PropTypes.shape({ - componentUid: PropTypes.string.isRequired, - id: PropTypes.number.isRequired, - }) - ), fieldSchema: PropTypes.shape({ - components: PropTypes.array.isRequired, + components: PropTypes.array, max: PropTypes.number, min: PropTypes.number, required: PropTypes.bool, }), - formErrors: PropTypes.object.isRequired, - isCreatingEntry: PropTypes.bool.isRequired, - isFieldAllowed: PropTypes.bool.isRequired, - isFieldReadable: PropTypes.bool.isRequired, labelAction: PropTypes.element, metadatas: PropTypes.shape({ description: PropTypes.string, label: PropTypes.string, }).isRequired, - moveComponentField: PropTypes.func.isRequired, name: PropTypes.string.isRequired, - removeComponentFromDynamicZone: PropTypes.func.isRequired, }; -const Memoized = memo(DynamicZone, isEqual); - -export default connect(Memoized, select); - export { DynamicZone }; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/tests/index.test.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/tests/index.test.js index cb416e998b..a6bdd21942 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/tests/index.test.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicZone/tests/index.test.js @@ -1,6 +1,8 @@ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render as renderRTL } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; +import { useCMEditViewDataManager } from '@strapi/helper-plugin'; import { IntlProvider } from 'react-intl'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; @@ -11,9 +13,25 @@ import { layoutData } from './fixtures'; const toggleNotification = jest.fn(); +const TEST_NAME = 'DynamicZoneComponent'; + +const defaultCMEditViewMock = { + isCreatingEntry: false, + addComponentToDynamicZone: jest.fn(), + removeComponentFromDynamicZone: jest.fn(), + moveComponentField: jest.fn(), + createActionAllowedFields: [TEST_NAME], + updateActionAllowedFields: [TEST_NAME], + readActionAllowedFields: [TEST_NAME], + modifiedData: {}, + formErrors: {}, +}; + jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), - useCMEditViewDataManager: jest.fn().mockImplementation(() => ({ modifiedData: {} })), + useCMEditViewDataManager: jest.fn().mockImplementation(() => ({ + ...defaultCMEditViewMock, + })), useNotification: jest.fn().mockImplementation(() => toggleNotification), NotAllowedInput: () => 'This field is not allowed', })); @@ -38,154 +56,193 @@ describe('DynamicZone', () => { }); const defaultProps = { - addComponentToDynamicZone: jest.fn(), - isCreatingEntry: true, - isFieldAllowed: true, - isFieldReadable: true, fieldSchema: { components: ['component1', 'component2', 'component3'], }, - formErrors: {}, metadatas: { label: 'dynamic zone', description: 'dynamic description', }, - moveComponentField: jest.fn(), name: 'DynamicZoneComponent', - removeComponentFromDynamicZone: jest.fn(), }; - const TestComponent = (props) => ( - - - - - - - - ); + const TestComponent = (props) => ; - const setup = (props) => render(); + const render = (props) => ({ + ...renderRTL(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }), + user: userEvent.setup(), + }); describe('rendering', () => { it('should not render the dynamic zone if there are no dynamic components to render', () => { - setup(); + const { queryByText } = render(); - expect(screen.queryByText('dynamic zone')).not.toBeInTheDocument(); - expect(screen.queryByText('dynamic description')).not.toBeInTheDocument(); + expect(queryByText('dynamic zone')).not.toBeInTheDocument(); + expect(queryByText('dynamic description')).not.toBeInTheDocument(); }); - it('should render the AddComponentButton by default and render the ComponentPicker when that button is clicked', () => { - setup(); + it('should render the AddComponentButton by default and render the ComponentPicker when that button is clicked', async () => { + const { getByRole, getByText, user } = render(); - const addComponentButton = screen.getByRole('button', { name: /Add a component to/i }); + const addComponentButton = getByRole('button', { name: /Add a component to/i }); expect(addComponentButton).toBeInTheDocument(); - fireEvent.click(addComponentButton); + await user.click(addComponentButton); - expect(screen.getByText('Pick one component')).toBeInTheDocument(); + expect(getByText('Pick one component')).toBeInTheDocument(); }); it('should render the dynamic zone of components when there are dynamic components to render', () => { - setup({ - dynamicDisplayedComponents: [ - { componentUid: 'component1', id: 0 }, - { componentUid: 'component2', id: 0 }, - ], - }); + useCMEditViewDataManager.mockImplementationOnce(() => ({ + ...defaultCMEditViewMock, + modifiedData: { + [TEST_NAME]: [ + { + __component: 'component1', + id: 0, + }, + { + __component: 'component2', + id: 0, + }, + ], + }, + })); + const { getByText } = render(); - expect(screen.getByText('dynamic zone')).toBeInTheDocument(); - expect(screen.getByText('dynamic description')).toBeInTheDocument(); + expect(getByText('dynamic zone')).toBeInTheDocument(); + expect(getByText('dynamic description')).toBeInTheDocument(); - expect(screen.getByText('component1')).toBeInTheDocument(); - expect(screen.getByText('component2')).toBeInTheDocument(); + expect(getByText('component1')).toBeInTheDocument(); + expect(getByText('component2')).toBeInTheDocument(); }); it('should render the not allowed input if the field is not allowed & the entry is being created', () => { - setup({ - isFieldAllowed: false, + useCMEditViewDataManager.mockImplementationOnce(() => ({ + ...defaultCMEditViewMock, isCreatingEntry: true, - }); + createActionAllowedFields: [], + })); + const { queryByText, getByText } = render(); - expect(screen.queryByText('dynamic zone')).not.toBeInTheDocument(); + expect(queryByText('dynamic zone')).not.toBeInTheDocument(); - expect(screen.getByText('This field is not allowed')).toBeInTheDocument(); + expect(getByText('This field is not allowed')).toBeInTheDocument(); }); it('should render the not allowed input if the field is not allowed & the entry is not being created and the field is not readable', () => { - setup({ - isFieldAllowed: false, - isCreatingEntry: false, - isFieldReadable: false, - }); + useCMEditViewDataManager.mockImplementationOnce(() => ({ + ...defaultCMEditViewMock, + updateActionAllowedFields: [], + readActionAllowedFields: [], + })); + const { queryByText, getByText } = render(); - expect(screen.queryByText('dynamic zone')).not.toBeInTheDocument(); + expect(queryByText('dynamic zone')).not.toBeInTheDocument(); - expect(screen.getByText('This field is not allowed')).toBeInTheDocument(); + expect(getByText('This field is not allowed')).toBeInTheDocument(); }); }); describe('callbacks', () => { - it('should call the addComponentToDynamicZone callback when the AddComponentButton is clicked', () => { + it('should call the addComponentToDynamicZone callback when the AddComponentButton is clicked', async () => { const addComponentToDynamicZone = jest.fn(); + useCMEditViewDataManager.mockImplementation(() => ({ + ...defaultCMEditViewMock, + addComponentToDynamicZone, + })); - setup({ addComponentToDynamicZone }); + const { user, getByRole } = render(); - const addComponentButton = screen.getByRole('button', { name: /Add a component to/i }); + const addComponentButton = getByRole('button', { name: /Add a component to/i }); - fireEvent.click(addComponentButton); + await user.click(addComponentButton); - const componentPickerButton = screen.getByRole('button', { - name: /component1/i, + const componentPickerButton = getByRole('button', { + name: 'component1', }); - fireEvent.click(componentPickerButton); + await user.click(componentPickerButton); expect(addComponentToDynamicZone).toHaveBeenCalledWith( 'DynamicZoneComponent', { category: 'myComponents', info: { displayName: 'component1', icon: undefined } }, - undefined, - false + expect.any(Object), + false, + undefined ); }); - it('should call the removeComponentFromDynamicZone callback when the RemoveButton is clicked', () => { + it('should call the removeComponentFromDynamicZone callback when the RemoveButton is clicked', async () => { const removeComponentFromDynamicZone = jest.fn(); - - setup({ + useCMEditViewDataManager.mockImplementationOnce(() => ({ + ...defaultCMEditViewMock, removeComponentFromDynamicZone, - dynamicDisplayedComponents: [ - { componentUid: 'component1', id: 0 }, - { componentUid: 'component2', id: 0 }, - ], - }); + modifiedData: { + [TEST_NAME]: [ + { + __component: 'component1', + id: 0, + }, + { + __component: 'component2', + id: 0, + }, + ], + }, + })); - const removeButton = screen.getByRole('button', { name: /Delete component1/i }); + const { user, getByRole } = render(); - fireEvent.click(removeButton); + const removeButton = getByRole('button', { name: /Delete component1/i }); + + await user.click(removeButton); expect(removeComponentFromDynamicZone).toHaveBeenCalledWith('DynamicZoneComponent', 0); }); }); describe('side effects', () => { - it('should call the toggleNotification callback if the amount of dynamic components has hit its max and the user tries to add another', () => { - setup({ - dynamicDisplayedComponents: [ - { componentUid: 'component1', id: 0 }, - { componentUid: 'component2', id: 0 }, - { componentUid: 'component3', id: 0 }, - ], + it('should call the toggleNotification callback if the amount of dynamic components has hit its max and the user tries to add another', async () => { + useCMEditViewDataManager.mockImplementationOnce(() => ({ + ...defaultCMEditViewMock, + modifiedData: { + [TEST_NAME]: [ + { + __component: 'component1', + id: 0, + }, + { + __component: 'component2', + id: 0, + }, + { + __component: 'component3', + id: 0, + }, + ], + }, + })); + + const { user, getByRole } = render({ fieldSchema: { components: ['component1', 'component2', 'component3'], max: 3, }, }); - const addComponentButton = screen.getByRole('button', { name: /Add a component to/i }); + const addComponentButton = getByRole('button', { name: /Add a component to/i }); - fireEvent.click(addComponentButton); + await user.click(addComponentButton); expect(toggleNotification).toHaveBeenCalledWith({ type: 'info', @@ -198,82 +255,188 @@ describe('DynamicZone', () => { describe('Accessibility', () => { it('should have have description text', () => { - setup({ - dynamicDisplayedComponents: [ - { componentUid: 'component1', id: 0 }, - { componentUid: 'component2', id: 0 }, - ], - }); + useCMEditViewDataManager.mockImplementationOnce(() => ({ + ...defaultCMEditViewMock, + modifiedData: { + [TEST_NAME]: [ + { + __component: 'component1', + id: 0, + }, + { + __component: 'component2', + id: 0, + }, + ], + }, + })); - expect(screen.queryByText('Press spacebar to grab and re-order')).toBeInTheDocument(); + const { queryByText } = render(); + + expect(queryByText('Press spacebar to grab and re-order')).toBeInTheDocument(); }); it('should update the live text when an item has been grabbed', async () => { - setup({ - dynamicDisplayedComponents: [ - { componentUid: 'component1', id: 0 }, - { componentUid: 'component2', id: 0 }, - ], - }); + useCMEditViewDataManager.mockImplementation(() => ({ + ...defaultCMEditViewMock, + modifiedData: { + [TEST_NAME]: [ + { + __component: 'component1', + id: 0, + }, + { + __component: 'component2', + id: 0, + }, + ], + }, + })); - const [draggedItem] = screen.getAllByText('Drag'); + const { getAllByRole, queryByText, user } = render(); - fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); + const [draggedItem] = getAllByRole('button', { name: 'Drag' }); + + draggedItem.focus(); + + await user.keyboard('[Space]'); expect( - screen.queryByText( + queryByText( /Press up and down arrow to change position, Spacebar to drop, Escape to cancel/ ) ).toBeInTheDocument(); }); - it('should change the live text when an item has been moved', () => { - setup({ - dynamicDisplayedComponents: [ - { componentUid: 'component1', id: 0 }, - { componentUid: 'component2', id: 0 }, - ], - }); + it('should change the live text when an item has been moved', async () => { + useCMEditViewDataManager.mockImplementation(() => ({ + ...defaultCMEditViewMock, + modifiedData: { + [TEST_NAME]: [ + { + __component: 'component1', + id: 0, + }, + { + __component: 'component2', + id: 0, + }, + ], + }, + })); - const [draggedItem] = screen.getAllByText('Drag'); + const { user, getAllByRole, queryByText } = render(); - fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); - fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' }); + const [draggedItem] = getAllByRole('button', { name: 'Drag' }); - expect(screen.queryByText(/New position in list/)).toBeInTheDocument(); + draggedItem.focus(); + + await user.keyboard('[Space]'); + await user.keyboard('[ArrowDown]'); + + expect(queryByText(/New position in list/)).toBeInTheDocument(); }); - it('should change the live text when an item has been dropped', () => { - setup({ - dynamicDisplayedComponents: [ - { componentUid: 'component1', id: 0 }, - { componentUid: 'component2', id: 0 }, - ], - }); + it('should change the live text when an item has been dropped', async () => { + useCMEditViewDataManager.mockImplementation(() => ({ + ...defaultCMEditViewMock, + modifiedData: { + [TEST_NAME]: [ + { + __component: 'component1', + id: 0, + }, + { + __component: 'component2', + id: 0, + }, + ], + }, + })); - const [draggedItem] = screen.getAllByText('Drag'); + const { getAllByRole, user, queryByText } = render(); - fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); - fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' }); - fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); + const [draggedItem] = getAllByRole('button', { name: 'Drag' }); - expect(screen.queryByText(/Final position in list/)).toBeInTheDocument(); + draggedItem.focus(); + + await user.keyboard('[Space]'); + await user.keyboard('[ArrowDown]'); + await user.keyboard('[Space]'); + + expect(queryByText(/Final position in list/)).toBeInTheDocument(); }); - it('should change the live text after the reordering interaction has been cancelled', () => { - setup({ - dynamicDisplayedComponents: [ - { componentUid: 'component1', id: 0 }, - { componentUid: 'component2', id: 0 }, - ], - }); + it('should change the live text after the reordering interaction has been cancelled', async () => { + useCMEditViewDataManager.mockImplementation(() => ({ + ...defaultCMEditViewMock, + modifiedData: { + [TEST_NAME]: [ + { + __component: 'component1', + id: 0, + }, + { + __component: 'component2', + id: 0, + }, + ], + }, + })); - const [draggedItem] = screen.getAllByText('Drag'); + const { getAllByRole, user, queryByText } = render(); - fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' }); - fireEvent.keyDown(draggedItem, { key: 'Escape', code: 'Escape' }); + const [draggedItem] = getAllByRole('button', { name: 'Drag' }); - expect(screen.queryByText(/Re-order cancelled/)).toBeInTheDocument(); + draggedItem.focus(); + + await user.keyboard('[Space]'); + await user.keyboard('[Escape]'); + + expect(queryByText(/Re-order cancelled/)).toBeInTheDocument(); + }); + }); + + describe('Add component button', () => { + it('should render the close label if the component picker is open prop is true', async () => { + const { getByRole, user } = render(); + + expect(getByRole('button', { name: /Add a component to/i })).toBeInTheDocument(); + + await user.click(getByRole('button', { name: /Add a component to/i })); + + expect(getByRole('button', { name: /Close/ })).toBeInTheDocument(); + }); + + it('should render the name of the field when the label is an empty string', () => { + const { getByRole } = render({ metadatas: {} }); + expect(getByRole('button', { name: `Add a component to ${TEST_NAME}` })).toBeInTheDocument(); + }); + + it('should render a too high error if there is hasMaxError is true and the component is not open', () => { + useCMEditViewDataManager.mockImplementation(() => ({ + ...defaultCMEditViewMock, + formErrors: { + [TEST_NAME]: { + id: 'components.Input.error.validation.max', + }, + }, + })); + const { getByRole } = render(); + expect(getByRole('button', { name: /The value is too high./ })).toBeInTheDocument(); + }); + + it('should render a label telling the user there are X missing components if hasMinError is true and the component is not open', () => { + useCMEditViewDataManager.mockImplementation(() => ({ + ...defaultCMEditViewMock, + formErrors: { + [TEST_NAME]: { + id: 'components.Input.error.validation.min', + }, + }, + })); + const { getByRole } = render(); + expect(getByRole('button', { name: /missing components/ })).toBeInTheDocument(); }); }); }); diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/utils/connect.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/utils/connect.js deleted file mode 100644 index eb1b2cd222..0000000000 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/utils/connect.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -function connect(WrappedComponent, select) { - return (props) => { - // eslint-disable-next-line react/prop-types - const selectors = select(props.name); - - return ; - }; -} - -export default connect; diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicZone/utils/select.js b/packages/core/admin/admin/src/content-manager/components/DynamicZone/utils/select.js deleted file mode 100644 index c04f0b8030..0000000000 --- a/packages/core/admin/admin/src/content-manager/components/DynamicZone/utils/select.js +++ /dev/null @@ -1,53 +0,0 @@ -import { useMemo } from 'react'; -import get from 'lodash/get'; -import { useCMEditViewDataManager } from '@strapi/helper-plugin'; - -function useSelect(name) { - const { - addComponentToDynamicZone, - createActionAllowedFields, - isCreatingEntry, - formErrors, - modifiedData, - moveComponentField, - removeComponentFromDynamicZone, - readActionAllowedFields, - updateActionAllowedFields, - } = useCMEditViewDataManager(); - - const dynamicDisplayedComponents = useMemo( - () => - get(modifiedData, [name], []).map((data) => { - return { - componentUid: data.__component, - id: data.id ?? data.__temp_key__, - }; - }), - [modifiedData, name] - ); - - const isFieldAllowed = useMemo(() => { - const allowedFields = isCreatingEntry ? createActionAllowedFields : updateActionAllowedFields; - - return allowedFields.includes(name); - }, [name, isCreatingEntry, createActionAllowedFields, updateActionAllowedFields]); - - const isFieldReadable = useMemo(() => { - const allowedFields = isCreatingEntry ? [] : readActionAllowedFields; - - return allowedFields.includes(name); - }, [name, isCreatingEntry, readActionAllowedFields]); - - return { - addComponentToDynamicZone, - formErrors, - isCreatingEntry, - isFieldAllowed, - isFieldReadable, - moveComponentField, - removeComponentFromDynamicZone, - dynamicDisplayedComponents, - }; -} - -export default useSelect; diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/index.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/index.js index 92d7401767..95696226f0 100644 --- a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/index.js +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/index.js @@ -195,14 +195,21 @@ const EditViewDataManagerProvider = ({ const dispatchAddComponent = useCallback( (type) => - (keys, componentLayoutData, components, shouldCheckErrors = false) => { + ( + keys, + componentLayoutData, + allComponents, + shouldCheckErrors = false, + position = undefined + ) => { trackUsageRef.current('didAddComponentToDynamicZone'); dispatch({ type, keys: keys.split('.'), + position, componentLayoutData, - allComponents: components, + allComponents, shouldCheckErrors, }); }, diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js index b418195601..7153d84c20 100644 --- a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/reducer.js @@ -52,7 +52,13 @@ const reducer = (state, action) => } case 'ADD_COMPONENT_TO_DYNAMIC_ZONE': case 'ADD_REPEATABLE_COMPONENT_TO_FIELD': { - const { keys, allComponents, componentLayoutData, shouldCheckErrors } = action; + const { + keys, + allComponents, + componentLayoutData, + shouldCheckErrors, + position = undefined, + } = action; if (shouldCheckErrors) { draftState.shouldCheckErrors = !state.shouldCheckErrors; @@ -62,7 +68,15 @@ const reducer = (state, action) => draftState.modifiedDZName = keys[0]; } - const currentValue = get(state, ['modifiedData', ...keys], []); + const currentValue = [...get(state, ['modifiedData', ...keys], [])]; + + let actualPosition = position; + + if (actualPosition === undefined) { + actualPosition = currentValue.length; + } else if (actualPosition < 0) { + actualPosition = 0; + } const defaultDataStructure = action.type === 'ADD_COMPONENT_TO_DYNAMIC_ZONE' @@ -87,11 +101,9 @@ const reducer = (state, action) => componentLayoutData.attributes ); - const newValue = Array.isArray(currentValue) - ? [...currentValue, componentDataStructure] - : [componentDataStructure]; + currentValue.splice(actualPosition, 0, componentDataStructure); - set(draftState, ['modifiedData', ...keys], newValue); + set(draftState, ['modifiedData', ...keys], currentValue); break; } diff --git a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/tests/reducer.test.js b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/tests/reducer.test.js index 996529974a..aaac98cea0 100644 --- a/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/tests/reducer.test.js +++ b/packages/core/admin/admin/src/content-manager/components/EditViewDataManagerProvider/tests/reducer.test.js @@ -851,6 +851,78 @@ describe('CONTENT MANAGER | COMPONENTS | EditViewDataManagerProvider | reducer', expect(reducer(state, action)).toEqual(expected); }); + + it('should add a component at a specific position in the array', () => { + const components = { + 'blog.simple': { + uid: 'blog.simple', + attributes: { + id: { + type: 'integer', + }, + name: { + type: 'string', + }, + }, + }, + }; + + const state = { + ...initialState, + componentsDataStructure: { + 'blog.simple': { name: 'test' }, + }, + initialData: { + name: 'name', + dz: [{ name: 'test', __component: 'blog.simple', id: 0 }], + }, + modifiedData: { + name: 'name', + dz: [{ name: 'test', __component: 'blog.simple', id: 0 }], + }, + }; + + const expected = { + ...initialState, + componentsDataStructure: { + 'blog.simple': { name: 'test' }, + }, + initialData: { + name: 'name', + dz: [{ name: 'test', __component: 'blog.simple', id: 0 }], + }, + modifiedData: { + name: 'name', + dz: [ + { name: 'test', __component: 'blog.simple', __temp_key__: 1 }, + { name: 'test', __component: 'blog.simple', id: 0 }, + ], + }, + modifiedDZName: 'dz', + shouldCheckErrors: true, + }; + + const action = { + type: 'ADD_COMPONENT_TO_DYNAMIC_ZONE', + componentLayoutData: { + uid: 'blog.simple', + attributes: { + id: { + type: 'integer', + }, + name: { + type: 'string', + }, + }, + }, + allComponents: components, + keys: ['dz'], + shouldCheckErrors: true, + position: -1, + }; + + expect(reducer(state, action)).toEqual(expected); + }); }); describe('CONNECT_RELATION', () => { diff --git a/packages/core/admin/admin/src/content-manager/pages/EditView/index.js b/packages/core/admin/admin/src/content-manager/pages/EditView/index.js index 836aba2076..16dca54fd8 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditView/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditView/index.js @@ -13,7 +13,7 @@ import { Pencil, Layer } from '@strapi/icons'; import InformationBox from 'ee_else_ce/content-manager/pages/EditView/InformationBox'; import { InjectionZone } from '../../../shared/components'; import permissions from '../../../permissions'; -import DynamicZone from '../../components/DynamicZone'; +import { DynamicZone } from '../../components/DynamicZone'; import CollectionTypeFormWrapper from '../../components/CollectionTypeFormWrapper'; import EditViewDataManagerProvider from '../../components/EditViewDataManagerProvider'; import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper';