mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 19:04:38 +00:00
feat: add dynamic components above and below (#16826)
Co-authored-by: Gustav Hansen <gustav.hansen@strapi.io>
This commit is contained in:
parent
6bd5d14d47
commit
aba64850a5
@ -6,14 +6,41 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { PlusCircle } from '@strapi/icons';
|
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 (
|
||||||
|
<StyledButton
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={isDisabled}
|
||||||
|
hasError={hasError}
|
||||||
|
background="neutral0"
|
||||||
|
paddingTop={3}
|
||||||
|
paddingBottom={3}
|
||||||
|
paddingLeft={4}
|
||||||
|
paddingRight={4}
|
||||||
|
style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}
|
||||||
|
>
|
||||||
|
<Flex as="span" gap={2}>
|
||||||
|
<StyledAddIcon aria-hidden $isOpen={isOpen} $hasError={hasError && !isOpen} />
|
||||||
|
<Typography
|
||||||
|
variant="pi"
|
||||||
|
fontWeight="bold"
|
||||||
|
textColor={hasError && !isOpen ? 'danger600' : 'neutral500'}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
</Flex>
|
||||||
|
</StyledButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const StyledAddIcon = styled(PlusCircle)`
|
const StyledAddIcon = styled(PlusCircle)`
|
||||||
|
height: ${({ theme }) => theme.spaces[6]};
|
||||||
|
width: ${({ theme }) => theme.spaces[6]};
|
||||||
transform: ${({ $isOpen }) => ($isOpen ? 'rotate(45deg)' : 'rotate(0deg)')};
|
transform: ${({ $isOpen }) => ($isOpen ? 'rotate(45deg)' : 'rotate(0deg)')};
|
||||||
> circle {
|
> circle {
|
||||||
fill: ${({ theme, $hasError }) =>
|
fill: ${({ theme, $hasError }) =>
|
||||||
@ -28,25 +55,11 @@ const StyledAddIcon = styled(PlusCircle)`
|
|||||||
const StyledButton = styled(BaseButton)`
|
const StyledButton = styled(BaseButton)`
|
||||||
border-radius: 26px;
|
border-radius: 26px;
|
||||||
border-color: ${({ theme }) => theme.colors.neutral150};
|
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};
|
box-shadow: ${({ theme }) => theme.shadows.filterShadow};
|
||||||
|
|
||||||
svg {
|
|
||||||
height: ${({ theme }) => theme.spaces[6]};
|
|
||||||
width: ${({ theme }) => theme.spaces[6]};
|
|
||||||
> path {
|
|
||||||
fill: ${({ theme }) => theme.colors.neutral600};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${({ theme }) => theme.colors.primary600} !important;
|
|
||||||
${Typography} {
|
${Typography} {
|
||||||
color: ${({ theme }) => theme.colors.primary600} !important;
|
color: ${({ theme }) => theme.colors.primary600};
|
||||||
}
|
}
|
||||||
|
|
||||||
${StyledAddIcon} {
|
${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 (
|
|
||||||
<Flex justifyContent="center">
|
|
||||||
<Box style={{ cursor: isDisabled ? 'not-allowed' : 'pointer' }}>
|
|
||||||
<StyledButton type="button" onClick={onClick} disabled={isDisabled} hasError={hasError}>
|
|
||||||
<Flex>
|
|
||||||
<BoxFullHeight aria-hidden paddingRight={2}>
|
|
||||||
<StyledAddIcon $isOpen={isOpen} $hasError={hasError && !isOpen} />
|
|
||||||
</BoxFullHeight>
|
|
||||||
<Typography
|
|
||||||
variant="pi"
|
|
||||||
fontWeight="bold"
|
|
||||||
textColor={hasError && !isOpen ? 'danger600' : 'neutral500'}
|
|
||||||
>
|
|
||||||
{buttonLabel}
|
|
||||||
</Typography>
|
|
||||||
</Flex>
|
|
||||||
</StyledButton>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AddComponentButton.defaultProps = {
|
AddComponentButton.defaultProps = {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
hasMaxError: false,
|
|
||||||
hasMinError: false,
|
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
label: '',
|
|
||||||
missingComponentNumber: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AddComponentButton.propTypes = {
|
AddComponentButton.propTypes = {
|
||||||
label: PropTypes.string,
|
children: PropTypes.node.isRequired,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
hasMaxError: PropTypes.bool,
|
|
||||||
hasMinError: PropTypes.bool,
|
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
missingComponentNumber: PropTypes.number,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddComponentButton;
|
|
||||||
|
|||||||
@ -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 (
|
|
||||||
<ComponentBox as="button" type="button" onClick={onClick} hasRadius>
|
|
||||||
<Flex direction="column" gap={1} alignItems="center" justifyContent="center">
|
|
||||||
<ComponentIcon />
|
|
||||||
|
|
||||||
<Typography variant="pi" fontWeight="bold" textColor="neutral600">
|
|
||||||
{children}
|
|
||||||
</Typography>
|
|
||||||
</Flex>
|
|
||||||
</ComponentBox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ComponentCard.defaultProps = {
|
|
||||||
onClick() {},
|
|
||||||
};
|
|
||||||
|
|
||||||
ComponentCard.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
@ -1,18 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 styled from 'styled-components';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import ComponentCard from './ComponentCard';
|
import { ComponentIcon } from '../../ComponentIcon';
|
||||||
|
|
||||||
const Grid = styled.div`
|
export const ComponentCategory = ({
|
||||||
display: grid;
|
category,
|
||||||
grid-template-columns: repeat(auto-fit, ${140 / 16}rem);
|
components,
|
||||||
grid-gap: ${({ theme }) => theme.spaces[1]};
|
variant,
|
||||||
`;
|
isOpen,
|
||||||
|
onAddComponent,
|
||||||
const ComponentCategory = ({ category, components, variant, isOpen, onAddComponent, onToggle }) => {
|
onToggle,
|
||||||
|
}) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
@ -30,9 +39,26 @@ const ComponentCategory = ({ category, components, variant, isOpen, onAddCompone
|
|||||||
<Box paddingTop={4} paddingBottom={4} paddingLeft={3} paddingRight={3}>
|
<Box paddingTop={4} paddingBottom={4} paddingLeft={3} paddingRight={3}>
|
||||||
<Grid>
|
<Grid>
|
||||||
{components.map(({ componentUid, info: { displayName } }) => (
|
{components.map(({ componentUid, info: { displayName } }) => (
|
||||||
<ComponentCard key={componentUid} onClick={onAddComponent(componentUid)}>
|
<ComponentBox
|
||||||
{formatMessage({ id: displayName, defaultMessage: displayName })}
|
key={componentUid}
|
||||||
</ComponentCard>
|
as="button"
|
||||||
|
type="button"
|
||||||
|
background="neutral100"
|
||||||
|
justifyContent="center"
|
||||||
|
onClick={onAddComponent(componentUid)}
|
||||||
|
hasRadius
|
||||||
|
height={pxToRem(84)}
|
||||||
|
shrink={0}
|
||||||
|
borderColor="neutral200"
|
||||||
|
>
|
||||||
|
<Flex direction="column" gap={1} alignItems="center" justifyContent="center">
|
||||||
|
<ComponentIcon />
|
||||||
|
|
||||||
|
<Typography variant="pi" fontWeight="bold" textColor="neutral600">
|
||||||
|
{formatMessage({ id: displayName, defaultMessage: displayName })}
|
||||||
|
</Typography>
|
||||||
|
</Flex>
|
||||||
|
</ComponentBox>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@ -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 = {
|
ComponentCategory.defaultProps = {
|
||||||
components: [],
|
components: [],
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
@ -55,5 +105,3 @@ ComponentCategory.propTypes = {
|
|||||||
onToggle: PropTypes.func.isRequired,
|
onToggle: PropTypes.func.isRequired,
|
||||||
variant: PropTypes.oneOf(['primary', 'secondary']),
|
variant: PropTypes.oneOf(['primary', 'secondary']),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ComponentCategory;
|
|
||||||
|
|||||||
@ -1,40 +1,24 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import groupBy from 'lodash/groupBy';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { KeyboardNavigable, Box, Flex, Typography } from '@strapi/design-system';
|
import { KeyboardNavigable, Box, Flex, Typography } from '@strapi/design-system';
|
||||||
|
|
||||||
import { getTrad } from '../../../utils';
|
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 { formatMessage } = useIntl();
|
||||||
const { getComponentLayout } = useContentTypeLayout();
|
|
||||||
const [categoryToOpen, setCategoryToOpen] = useState('');
|
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(() => {
|
useEffect(() => {
|
||||||
if (isOpen && dynamicComponentCategories.length > 0) {
|
const categoryKeys = Object.keys(dynamicComponentsByCategory);
|
||||||
setCategoryToOpen(dynamicComponentCategories[0].category);
|
|
||||||
|
if (isOpen && categoryKeys.length > 0) {
|
||||||
|
setCategoryToOpen(categoryKeys[0]);
|
||||||
}
|
}
|
||||||
}, [isOpen, dynamicComponentCategories]);
|
}, [isOpen, dynamicComponentsByCategory]);
|
||||||
|
|
||||||
const handleAddComponentToDz = (componentUid) => () => {
|
const handleAddComponentToDz = (componentUid) => () => {
|
||||||
onClickAddComponent(componentUid);
|
onClickAddComponent(componentUid);
|
||||||
@ -53,54 +37,57 @@ const ComponentPicker = ({ components, isOpen, onClickAddComponent }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box paddingBottom={6}>
|
<Box
|
||||||
<Box
|
paddingTop={6}
|
||||||
paddingTop={6}
|
paddingBottom={6}
|
||||||
paddingBottom={6}
|
paddingLeft={5}
|
||||||
paddingLeft={5}
|
paddingRight={5}
|
||||||
paddingRight={5}
|
background="neutral0"
|
||||||
background="neutral0"
|
shadow="tableShadow"
|
||||||
shadow="tableShadow"
|
borderColor="neutral150"
|
||||||
borderColor="neutral150"
|
hasRadius
|
||||||
hasRadius
|
>
|
||||||
>
|
<Flex justifyContent="center">
|
||||||
<Flex justifyContent="center">
|
<Typography fontWeight="bold" textColor="neutral600">
|
||||||
<Typography fontWeight="bold" textColor="neutral600">
|
{formatMessage({
|
||||||
{formatMessage({
|
id: getTrad('components.DynamicZone.ComponentPicker-label'),
|
||||||
id: getTrad('components.DynamicZone.ComponentPicker-label'),
|
defaultMessage: 'Pick one component',
|
||||||
defaultMessage: 'Pick one component',
|
})}
|
||||||
})}
|
</Typography>
|
||||||
</Typography>
|
</Flex>
|
||||||
</Flex>
|
<Box paddingTop={2}>
|
||||||
<Box paddingTop={2}>
|
<KeyboardNavigable attributeName="data-strapi-accordion-toggle">
|
||||||
<KeyboardNavigable attributeName="data-strapi-accordion-toggle">
|
{Object.entries(dynamicComponentsByCategory).map(([category, components], index) => (
|
||||||
{dynamicComponentCategories.map(({ category, components }, index) => (
|
<ComponentCategory
|
||||||
<ComponentCategory
|
key={category}
|
||||||
key={category}
|
category={category}
|
||||||
category={category}
|
components={components}
|
||||||
components={components}
|
onAddComponent={handleAddComponentToDz}
|
||||||
onAddComponent={handleAddComponentToDz}
|
isOpen={category === categoryToOpen}
|
||||||
isOpen={category === categoryToOpen}
|
onToggle={handleClickToggle}
|
||||||
onToggle={handleClickToggle}
|
variant={index % 2 === 1 ? 'primary' : 'secondary'}
|
||||||
variant={index % 2 === 1 ? 'primary' : 'secondary'}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
</KeyboardNavigable>
|
||||||
</KeyboardNavigable>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ComponentPicker.defaultProps = {
|
ComponentPicker.defaultProps = {
|
||||||
components: [],
|
dynamicComponentsByCategory: {},
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
ComponentPicker.propTypes = {
|
ComponentPicker.propTypes = {
|
||||||
components: PropTypes.array,
|
dynamicComponentsByCategory: PropTypes.shape({
|
||||||
|
components: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
componentUid: PropTypes.string.isRequired,
|
||||||
|
info: PropTypes.object,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
onClickAddComponent: PropTypes.func.isRequired,
|
onClickAddComponent: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ComponentPicker;
|
|
||||||
|
|||||||
@ -12,67 +12,18 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
|
VisuallyHidden,
|
||||||
} from '@strapi/design-system';
|
} from '@strapi/design-system';
|
||||||
|
import { Menu, MenuItem } from '@strapi/design-system/v2';
|
||||||
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
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 { useContentTypeLayout, useDragAndDrop } from '../../../hooks';
|
||||||
import { composeRefs, getTrad, ItemTypes } from '../../../utils';
|
import { composeRefs, getTrad, ItemTypes } from '../../../utils';
|
||||||
|
|
||||||
import FieldComponent from '../../FieldComponent';
|
import FieldComponent from '../../FieldComponent';
|
||||||
|
|
||||||
const ActionsFlex = styled(Flex)`
|
export const DynamicComponent = ({
|
||||||
/*
|
|
||||||
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 = ({
|
|
||||||
componentUid,
|
componentUid,
|
||||||
formErrors,
|
formErrors,
|
||||||
index,
|
index,
|
||||||
@ -83,6 +34,8 @@ const DynamicZoneComponent = ({
|
|||||||
onGrabItem,
|
onGrabItem,
|
||||||
onDropItem,
|
onDropItem,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
dynamicComponentsByCategory,
|
||||||
|
onAddComponent,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@ -180,11 +133,70 @@ const DynamicZoneComponent = ({
|
|||||||
>
|
>
|
||||||
<Drag />
|
<Drag />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger size="S" endIcon={undefined} paddingLeft={2} paddingRight={2}>
|
||||||
|
<More aria-hidden focusable={false} />
|
||||||
|
<VisuallyHidden as="span">
|
||||||
|
{formatMessage({
|
||||||
|
id: getTrad('components.DynamicZone.more-actions'),
|
||||||
|
defaultMessage: 'More actions',
|
||||||
|
})}
|
||||||
|
</VisuallyHidden>
|
||||||
|
</Menu.Trigger>
|
||||||
|
<Menu.Content>
|
||||||
|
<Menu.SubRoot>
|
||||||
|
<Menu.SubTrigger>
|
||||||
|
{formatMessage({
|
||||||
|
id: getTrad('components.DynamicZone.add-item-above'),
|
||||||
|
defaultMessage: 'Add component above',
|
||||||
|
})}
|
||||||
|
</Menu.SubTrigger>
|
||||||
|
<Menu.SubContent>
|
||||||
|
{Object.entries(dynamicComponentsByCategory).map(([category, components]) => (
|
||||||
|
<React.Fragment key={category}>
|
||||||
|
<Menu.Label>{category}</Menu.Label>
|
||||||
|
{components.map(({ componentUid, info: { displayName } }) => (
|
||||||
|
<MenuItem
|
||||||
|
key={componentUid}
|
||||||
|
onSelect={() => onAddComponent(componentUid, index)}
|
||||||
|
>
|
||||||
|
{displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Menu.SubContent>
|
||||||
|
</Menu.SubRoot>
|
||||||
|
<Menu.SubRoot>
|
||||||
|
<Menu.SubTrigger>
|
||||||
|
{formatMessage({
|
||||||
|
id: getTrad('components.DynamicZone.add-item-below'),
|
||||||
|
defaultMessage: 'Add component below',
|
||||||
|
})}
|
||||||
|
</Menu.SubTrigger>
|
||||||
|
<Menu.SubContent>
|
||||||
|
{Object.entries(dynamicComponentsByCategory).map(([category, components]) => (
|
||||||
|
<React.Fragment key={category}>
|
||||||
|
<Menu.Label>{category}</Menu.Label>
|
||||||
|
{components.map(({ componentUid, info: { displayName } }) => (
|
||||||
|
<MenuItem
|
||||||
|
key={componentUid}
|
||||||
|
onSelect={() => onAddComponent(componentUid, index + 1)}
|
||||||
|
>
|
||||||
|
{displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Menu.SubContent>
|
||||||
|
</Menu.SubRoot>
|
||||||
|
</Menu.Content>
|
||||||
|
</Menu.Root>
|
||||||
</ActionsFlex>
|
</ActionsFlex>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComponentContainer as="li">
|
<ComponentContainer as="li" width="100%">
|
||||||
<Flex justifyContent="center">
|
<Flex justifyContent="center">
|
||||||
<Rectangle background="neutral200" />
|
<Rectangle background="neutral200" />
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -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: {},
|
formErrors: {},
|
||||||
index: 0,
|
index: 0,
|
||||||
isFieldAllowed: true,
|
isFieldAllowed: true,
|
||||||
|
onAddComponent: undefined,
|
||||||
onGrabItem: undefined,
|
onGrabItem: undefined,
|
||||||
onDropItem: undefined,
|
onDropItem: undefined,
|
||||||
onCancel: undefined,
|
onCancel: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
DynamicZoneComponent.propTypes = {
|
DynamicComponent.propTypes = {
|
||||||
componentUid: PropTypes.string.isRequired,
|
componentUid: PropTypes.string.isRequired,
|
||||||
|
dynamicComponentsByCategory: PropTypes.shape({
|
||||||
|
components: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
componentUid: PropTypes.string.isRequired,
|
||||||
|
info: PropTypes.object,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
formErrors: PropTypes.object,
|
formErrors: PropTypes.object,
|
||||||
index: PropTypes.number,
|
index: PropTypes.number,
|
||||||
isFieldAllowed: PropTypes.bool,
|
isFieldAllowed: PropTypes.bool,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
onAddComponent: PropTypes.func,
|
||||||
onGrabItem: PropTypes.func,
|
onGrabItem: PropTypes.func,
|
||||||
onDropItem: PropTypes.func,
|
onDropItem: PropTypes.func,
|
||||||
onCancel: PropTypes.func,
|
onCancel: PropTypes.func,
|
||||||
onMoveComponent: PropTypes.func.isRequired,
|
onMoveComponent: PropTypes.func.isRequired,
|
||||||
onRemoveComponentClick: PropTypes.func.isRequired,
|
onRemoveComponentClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DynamicZoneComponent;
|
|
||||||
|
|||||||
@ -7,15 +7,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import styled from 'styled-components';
|
|
||||||
import { pxToRem } from '@strapi/helper-plugin';
|
import { pxToRem } from '@strapi/helper-plugin';
|
||||||
import { Box, Flex, Typography } from '@strapi/design-system';
|
import { Box, Flex, Typography } from '@strapi/design-system';
|
||||||
|
|
||||||
const StyledBox = styled(Box)`
|
export const DynamicZoneLabel = ({
|
||||||
border-radius: ${pxToRem(26)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DynamicZoneLabel = ({
|
|
||||||
label,
|
label,
|
||||||
labelAction,
|
labelAction,
|
||||||
name,
|
name,
|
||||||
@ -28,36 +23,35 @@ const DynamicZoneLabel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justifyContent="center">
|
<Flex justifyContent="center">
|
||||||
<Box>
|
<Box
|
||||||
<StyledBox
|
paddingTop={3}
|
||||||
paddingTop={3}
|
paddingBottom={3}
|
||||||
paddingBottom={3}
|
paddingRight={4}
|
||||||
paddingRight={4}
|
paddingLeft={4}
|
||||||
paddingLeft={4}
|
borderRadius={26}
|
||||||
background="neutral0"
|
background="neutral0"
|
||||||
shadow="filterShadow"
|
shadow="filterShadow"
|
||||||
color="neutral500"
|
color="neutral500"
|
||||||
>
|
>
|
||||||
<Flex direction="column" justifyContent="center">
|
<Flex direction="column" justifyContent="center">
|
||||||
<Flex maxWidth={pxToRem(356)}>
|
<Flex maxWidth={pxToRem(356)}>
|
||||||
<Typography variant="pi" textColor="neutral600" fontWeight="bold" ellipsis>
|
<Typography variant="pi" textColor="neutral600" fontWeight="bold" ellipsis>
|
||||||
{intlLabel}
|
{intlLabel}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="pi" textColor="neutral600" fontWeight="bold">
|
<Typography variant="pi" textColor="neutral600" fontWeight="bold">
|
||||||
({numberOfComponents})
|
({numberOfComponents})
|
||||||
</Typography>
|
</Typography>
|
||||||
{required && <Typography textColor="danger600">*</Typography>}
|
{required && <Typography textColor="danger600">*</Typography>}
|
||||||
{labelAction && <Box paddingLeft={1}>{labelAction}</Box>}
|
{labelAction && <Box paddingLeft={1}>{labelAction}</Box>}
|
||||||
</Flex>
|
|
||||||
{intlDescription && (
|
|
||||||
<Box paddingTop={1} maxWidth={pxToRem(356)}>
|
|
||||||
<Typography variant="pi" textColor="neutral600" ellipsis>
|
|
||||||
{formatMessage(intlDescription)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</StyledBox>
|
{intlDescription && (
|
||||||
|
<Box paddingTop={1} maxWidth={pxToRem(356)}>
|
||||||
|
<Typography variant="pi" textColor="neutral600" ellipsis>
|
||||||
|
{formatMessage(intlDescription)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@ -82,5 +76,3 @@ DynamicZoneLabel.propTypes = {
|
|||||||
numberOfComponents: PropTypes.number,
|
numberOfComponents: PropTypes.number,
|
||||||
required: PropTypes.bool,
|
required: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DynamicZoneLabel;
|
|
||||||
|
|||||||
@ -1,66 +1,54 @@
|
|||||||
import React from 'react';
|
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 { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import AddComponentButton from '../AddComponentButton';
|
import { AddComponentButton } from '../AddComponentButton';
|
||||||
|
|
||||||
describe('<AddComponentButton />', () => {
|
describe('<AddComponentButton />', () => {
|
||||||
const setup = (props) =>
|
const render = (props) => ({
|
||||||
render(
|
...renderRTL(
|
||||||
<ThemeProvider theme={lightTheme}>
|
<AddComponentButton onClick={jest.fn()} {...props}>
|
||||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
test
|
||||||
<AddComponentButton label="test" name="name" onClick={jest.fn()} {...props} />
|
</AddComponentButton>,
|
||||||
</IntlProvider>
|
{
|
||||||
</ThemeProvider>
|
wrapper: ({ children }) => (
|
||||||
);
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||||
|
{children}
|
||||||
|
</IntlProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
user: userEvent.setup(),
|
||||||
|
});
|
||||||
|
|
||||||
it('should render the label by default', () => {
|
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', () => {
|
it('should call the onClick handler when the button is clicked', async () => {
|
||||||
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', () => {
|
|
||||||
const onClick = jest.fn();
|
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();
|
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();
|
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();
|
expect(onClick).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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(
|
|
||||||
<ThemeProvider theme={lightTheme}>
|
|
||||||
<ComponentCard {...props}>test</ComponentCard>
|
|
||||||
<GlobalStyle />
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
it('should call the onClick handler when passed', () => {
|
|
||||||
const onClick = jest.fn();
|
|
||||||
const { getByText } = setup({ onClick });
|
|
||||||
fireEvent.click(getByText('test'));
|
|
||||||
expect(onClick).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,27 +1,35 @@
|
|||||||
import React from 'react';
|
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 { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import ComponentCategory from '../ComponentCategory';
|
import { ComponentCategory } from '../ComponentCategory';
|
||||||
|
|
||||||
describe('ComponentCategory', () => {
|
describe('ComponentCategory', () => {
|
||||||
const setup = (props) =>
|
const render = (props) => ({
|
||||||
render(
|
...renderRTL(
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ComponentCategory
|
||||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
onAddComponent={jest.fn()}
|
||||||
<ComponentCategory
|
onToggle={jest.fn()}
|
||||||
onAddComponent={jest.fn()}
|
category="testing"
|
||||||
onToggle={jest.fn()}
|
{...props}
|
||||||
category="testing"
|
/>,
|
||||||
{...props}
|
{
|
||||||
/>
|
wrapper: ({ children }) => (
|
||||||
</IntlProvider>
|
<ThemeProvider theme={lightTheme}>
|
||||||
</ThemeProvider>
|
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||||
);
|
{children}
|
||||||
|
</IntlProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
user: userEvent.setup(),
|
||||||
|
});
|
||||||
|
|
||||||
it('should render my array of components when passed and the accordion is open', () => {
|
it('should render my array of components when passed and the accordion is open', () => {
|
||||||
setup({
|
const { getByRole } = render({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
components: [
|
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', () => {
|
it('should render the category as the accordion buttons label', () => {
|
||||||
setup({
|
const { getByText } = render({
|
||||||
category: 'myCategory',
|
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();
|
const onToggle = jest.fn();
|
||||||
setup({
|
const { getByRole, user } = render({
|
||||||
onToggle,
|
onToggle,
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(/testing/));
|
await user.click(getByRole('button', { name: /testing/ }));
|
||||||
|
|
||||||
expect(onToggle).toHaveBeenCalledWith('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();
|
const onAddComponent = jest.fn();
|
||||||
setup({
|
const { getByRole, user } = render({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
onAddComponent,
|
onAddComponent,
|
||||||
components: [
|
components: [
|
||||||
@ -72,7 +80,7 @@ describe('ComponentCategory', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(/myComponent/));
|
await user.click(getByRole('button', { name: /myComponent/ }));
|
||||||
|
|
||||||
expect(onAddComponent).toHaveBeenCalledWith('test');
|
expect(onAddComponent).toHaveBeenCalledWith('test');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,70 +1,73 @@
|
|||||||
import React from 'react';
|
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 { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import ComponentPicker from '../ComponentPicker';
|
import { ComponentPicker } from '../ComponentPicker';
|
||||||
|
|
||||||
import { layoutData } from './fixtures';
|
import { dynamicComponentsByCategory } from './fixtures';
|
||||||
|
|
||||||
jest.mock('../../../../hooks', () => ({
|
|
||||||
useContentTypeLayout: jest.fn().mockReturnValue({
|
|
||||||
getComponentLayout: jest.fn().mockImplementation((componentUid) => layoutData[componentUid]),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ComponentPicker', () => {
|
describe('ComponentPicker', () => {
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const Component = (props) => (
|
const Component = (props) => (
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ComponentPicker
|
||||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
isOpen
|
||||||
<ComponentPicker isOpen onClickAddComponent={jest.fn()} {...props} />
|
onClickAddComponent={jest.fn()}
|
||||||
</IntlProvider>
|
dynamicComponentsByCategory={dynamicComponentsByCategory}
|
||||||
</ThemeProvider>
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const setup = (props) => render(<Component {...props} />);
|
const render = (props) => ({
|
||||||
|
...renderRTL(<Component {...props} />, {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||||
|
{children}
|
||||||
|
</IntlProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
user: userEvent.setup(),
|
||||||
|
});
|
||||||
|
|
||||||
it('should by default give me the instruction to Pick one Component', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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,
|
isOpen: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
rerender(<Component isOpen components={['component1', 'component2', 'component3']} />);
|
rerender(<Component isOpen components={['component1', 'component2', 'component3']} />);
|
||||||
|
|
||||||
expect(screen.getByText(/component1/)).toBeInTheDocument();
|
expect(getByRole('button', { name: /component1/ })).toBeInTheDocument();
|
||||||
expect(screen.queryByText(/component3/)).not.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();
|
const onClickAddComponent = jest.fn();
|
||||||
setup({
|
const { user, getByRole } = render({
|
||||||
components: ['component1', 'component2'],
|
components: ['component1', 'component2'],
|
||||||
onClickAddComponent,
|
onClickAddComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(/component1/));
|
await user.click(getByRole('button', { name: /component1/ }));
|
||||||
|
|
||||||
expect(onClickAddComponent).toHaveBeenCalledWith('component1');
|
expect(onClickAddComponent).toHaveBeenCalledWith('component1');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
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 { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
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.mock('../../../../hooks', () => ({
|
||||||
...jest.requireActual('../../../../hooks'),
|
...jest.requireActual('../../../../hooks'),
|
||||||
@ -42,65 +43,72 @@ describe('DynamicComponent', () => {
|
|||||||
|
|
||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
const TestComponent = ({ testingDnd, ...restProps }) => (
|
const TestComponent = ({ testingDnd, ...restProps }) => (
|
||||||
<ThemeProvider theme={lightTheme}>
|
<>
|
||||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
<DynamicComponent {...defaultProps} {...restProps} />
|
||||||
<DndProvider backend={HTML5Backend}>
|
{testingDnd ? <DynamicComponent {...defaultProps} {...restProps} /> : null}
|
||||||
<DynamicComponent {...defaultProps} {...restProps} />
|
</>
|
||||||
{testingDnd ? <DynamicComponent {...defaultProps} {...restProps} /> : null}
|
|
||||||
</DndProvider>
|
|
||||||
</IntlProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setup = (props) => render(<TestComponent {...props} />);
|
const render = (props) => ({
|
||||||
|
...renderRTL(<TestComponent {...props} />, {
|
||||||
it('should by default render the name of the component in the accordion trigger', () => {
|
wrapper: ({ children }) => (
|
||||||
setup();
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||||
expect(screen.getByRole('button', { name: 'component1' })).toBeInTheDocument();
|
<DndProvider backend={HTML5Backend}>{children}</DndProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
user: userEvent.setup(),
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow removal of the component & call the onRemoveComponentClick callback when the field isAllowed', () => {
|
it('should by default render the name of the component in the accordion trigger', () => {
|
||||||
const onRemoveComponentClick = jest.fn();
|
const { getByRole } = render();
|
||||||
setup({ isFieldAllowed: true, onRemoveComponentClick });
|
|
||||||
|
|
||||||
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();
|
expect(onRemoveComponentClick).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show you the delete component button if isFieldAllowed is false', () => {
|
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', () => {
|
it('should hide the field component when you close the accordion', async () => {
|
||||||
setup();
|
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', () => {
|
describe('Keyboard drag and drop', () => {
|
||||||
it('should not move with arrow keys if the button is not pressed first', () => {
|
it('should not move with arrow keys if the button is not pressed first', () => {
|
||||||
const onMoveComponent = jest.fn();
|
const onMoveComponent = jest.fn();
|
||||||
setup({
|
const { getAllByText } = render({
|
||||||
onMoveComponent,
|
onMoveComponent,
|
||||||
testingDnd: true,
|
testingDnd: true,
|
||||||
});
|
});
|
||||||
const [draggedItem] = screen.getAllByText('Drag');
|
const [draggedItem] = getAllByText('Drag');
|
||||||
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
|
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
|
||||||
expect(onMoveComponent).not.toBeCalled();
|
expect(onMoveComponent).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move with the arrow keys if the button has been activated first', () => {
|
it('should move with the arrow keys if the button has been activated first', () => {
|
||||||
const onMoveComponent = jest.fn();
|
const onMoveComponent = jest.fn();
|
||||||
setup({ onMoveComponent, testingDnd: true });
|
const { getAllByText } = render({ onMoveComponent, testingDnd: true });
|
||||||
const [draggedItem] = screen.getAllByText('Drag');
|
const [draggedItem] = getAllByText('Drag');
|
||||||
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
|
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
|
||||||
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
|
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
|
||||||
expect(onMoveComponent).toBeCalledWith(1, 0);
|
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', () => {
|
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();
|
const onMoveComponent = jest.fn();
|
||||||
setup({ onMoveComponent, testingDnd: true });
|
const { getAllByText } = render({ onMoveComponent, testingDnd: true });
|
||||||
const [draggedItem] = screen.getAllByText('Drag');
|
const [draggedItem] = getAllByText('Drag');
|
||||||
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
|
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
|
||||||
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
|
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
|
||||||
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
|
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', () => {
|
it('should exit drag and drop mode when the escape key is pressed', () => {
|
||||||
const onMoveComponent = jest.fn();
|
const onMoveComponent = jest.fn();
|
||||||
setup({ onMoveComponent, testingDnd: true });
|
const { getAllByText } = render({ onMoveComponent, testingDnd: true });
|
||||||
const [draggedItem] = screen.getAllByText('Drag');
|
const [draggedItem] = getAllByText('Drag');
|
||||||
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
|
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
|
||||||
fireEvent.keyDown(draggedItem, { key: 'Escape', code: 'Escape' });
|
fireEvent.keyDown(draggedItem, { key: 'Escape', code: 'Escape' });
|
||||||
fireEvent.keyDown(draggedItem, { key: 'ArrowUp', code: 'ArrowUp' });
|
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');
|
it.todo('should handle errors in the fields');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IntlProvider } from 'react-intl';
|
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 { ThemeProvider, lightTheme, Tooltip } from '@strapi/design-system';
|
||||||
import { Earth } from '@strapi/icons';
|
import { Earth } from '@strapi/icons';
|
||||||
|
|
||||||
import DynamicZoneLabel from '../DynamicZoneLabel';
|
import { DynamicZoneLabel } from '../DynamicZoneLabel';
|
||||||
|
|
||||||
const LabelAction = () => {
|
const LabelAction = () => {
|
||||||
return (
|
return (
|
||||||
@ -26,50 +26,50 @@ describe('DynamicZoneLabel', () => {
|
|||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const setup = (props) => render(<Component {...props} />);
|
const render = (props) => renderRTL(<Component {...props} />);
|
||||||
|
|
||||||
it('should render the label by default', () => {
|
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', () => {
|
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', () => {
|
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(<Component numberOfComponents={2} />);
|
rerender(<Component numberOfComponents={2} />);
|
||||||
|
|
||||||
expect(screen.getByText(/2/)).toBeInTheDocument();
|
expect(getByText(/2/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render an asteriks when the required prop is true', () => {
|
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', () => {
|
it('should render the labelAction when it is provided', () => {
|
||||||
setup({ labelAction: <LabelAction /> });
|
const { getByLabelText } = render({ labelAction: <LabelAction /> });
|
||||||
|
|
||||||
expect(screen.getByLabelText(/i18n/)).toBeInTheDocument();
|
expect(getByLabelText(/i18n/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a description if passed as a prop', () => {
|
it('should render a description if passed as a prop', () => {
|
||||||
setup({
|
const { getByText } = render({
|
||||||
intlDescription: {
|
intlDescription: {
|
||||||
id: 'description',
|
id: 'description',
|
||||||
defaultMessage: 'description',
|
defaultMessage: 'description',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText(/description/)).toBeInTheDocument();
|
expect(getByText(/description/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|||||||
@ -1,76 +1,113 @@
|
|||||||
import React, { memo, useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import get from 'lodash/get';
|
|
||||||
import isEqual from 'lodash/isEqual';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Box, Flex, VisuallyHidden } from '@strapi/design-system';
|
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 { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { getTrad } from '../../utils';
|
import { getTrad } from '../../utils';
|
||||||
|
|
||||||
import connect from './utils/connect';
|
import { DynamicComponent } from './components/DynamicComponent';
|
||||||
import select from './utils/select';
|
import { AddComponentButton } from './components/AddComponentButton';
|
||||||
|
import { DynamicZoneLabel } from './components/DynamicZoneLabel';
|
||||||
import DynamicZoneComponent from './components/DynamicComponent';
|
import { ComponentPicker } from './components/ComponentPicker';
|
||||||
import AddComponentButton from './components/AddComponentButton';
|
|
||||||
import DynamicZoneLabel from './components/DynamicZoneLabel';
|
|
||||||
import ComponentPicker from './components/ComponentPicker';
|
|
||||||
|
|
||||||
import { useContentTypeLayout } from '../../hooks';
|
import { useContentTypeLayout } from '../../hooks';
|
||||||
|
|
||||||
const DynamicZone = ({
|
const DynamicZone = ({ name, labelAction, fieldSchema, metadatas }) => {
|
||||||
name,
|
// We cannot use the default props here
|
||||||
// Passed with the select function
|
const { max = Infinity, min = -Infinity, components = [], required = false } = fieldSchema;
|
||||||
addComponentToDynamicZone,
|
|
||||||
formErrors,
|
|
||||||
isCreatingEntry,
|
|
||||||
isFieldAllowed,
|
|
||||||
isFieldReadable,
|
|
||||||
labelAction,
|
|
||||||
moveComponentField,
|
|
||||||
removeComponentFromDynamicZone,
|
|
||||||
dynamicDisplayedComponents,
|
|
||||||
fieldSchema,
|
|
||||||
metadatas,
|
|
||||||
}) => {
|
|
||||||
const [addComponentIsOpen, setAddComponentIsOpen] = useState(false);
|
const [addComponentIsOpen, setAddComponentIsOpen] = useState(false);
|
||||||
const [liveText, setLiveText] = useState('');
|
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<string, Array<{category: string; info: unknown, attributes: Record<string, unknown>}>>}
|
||||||
|
*/
|
||||||
|
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 { formatMessage } = useIntl();
|
||||||
|
|
||||||
const toggleNotification = useNotification();
|
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 dynamicDisplayedComponentsLength = dynamicDisplayedComponents.length;
|
||||||
const intlDescription = metadatas.description
|
const intlDescription = metadatas.description
|
||||||
? { id: metadatas.description, defaultMessage: metadatas.description }
|
? { id: metadatas.description, defaultMessage: metadatas.description }
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// We cannot use the default props here
|
const dynamicZoneError = formErrors[name];
|
||||||
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 missingComponentNumber = min - dynamicDisplayedComponentsLength;
|
const missingComponentNumber = min - dynamicDisplayedComponentsLength;
|
||||||
const hasError = dynamicZoneErrors.length > 0;
|
const hasError = !!dynamicZoneError;
|
||||||
|
|
||||||
const hasMinError =
|
const handleAddComponent = (componentUid, position) => {
|
||||||
dynamicZoneErrors.length > 0 && get(dynamicZoneErrors, [0, 'id'], '').includes('min');
|
|
||||||
|
|
||||||
const hasMaxError =
|
|
||||||
hasError && get(dynamicZoneErrors, [0, 'id'], '') === 'components.Input.error.validation.max';
|
|
||||||
|
|
||||||
const handleAddComponent = (componentUid) => {
|
|
||||||
setAddComponentIsOpen(false);
|
setAddComponentIsOpen(false);
|
||||||
|
|
||||||
const componentLayoutData = getComponentLayout(componentUid);
|
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 = () => {
|
const handleClickOpenPicker = () => {
|
||||||
@ -160,6 +197,38 @@ const DynamicZone = ({
|
|||||||
removeComponentFromDynamicZone(name, currentIndex);
|
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))) {
|
if (!isFieldAllowed && (isCreatingEntry || (!isFieldReadable && !isCreatingEntry))) {
|
||||||
return (
|
return (
|
||||||
<NotAllowedInput
|
<NotAllowedInput
|
||||||
@ -183,7 +252,7 @@ const DynamicZone = ({
|
|||||||
labelAction={labelAction}
|
labelAction={labelAction}
|
||||||
name={name}
|
name={name}
|
||||||
numberOfComponents={dynamicDisplayedComponentsLength}
|
numberOfComponents={dynamicDisplayedComponentsLength}
|
||||||
required={fieldSchema.required || false}
|
required={required}
|
||||||
/>
|
/>
|
||||||
<VisuallyHidden id={ariaDescriptionId}>
|
<VisuallyHidden id={ariaDescriptionId}>
|
||||||
{formatMessage({
|
{formatMessage({
|
||||||
@ -194,7 +263,7 @@ const DynamicZone = ({
|
|||||||
<VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>
|
<VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>
|
||||||
<ol aria-describedby={ariaDescriptionId}>
|
<ol aria-describedby={ariaDescriptionId}>
|
||||||
{dynamicDisplayedComponents.map(({ componentUid, id }, index) => (
|
{dynamicDisplayedComponents.map(({ componentUid, id }, index) => (
|
||||||
<DynamicZoneComponent
|
<DynamicComponent
|
||||||
componentUid={componentUid}
|
componentUid={componentUid}
|
||||||
formErrors={formErrors}
|
formErrors={formErrors}
|
||||||
key={`${componentUid}-${id}`}
|
key={`${componentUid}-${id}`}
|
||||||
@ -206,26 +275,26 @@ const DynamicZone = ({
|
|||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onDropItem={handleDropItem}
|
onDropItem={handleDropItem}
|
||||||
onGrabItem={handleGrabItem}
|
onGrabItem={handleGrabItem}
|
||||||
|
onAddComponent={handleAddComponent}
|
||||||
|
dynamicComponentsByCategory={dynamicComponentsByCategory}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
<Flex justifyContent="center">
|
||||||
<AddComponentButton
|
<AddComponentButton
|
||||||
hasError={hasError}
|
hasError={hasError}
|
||||||
hasMaxError={hasMaxError}
|
isDisabled={!isFieldAllowed}
|
||||||
hasMinError={hasMinError}
|
isOpen={addComponentIsOpen}
|
||||||
isDisabled={!isFieldAllowed}
|
onClick={handleClickOpenPicker}
|
||||||
label={metadatas.label}
|
>
|
||||||
missingComponentNumber={missingComponentNumber}
|
{renderButtonLabel()}
|
||||||
isOpen={addComponentIsOpen}
|
</AddComponentButton>
|
||||||
name={name}
|
</Flex>
|
||||||
onClick={handleClickOpenPicker}
|
|
||||||
/>
|
|
||||||
<ComponentPicker
|
<ComponentPicker
|
||||||
|
dynamicComponentsByCategory={dynamicComponentsByCategory}
|
||||||
isOpen={addComponentIsOpen}
|
isOpen={addComponentIsOpen}
|
||||||
components={fieldSchema.components ?? []}
|
|
||||||
onClickAddComponent={handleAddComponent}
|
onClickAddComponent={handleAddComponent}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -233,44 +302,23 @@ const DynamicZone = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
DynamicZone.defaultProps = {
|
DynamicZone.defaultProps = {
|
||||||
dynamicDisplayedComponents: [],
|
fieldSchema: {},
|
||||||
fieldSchema: {
|
|
||||||
max: Infinity,
|
|
||||||
min: -Infinity,
|
|
||||||
},
|
|
||||||
labelAction: null,
|
labelAction: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
DynamicZone.propTypes = {
|
DynamicZone.propTypes = {
|
||||||
addComponentToDynamicZone: PropTypes.func.isRequired,
|
|
||||||
dynamicDisplayedComponents: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
componentUid: PropTypes.string.isRequired,
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
fieldSchema: PropTypes.shape({
|
fieldSchema: PropTypes.shape({
|
||||||
components: PropTypes.array.isRequired,
|
components: PropTypes.array,
|
||||||
max: PropTypes.number,
|
max: PropTypes.number,
|
||||||
min: PropTypes.number,
|
min: PropTypes.number,
|
||||||
required: PropTypes.bool,
|
required: PropTypes.bool,
|
||||||
}),
|
}),
|
||||||
formErrors: PropTypes.object.isRequired,
|
|
||||||
isCreatingEntry: PropTypes.bool.isRequired,
|
|
||||||
isFieldAllowed: PropTypes.bool.isRequired,
|
|
||||||
isFieldReadable: PropTypes.bool.isRequired,
|
|
||||||
labelAction: PropTypes.element,
|
labelAction: PropTypes.element,
|
||||||
metadatas: PropTypes.shape({
|
metadatas: PropTypes.shape({
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
moveComponentField: PropTypes.func.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
removeComponentFromDynamicZone: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Memoized = memo(DynamicZone, isEqual);
|
|
||||||
|
|
||||||
export default connect(Memoized, select);
|
|
||||||
|
|
||||||
export { DynamicZone };
|
export { DynamicZone };
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
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 { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
|
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
@ -11,9 +13,25 @@ import { layoutData } from './fixtures';
|
|||||||
|
|
||||||
const toggleNotification = jest.fn();
|
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.mock('@strapi/helper-plugin', () => ({
|
||||||
...jest.requireActual('@strapi/helper-plugin'),
|
...jest.requireActual('@strapi/helper-plugin'),
|
||||||
useCMEditViewDataManager: jest.fn().mockImplementation(() => ({ modifiedData: {} })),
|
useCMEditViewDataManager: jest.fn().mockImplementation(() => ({
|
||||||
|
...defaultCMEditViewMock,
|
||||||
|
})),
|
||||||
useNotification: jest.fn().mockImplementation(() => toggleNotification),
|
useNotification: jest.fn().mockImplementation(() => toggleNotification),
|
||||||
NotAllowedInput: () => 'This field is not allowed',
|
NotAllowedInput: () => 'This field is not allowed',
|
||||||
}));
|
}));
|
||||||
@ -38,154 +56,193 @@ describe('DynamicZone', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
addComponentToDynamicZone: jest.fn(),
|
|
||||||
isCreatingEntry: true,
|
|
||||||
isFieldAllowed: true,
|
|
||||||
isFieldReadable: true,
|
|
||||||
fieldSchema: {
|
fieldSchema: {
|
||||||
components: ['component1', 'component2', 'component3'],
|
components: ['component1', 'component2', 'component3'],
|
||||||
},
|
},
|
||||||
formErrors: {},
|
|
||||||
metadatas: {
|
metadatas: {
|
||||||
label: 'dynamic zone',
|
label: 'dynamic zone',
|
||||||
description: 'dynamic description',
|
description: 'dynamic description',
|
||||||
},
|
},
|
||||||
moveComponentField: jest.fn(),
|
|
||||||
name: 'DynamicZoneComponent',
|
name: 'DynamicZoneComponent',
|
||||||
removeComponentFromDynamicZone: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TestComponent = (props) => (
|
const TestComponent = (props) => <DynamicZone {...defaultProps} {...props} />;
|
||||||
<ThemeProvider theme={lightTheme}>
|
|
||||||
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
|
||||||
<DndProvider backend={HTML5Backend}>
|
|
||||||
<DynamicZone {...defaultProps} {...props} />
|
|
||||||
</DndProvider>
|
|
||||||
</IntlProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const setup = (props) => render(<TestComponent {...props} />);
|
const render = (props) => ({
|
||||||
|
...renderRTL(<TestComponent {...props} />, {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||||
|
<DndProvider backend={HTML5Backend}>{children}</DndProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
user: userEvent.setup(),
|
||||||
|
});
|
||||||
|
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
it('should not render the dynamic zone if there are no dynamic components to render', () => {
|
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(queryByText('dynamic zone')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('dynamic description')).not.toBeInTheDocument();
|
expect(queryByText('dynamic description')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the AddComponentButton by default and render the ComponentPicker when that button is clicked', () => {
|
it('should render the AddComponentButton by default and render the ComponentPicker when that button is clicked', async () => {
|
||||||
setup();
|
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();
|
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', () => {
|
it('should render the dynamic zone of components when there are dynamic components to render', () => {
|
||||||
setup({
|
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||||
dynamicDisplayedComponents: [
|
...defaultCMEditViewMock,
|
||||||
{ componentUid: 'component1', id: 0 },
|
modifiedData: {
|
||||||
{ componentUid: 'component2', id: 0 },
|
[TEST_NAME]: [
|
||||||
],
|
{
|
||||||
});
|
__component: 'component1',
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__component: 'component2',
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const { getByText } = render();
|
||||||
|
|
||||||
expect(screen.getByText('dynamic zone')).toBeInTheDocument();
|
expect(getByText('dynamic zone')).toBeInTheDocument();
|
||||||
expect(screen.getByText('dynamic description')).toBeInTheDocument();
|
expect(getByText('dynamic description')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByText('component1')).toBeInTheDocument();
|
expect(getByText('component1')).toBeInTheDocument();
|
||||||
expect(screen.getByText('component2')).toBeInTheDocument();
|
expect(getByText('component2')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the not allowed input if the field is not allowed & the entry is being created', () => {
|
it('should render the not allowed input if the field is not allowed & the entry is being created', () => {
|
||||||
setup({
|
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||||
isFieldAllowed: false,
|
...defaultCMEditViewMock,
|
||||||
isCreatingEntry: true,
|
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', () => {
|
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({
|
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||||
isFieldAllowed: false,
|
...defaultCMEditViewMock,
|
||||||
isCreatingEntry: false,
|
updateActionAllowedFields: [],
|
||||||
isFieldReadable: false,
|
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', () => {
|
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();
|
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', {
|
const componentPickerButton = getByRole('button', {
|
||||||
name: /component1/i,
|
name: 'component1',
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(componentPickerButton);
|
await user.click(componentPickerButton);
|
||||||
|
|
||||||
expect(addComponentToDynamicZone).toHaveBeenCalledWith(
|
expect(addComponentToDynamicZone).toHaveBeenCalledWith(
|
||||||
'DynamicZoneComponent',
|
'DynamicZoneComponent',
|
||||||
{ category: 'myComponents', info: { displayName: 'component1', icon: undefined } },
|
{ category: 'myComponents', info: { displayName: 'component1', icon: undefined } },
|
||||||
undefined,
|
expect.any(Object),
|
||||||
false
|
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();
|
const removeComponentFromDynamicZone = jest.fn();
|
||||||
|
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||||
setup({
|
...defaultCMEditViewMock,
|
||||||
removeComponentFromDynamicZone,
|
removeComponentFromDynamicZone,
|
||||||
dynamicDisplayedComponents: [
|
modifiedData: {
|
||||||
{ componentUid: 'component1', id: 0 },
|
[TEST_NAME]: [
|
||||||
{ componentUid: 'component2', id: 0 },
|
{
|
||||||
],
|
__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);
|
expect(removeComponentFromDynamicZone).toHaveBeenCalledWith('DynamicZoneComponent', 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('side effects', () => {
|
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', () => {
|
it('should call the toggleNotification callback if the amount of dynamic components has hit its max and the user tries to add another', async () => {
|
||||||
setup({
|
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||||
dynamicDisplayedComponents: [
|
...defaultCMEditViewMock,
|
||||||
{ componentUid: 'component1', id: 0 },
|
modifiedData: {
|
||||||
{ componentUid: 'component2', id: 0 },
|
[TEST_NAME]: [
|
||||||
{ componentUid: 'component3', id: 0 },
|
{
|
||||||
],
|
__component: 'component1',
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__component: 'component2',
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__component: 'component3',
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { user, getByRole } = render({
|
||||||
fieldSchema: {
|
fieldSchema: {
|
||||||
components: ['component1', 'component2', 'component3'],
|
components: ['component1', 'component2', 'component3'],
|
||||||
max: 3,
|
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({
|
expect(toggleNotification).toHaveBeenCalledWith({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -198,82 +255,188 @@ describe('DynamicZone', () => {
|
|||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
it('should have have description text', () => {
|
it('should have have description text', () => {
|
||||||
setup({
|
useCMEditViewDataManager.mockImplementationOnce(() => ({
|
||||||
dynamicDisplayedComponents: [
|
...defaultCMEditViewMock,
|
||||||
{ componentUid: 'component1', id: 0 },
|
modifiedData: {
|
||||||
{ componentUid: 'component2', id: 0 },
|
[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 () => {
|
it('should update the live text when an item has been grabbed', async () => {
|
||||||
setup({
|
useCMEditViewDataManager.mockImplementation(() => ({
|
||||||
dynamicDisplayedComponents: [
|
...defaultCMEditViewMock,
|
||||||
{ componentUid: 'component1', id: 0 },
|
modifiedData: {
|
||||||
{ componentUid: 'component2', id: 0 },
|
[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(
|
expect(
|
||||||
screen.queryByText(
|
queryByText(
|
||||||
/Press up and down arrow to change position, Spacebar to drop, Escape to cancel/
|
/Press up and down arrow to change position, Spacebar to drop, Escape to cancel/
|
||||||
)
|
)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the live text when an item has been moved', () => {
|
it('should change the live text when an item has been moved', async () => {
|
||||||
setup({
|
useCMEditViewDataManager.mockImplementation(() => ({
|
||||||
dynamicDisplayedComponents: [
|
...defaultCMEditViewMock,
|
||||||
{ componentUid: 'component1', id: 0 },
|
modifiedData: {
|
||||||
{ componentUid: 'component2', id: 0 },
|
[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' });
|
const [draggedItem] = getAllByRole('button', { name: 'Drag' });
|
||||||
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
|
|
||||||
|
|
||||||
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', () => {
|
it('should change the live text when an item has been dropped', async () => {
|
||||||
setup({
|
useCMEditViewDataManager.mockImplementation(() => ({
|
||||||
dynamicDisplayedComponents: [
|
...defaultCMEditViewMock,
|
||||||
{ componentUid: 'component1', id: 0 },
|
modifiedData: {
|
||||||
{ componentUid: 'component2', id: 0 },
|
[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' });
|
const [draggedItem] = getAllByRole('button', { name: 'Drag' });
|
||||||
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
|
|
||||||
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
|
|
||||||
|
|
||||||
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', () => {
|
it('should change the live text after the reordering interaction has been cancelled', async () => {
|
||||||
setup({
|
useCMEditViewDataManager.mockImplementation(() => ({
|
||||||
dynamicDisplayedComponents: [
|
...defaultCMEditViewMock,
|
||||||
{ componentUid: 'component1', id: 0 },
|
modifiedData: {
|
||||||
{ componentUid: 'component2', id: 0 },
|
[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' });
|
const [draggedItem] = getAllByRole('button', { name: 'Drag' });
|
||||||
fireEvent.keyDown(draggedItem, { key: 'Escape', code: 'Escape' });
|
|
||||||
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 <WrappedComponent {...props} {...selectors} />;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect;
|
|
||||||
@ -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;
|
|
||||||
@ -195,14 +195,21 @@ const EditViewDataManagerProvider = ({
|
|||||||
|
|
||||||
const dispatchAddComponent = useCallback(
|
const dispatchAddComponent = useCallback(
|
||||||
(type) =>
|
(type) =>
|
||||||
(keys, componentLayoutData, components, shouldCheckErrors = false) => {
|
(
|
||||||
|
keys,
|
||||||
|
componentLayoutData,
|
||||||
|
allComponents,
|
||||||
|
shouldCheckErrors = false,
|
||||||
|
position = undefined
|
||||||
|
) => {
|
||||||
trackUsageRef.current('didAddComponentToDynamicZone');
|
trackUsageRef.current('didAddComponentToDynamicZone');
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type,
|
type,
|
||||||
keys: keys.split('.'),
|
keys: keys.split('.'),
|
||||||
|
position,
|
||||||
componentLayoutData,
|
componentLayoutData,
|
||||||
allComponents: components,
|
allComponents,
|
||||||
shouldCheckErrors,
|
shouldCheckErrors,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,13 @@ const reducer = (state, action) =>
|
|||||||
}
|
}
|
||||||
case 'ADD_COMPONENT_TO_DYNAMIC_ZONE':
|
case 'ADD_COMPONENT_TO_DYNAMIC_ZONE':
|
||||||
case 'ADD_REPEATABLE_COMPONENT_TO_FIELD': {
|
case 'ADD_REPEATABLE_COMPONENT_TO_FIELD': {
|
||||||
const { keys, allComponents, componentLayoutData, shouldCheckErrors } = action;
|
const {
|
||||||
|
keys,
|
||||||
|
allComponents,
|
||||||
|
componentLayoutData,
|
||||||
|
shouldCheckErrors,
|
||||||
|
position = undefined,
|
||||||
|
} = action;
|
||||||
|
|
||||||
if (shouldCheckErrors) {
|
if (shouldCheckErrors) {
|
||||||
draftState.shouldCheckErrors = !state.shouldCheckErrors;
|
draftState.shouldCheckErrors = !state.shouldCheckErrors;
|
||||||
@ -62,7 +68,15 @@ const reducer = (state, action) =>
|
|||||||
draftState.modifiedDZName = keys[0];
|
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 =
|
const defaultDataStructure =
|
||||||
action.type === 'ADD_COMPONENT_TO_DYNAMIC_ZONE'
|
action.type === 'ADD_COMPONENT_TO_DYNAMIC_ZONE'
|
||||||
@ -87,11 +101,9 @@ const reducer = (state, action) =>
|
|||||||
componentLayoutData.attributes
|
componentLayoutData.attributes
|
||||||
);
|
);
|
||||||
|
|
||||||
const newValue = Array.isArray(currentValue)
|
currentValue.splice(actualPosition, 0, componentDataStructure);
|
||||||
? [...currentValue, componentDataStructure]
|
|
||||||
: [componentDataStructure];
|
|
||||||
|
|
||||||
set(draftState, ['modifiedData', ...keys], newValue);
|
set(draftState, ['modifiedData', ...keys], currentValue);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -851,6 +851,78 @@ describe('CONTENT MANAGER | COMPONENTS | EditViewDataManagerProvider | reducer',
|
|||||||
|
|
||||||
expect(reducer(state, action)).toEqual(expected);
|
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', () => {
|
describe('CONNECT_RELATION', () => {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { Pencil, Layer } from '@strapi/icons';
|
|||||||
import InformationBox from 'ee_else_ce/content-manager/pages/EditView/InformationBox';
|
import InformationBox from 'ee_else_ce/content-manager/pages/EditView/InformationBox';
|
||||||
import { InjectionZone } from '../../../shared/components';
|
import { InjectionZone } from '../../../shared/components';
|
||||||
import permissions from '../../../permissions';
|
import permissions from '../../../permissions';
|
||||||
import DynamicZone from '../../components/DynamicZone';
|
import { DynamicZone } from '../../components/DynamicZone';
|
||||||
import CollectionTypeFormWrapper from '../../components/CollectionTypeFormWrapper';
|
import CollectionTypeFormWrapper from '../../components/CollectionTypeFormWrapper';
|
||||||
import EditViewDataManagerProvider from '../../components/EditViewDataManagerProvider';
|
import EditViewDataManagerProvider from '../../components/EditViewDataManagerProvider';
|
||||||
import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper';
|
import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user