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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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