Change group to components

This commit is contained in:
soupette 2019-10-28 11:08:26 +01:00 committed by Alexandre Bodin
parent 75378bfe06
commit beb6884c4b
47 changed files with 1773 additions and 234 deletions

View File

@ -8,7 +8,8 @@
"attributes": {
"name": {
"type": "string",
"required": true
"required": true,
"default": "something"
},
"is_available": {
"type": "boolean",

View File

@ -0,0 +1,17 @@
import styled from 'styled-components';
const Flex = styled.div`
display: flex;
> button {
cursor: pointer;
padding-top: 0;
}
.trash-icon {
color: #4b515a;
}
.button-wrapper {
line-height: 35px;
}
`;
export default Flex;

View File

@ -0,0 +1,24 @@
import styled from 'styled-components';
const ImgWrapper = styled.div`
width: 19px;
height: 19px;
margin: auto;
margin-right: 19px;
border-radius: 50%;
background-color: ${({ hasErrors, isOpen }) => {
if (hasErrors) {
return '#FAA684';
} else if (isOpen) {
return '#AED4FB';
} else {
return '#F3F4F4';
}
}}
text-align: center;
line-height: 19px;
${({ isOpen }) => !isOpen && 'transform: rotate(180deg)'}
`;
export default ImgWrapper;

View File

@ -0,0 +1,87 @@
import styled, { css } from 'styled-components';
import Flex from './Flex';
const Wrapper = styled(Flex)`
height: 36px;
padding: 0 10px 0 15px;
justify-content: space-between;
border: 1px solid
${({ hasErrors, isOpen }) => {
if (hasErrors) {
return '#FFA784';
} else if (isOpen) {
return '#AED4FB';
} else {
return 'rgba(227, 233, 243, 0.75)';
}
}};
${({ doesPreviousFieldContainErrorsAndIsOpen }) => {
if (doesPreviousFieldContainErrorsAndIsOpen) {
return css`
border-top: 1px solid #ffa784;
`;
}
}}
${({ isFirst }) => {
if (isFirst) {
return css`
border-top-right-radius: 2px;
border-top-left-radius: 2px;
`;
}
}}
border-bottom: 0;
line-height: 36px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
background-color: ${({ hasErrors, isOpen }) => {
if (hasErrors && isOpen) {
return '#FFE9E0';
} else if (isOpen) {
return '#E6F0FB';
} else {
return '#ffffff';
}
}};
${({ hasErrors, isOpen }) => {
if (hasErrors) {
return css`
color: #f64d0a;
font-weight: 600;
`;
}
if (isOpen) {
return css`
color: #007eff;
font-weight: 600;
`;
}
}}
button,
i, img {
&:active,
&:focus {
outline: 0;
}
${({ isOpen }) => {
if (isOpen) {
return css`
&.trash-icon i {
color: #007eff;
}
`;
}
}}
webkit-font-smoothing: antialiased;
`;
export default Wrapper;

View File

@ -0,0 +1,131 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { get } from 'lodash';
import pluginId from '../../pluginId';
import Grab from '../../assets/images/grab_icon.svg';
import Logo from '../../assets/images/caret_top.svg';
import LogoGrey from '../../assets/images/caret_top_grey.svg';
import LogoError from '../../assets/images/caret_top_error.svg';
import GrabBlue from '../../assets/images/grab_icon_blue.svg';
import GrabError from '../../assets/images/grab_icon_error.svg';
import PreviewCarret from '../PreviewCarret';
import Flex from './Flex';
import ImgWrapper from './ImgWrapper';
import Wrapper from './Wrapper';
// eslint-disable-next-line react/display-name
const ComponentBanner = forwardRef(
(
{
doesPreviousFieldContainErrorsAndIsOpen,
hasErrors,
isFirst,
isDragging,
isOpen,
mainField,
modifiedData,
name,
onClick,
removeField,
style,
},
ref
) => {
let logo = isOpen ? Logo : LogoGrey;
let grab = isOpen ? GrabBlue : Grab;
const opacity = isDragging ? 0.5 : 1;
const displayedValue = get(
modifiedData,
[...name.split('.'), mainField],
null
);
if (hasErrors) {
grab = GrabError;
logo = LogoError;
}
return (
<div ref={ref}>
{isDragging ? (
<PreviewCarret isComponent />
) : (
<Wrapper
doesPreviousFieldContainErrorsAndIsOpen={
doesPreviousFieldContainErrorsAndIsOpen
}
hasErrors={hasErrors}
isFirst={isFirst}
isOpen={isOpen}
onClick={onClick}
style={{ opacity, ...style }}
>
<Flex>
<ImgWrapper hasErrors={hasErrors} isOpen={isOpen}>
<img src={logo} alt="logo" />
</ImgWrapper>
<FormattedMessage
id={`${pluginId}.containers.Edit.pluginHeader.title.new`}
>
{msg => {
return <span>{displayedValue || msg}</span>;
}}
</FormattedMessage>
</Flex>
<Flex className="button-wrapper">
<button
className="trash-icon"
type="button"
style={{ marginRight: 6 }}
onClick={removeField}
>
<i className="fa fa-trash" />
</button>
<button type="button">
<img
src={grab}
alt="grab icon"
style={{ verticalAlign: 'unset' }}
/>
</button>
</Flex>
</Wrapper>
)}
</div>
);
}
);
ComponentBanner.defaultProps = {
doesPreviousFieldContainErrorsAndIsOpen: false,
hasErrors: false,
isCreating: true,
isDragging: false,
isFirst: false,
isOpen: false,
mainField: 'id',
onClick: () => {},
removeField: () => {},
style: {},
};
ComponentBanner.propTypes = {
doesPreviousFieldContainErrorsAndIsOpen: PropTypes.bool,
hasErrors: PropTypes.bool,
isFirst: PropTypes.bool,
isDragging: PropTypes.bool,
isOpen: PropTypes.bool,
mainField: PropTypes.string,
modifiedData: PropTypes.object,
name: PropTypes.string,
onClick: PropTypes.func,
removeField: PropTypes.func,
style: PropTypes.object,
};
export default ComponentBanner;

View File

@ -0,0 +1,38 @@
import styled, { css } from 'styled-components';
const Button = styled.div`
width: 100%;
height: 37px;
margin-bottom: 27px;
text-align: center;
border: 1px solid rgba(227, 233, 243, 0.75);
border-top: 1px solid
${({ doesPreviousFieldContainErrorsAndIsClosed }) =>
doesPreviousFieldContainErrorsAndIsClosed
? '#FFA784'
: 'rgba(227, 233, 243, 0.75)'};
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
${({ withBorderRadius }) => {
if (withBorderRadius) {
return css`
border-radius: 2px;
`;
}
}}
color: #007eff;
font-size: 12px;
font-weight: 700;
-webkit-font-smoothing: antialiased;
line-height: 37px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.5px;
> i {
margin-right: 10px;
}
`;
export default Button;

View File

@ -0,0 +1,174 @@
import React, { Fragment, useMemo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { DragSource, DropTarget } from 'react-dnd';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { Collapse } from 'reactstrap';
import ItemTypes from '../../utils/ItemTypes';
import ComponentBanner from '../ComponentBanner';
import Form from './Form';
import FormWrapper from './FormWrapper';
function ComponentCollapse({
checkFormErrors,
connectDragSource,
connectDropTarget,
connectDragPreview,
doesPreviousFieldContainErrorsAndIsOpen,
hasErrors,
isDragging,
isFirst,
isOpen,
layout,
modifiedData,
name,
onChange,
onClick,
removeField,
}) {
const mainField = useMemo(
() => get(layout, ['settings', 'mainField'], 'id'),
[layout]
);
const fields = get(layout, ['layouts', 'edit'], []);
const ref = React.useRef(null);
connectDragSource(ref);
connectDropTarget(ref);
useEffect(() => {
connectDragPreview(getEmptyImage(), { captureDraggingState: true });
}, [connectDragPreview]);
return (
<Fragment>
<ComponentBanner
doesPreviousFieldContainErrorsAndIsOpen={
doesPreviousFieldContainErrorsAndIsOpen
}
hasErrors={hasErrors}
isFirst={isFirst}
isDragging={isDragging}
isOpen={isOpen}
modifiedData={modifiedData}
mainField={mainField}
name={name}
onClick={onClick}
ref={ref}
removeField={removeField}
/>
<Collapse isOpen={isOpen} style={{ backgroundColor: '#FAFAFB' }}>
<FormWrapper hasErrors={hasErrors} isOpen={isOpen}>
{fields.map((fieldRow, key) => {
return (
<div className="row" key={key}>
{fieldRow.map(field => {
const keys = `${name}.${field.name}`;
return (
<Form
checkFormErrors={checkFormErrors}
key={keys}
modifiedData={modifiedData}
keys={keys}
fieldName={field.name}
layout={layout}
onChange={onChange}
shouldCheckErrors={hasErrors}
/>
);
})}
</div>
);
})}
</FormWrapper>
</Collapse>
</Fragment>
);
}
ComponentCollapse.defaultProps = {
addRelation: () => {},
doesPreviousFieldContainErrorsAndIsOpen: false,
hasErrors: false,
isCreating: true,
isDragging: false,
isFirst: false,
isOpen: false,
layout: {},
move: () => {},
removeField: () => {},
};
ComponentCollapse.propTypes = {
checkFormErrors: PropTypes.func.isRequired,
connectDragPreview: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired,
doesPreviousFieldContainErrorsAndIsOpen: PropTypes.bool,
hasErrors: PropTypes.bool,
isDragging: PropTypes.bool,
isFirst: PropTypes.bool,
isOpen: PropTypes.bool,
layout: PropTypes.object,
modifiedData: PropTypes.object,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
removeField: PropTypes.func,
};
export default DropTarget(
ItemTypes.COMPONENT,
{
canDrop: () => true,
hover(props, monitor) {
const { id: draggedId } = monitor.getItem();
const { id: overId } = props;
if (draggedId !== overId) {
const { index: overIndex } = props.findField(overId);
props.move(draggedId, overIndex, props.componentName);
}
},
},
connect => ({
connectDropTarget: connect.dropTarget(),
})
)(
DragSource(
ItemTypes.COMPONENT,
{
beginDrag: props => {
props.collapseAll();
props.resetErrors();
return {
id: props.id,
mainField: get(props.layout, ['settings', 'mainField'], 'id'),
modifiedData: props.modifiedData,
name: props.name,
originalIndex: props.findField(props.id).index,
};
},
// COMMENTING ON PURPOSE NOT SURE IF WE ALLOW DROPPING OUTSIDE THE DROP TARGET
// endDrag(props, monitor) {
// const { id: droppedId, originalIndex } = monitor.getItem();
// const didDrop = monitor.didDrop();
// if (!didDrop) {
// props.move(droppedId, originalIndex, props.groupName);
// }
// },
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging(),
})
)(ComponentCollapse)
);

View File

@ -0,0 +1,13 @@
import styled from 'styled-components';
const EmptyComponent = styled.div`
height: 72px;
border: 1px solid rgba(227, 233, 243, 0.75);
border-top-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom: 0;
line-height: 73px;
text-align: center;
`;
export default EmptyComponent;

View File

@ -0,0 +1,74 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import Inputs from '../Inputs';
import SelectWrapper from '../SelectWrapper';
const Form = ({
checkFormErrors,
keys,
layout,
modifiedData,
fieldName,
onChange,
shouldCheckErrors,
}) => {
const currentField = useMemo(() => {
// We are not providing any deps to the hook in purpose
// We don't need any recalculation there since these values are not changed in the component's lifecycle
return get(layout, ['schema', 'attributes', fieldName], '');
}, []);
const currentFieldMeta = useMemo(() => {
return get(layout, ['metadatas', fieldName, 'edit'], {});
}, []);
if (currentField.type === 'relation') {
return (
<div className="col-6" key={keys}>
<SelectWrapper
{...currentFieldMeta}
name={keys}
plugin={currentField.plugin}
relationType={currentField.relationType}
targetModel={currentField.targetModel}
value={get(modifiedData, keys)}
/>
</div>
);
}
return (
<Inputs
key={keys}
layout={layout}
modifiedData={modifiedData}
keys={keys}
name={fieldName}
onBlur={shouldCheckErrors ? checkFormErrors : null}
onChange={({ target: { value } }) => {
onChange({
target: { name: keys, value },
});
}}
/>
);
};
Form.defaultProps = {
checkFormErrors: () => {},
shouldCheckErrors: false,
};
Form.propTypes = {
checkFormErrors: PropTypes.func,
fieldName: PropTypes.string.isRequired,
keys: PropTypes.string.isRequired,
layout: PropTypes.object.isRequired,
modifiedData: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
shouldCheckErrors: PropTypes.bool,
};
export default memo(Form);

View File

@ -0,0 +1,20 @@
import styled from 'styled-components';
const FormWrapper = styled.div`
padding-top: 27px;
padding-left: 20px;
padding-right: 20px;
padding-bottom: 8px;
border-top: 1px solid
${({ hasErrors, isOpen }) => {
if (hasErrors) {
return '#ffa784';
} else if (isOpen) {
return '#AED4FB';
} else {
return 'rgba(227, 233, 243, 0.75)';
}
}};
`;
export default FormWrapper;

View File

@ -0,0 +1,67 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import pluginId from '../../pluginId';
import Form from './Form';
import P from './P';
import NonRepeatableWrapper from './NonRepeatableWrapper';
const NonRepeatableComponent = ({
addField,
isInitialized,
fields,
modifiedData,
name,
layout,
onChange,
}) => {
if (!isInitialized) {
return (
<NonRepeatableWrapper isEmpty onClick={() => addField(name, false)}>
<div />
<FormattedMessage id={`${pluginId}.components.Group.empty.repeatable`}>
{msg => <P style={{ paddingTop: 75 }}>{msg}</P>}
</FormattedMessage>
</NonRepeatableWrapper>
);
}
return (
<NonRepeatableWrapper>
{fields.map((fieldRow, key) => {
return (
<div className="row" key={key}>
{fieldRow.map(field => {
const keys = `${name}.${field.name}`;
return (
<Form
key={keys}
modifiedData={modifiedData}
keys={keys}
fieldName={field.name}
layout={layout}
onChange={onChange}
/>
);
})}
</div>
);
})}
</NonRepeatableWrapper>
);
};
NonRepeatableComponent.defaultProps = {};
NonRepeatableComponent.propTypes = {
addField: PropTypes.func.isRequired,
isInitialized: PropTypes.bool,
fields: PropTypes.array,
modifiedData: PropTypes.object,
name: PropTypes.string.isRequired,
layout: PropTypes.object,
onChange: PropTypes.func,
};
export default memo(NonRepeatableComponent);

View File

@ -0,0 +1,66 @@
import styled, { css } from 'styled-components';
const NonRepeatableWrapper = styled.div`
margin: 0 15px !important;
padding: 0 20px !important;
${({ isEmpty }) => {
if (isEmpty) {
return css`
position: relative;
height: 108px;
margin-bottom: 21px !important;
background-color: #fafafb;
text-align: center;
cursor: pointer;
border-radius: 2px;
> div {
position: absolute;
top: 30px;
left: calc(50% - 18px);
height: 36px;
width: 36px;
line-height: 38px;
border-radius: 50%;
background-color: #f3f4f4;
cursor: pointer;
&:before {
content: '\f067';
font-family: FontAwesome;
font-size: 15px;
color: #b4b6ba;
}
}
border: 1px solid transparent;
&:hover {
border: 1px solid #aed4fb;
background-color: #e6f0fb;
> p {
color: #007eff;
}
> div {
background-color: #aed4fb;
&:before {
content: '\f067';
font-family: FontAwesome;
font-size: 15px;
color: #007eff;
}
}
}
`;
}
return css`
padding-top: 21px !important;
background-color: #f7f8f8;
margin-bottom: 18px !important;
`;
}}
`;
export default NonRepeatableWrapper;

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
const P = styled.p`
color: #9ea7b8;
font-size: 13px;
font-weight: 500;
`;
export default P;

View File

@ -0,0 +1,41 @@
import styled from 'styled-components';
const ResetComponent = styled.div`
position: absolute;
top: 0;
right: 15px;
display: flex;
cursor: pointer;
color: #4b515a;
> span {
margin-right: 10px;
display: none;
}
&:hover {
> div {
background-color: #faa684;
}
color: #f64d0a;
> span {
display: initial;
}
}
> div {
width: 24px;
height: 24px;
background-color: #f3f4f4;
text-align: center;
border-radius: 2px;
&:after {
content: '\f1f8';
font-size: 14px;
font-family: FontAwesome;
}
}
`;
export default ResetComponent;

View File

@ -0,0 +1,273 @@
import React, { useEffect, useReducer, memo } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { get, size } from 'lodash';
import pluginId from '../../pluginId';
import { useEditView } from '../../contexts/EditView';
import Button from './Button';
import ComponentCollapse from './ComponentCollapse';
import EmptyComponent from './EmptyComponent';
import P from './P';
import ResetComponent from './ResetComponent';
import init from './init';
import reducer, { initialState } from './reducer';
import NonRepeatableComponent from './NonRepeatableComponent';
function ComponentField({
addField,
componentErrorKeys,
moveComponentField,
componentValue,
isRepeatable,
label,
layout,
min,
max,
modifiedData,
name,
onChange,
removeField,
}) {
const {
checkFormErrors,
didCheckErrors,
errors,
resetErrors,
resetComponentData,
} = useEditView();
const fields = get(layout, ['layouts', 'edit'], []);
const [state, dispatch] = useReducer(reducer, initialState, () =>
init(initialState, componentValue)
);
const { collapses } = state.toJS();
const findField = React.useCallback(
id => {
const field = componentValue.filter(
current => current._temp__id === id
)[0];
return {
field,
index: componentValue.indexOf(field),
};
},
[componentValue]
);
const move = React.useCallback(
(id, atIndex) => {
const { index } = findField(id);
moveComponentField(index, atIndex, name);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[componentValue]
);
useEffect(() => {
const collapsesToOpen = Object.keys(errors)
.filter(errorPath => errorPath.split('.')[0] === name && isRepeatable)
.map(errorPath => errorPath.split('.')[1]);
if (collapsesToOpen.length > 0) {
dispatch({
type: 'OPEN_COLLAPSES_THAT_HAVE_ERRORS',
collapsesToOpen: collapsesToOpen.filter(
(v, index) => collapsesToOpen.indexOf(v) === index
),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [didCheckErrors]);
const componentValueLength = size(componentValue);
const isInitialized = get(modifiedData, name, null) !== null;
return (
<>
<div className="row">
<div
className="col-12"
style={{
paddingTop: 0,
marginTop: '-2px',
paddingBottom: isRepeatable ? 7 : 14,
position: 'relative',
}}
>
<span
style={{
fontSize: 13,
fontWeight: 500,
}}
>
{label}&nbsp;
{isRepeatable && `(${componentValueLength})`}
</span>
{!isRepeatable && isInitialized && (
<ResetComponent
onClick={e => {
e.preventDefault();
e.stopPropagation();
resetComponentData(name);
}}
>
<FormattedMessage id={`${pluginId}.components.Group.reset`} />
<div />
</ResetComponent>
)}
</div>
</div>
{!isRepeatable ? (
<NonRepeatableComponent
addField={addField}
isInitialized={isInitialized}
fields={fields}
modifiedData={modifiedData}
layout={layout}
name={name}
onChange={onChange}
/>
) : (
<div
style={{
margin: '0 15px',
}}
>
{componentValue.length === 0 && (
<EmptyComponent>
<FormattedMessage
id={`${pluginId}.components.Group.empty.repeatable`}
>
{msg => <P>{msg}</P>}
</FormattedMessage>
</EmptyComponent>
)}
{componentValue.map((field, index) => {
const componentFieldName = `${name}.${index}`;
const doesPreviousFieldContainErrorsAndIsOpen =
componentErrorKeys.includes(`${name}.${index - 1}`) &&
index !== 0 &&
collapses[index - 1].isOpen === false;
const hasErrors = componentErrorKeys.includes(componentFieldName);
return (
<ComponentCollapse
key={field._temp__id}
checkFormErrors={checkFormErrors}
collapseAll={() => {
dispatch({
type: 'COLLAPSE_ALL',
});
}}
doesPreviousFieldContainErrorsAndIsOpen={
doesPreviousFieldContainErrorsAndIsOpen
}
onClick={() => {
dispatch({
type: 'TOGGLE_COLLAPSE',
index,
});
}}
findField={findField}
componentName={name}
hasErrors={hasErrors}
isFirst={index === 0}
isOpen={collapses[index].isOpen}
id={field._temp__id}
layout={layout}
modifiedData={modifiedData}
move={move}
name={componentFieldName}
onChange={onChange}
removeField={e => {
e.stopPropagation();
if (componentValue.length - 1 < min) {
strapi.notification.info(
`${pluginId}.components.Group.notification.info.minimum-requirement`
);
}
const shouldAddEmptyField = componentValue.length - 1 < min;
dispatch({
type: 'REMOVE_COLLAPSE',
index,
shouldAddEmptyField,
});
removeField(`${name}.${index}`, shouldAddEmptyField);
}}
resetErrors={resetErrors}
/>
);
})}
<Button
doesPreviousFieldContainErrorsAndIsClosed={
componentValueLength > 0 &&
componentErrorKeys.includes(
`${name}.${componentValueLength - 1}`
) &&
collapses[componentValueLength - 1].isOpen === false
}
onClick={() => {
if (componentValue.length < max) {
addField(name);
dispatch({
type: 'ADD_NEW_FIELD',
});
return;
}
strapi.notification.info(
`${pluginId}.components.Group.notification.info.maximum-requirement`
);
}}
withBorderRadius={false}
>
<i className="fa fa-plus" />
<FormattedMessage
id={`${pluginId}.containers.EditView.Group.add.new`}
/>
</Button>
</div>
)}
</>
);
}
ComponentField.defaultProps = {
addRelation: () => {},
componentErrorKeys: [],
componentValue: {},
label: '',
layout: {},
max: Infinity,
min: -Infinity,
modifiedData: {},
};
ComponentField.propTypes = {
addField: PropTypes.func.isRequired,
addRelation: PropTypes.func,
componentErrorKeys: PropTypes.array,
componentValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
isRepeatable: PropTypes.bool.isRequired,
label: PropTypes.string,
layout: PropTypes.object,
max: PropTypes.number,
min: PropTypes.number,
modifiedData: PropTypes.object,
moveComponentField: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
pathname: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
removeField: PropTypes.func.isRequired,
};
export default memo(ComponentField);

View File

@ -0,0 +1,18 @@
import { fromJS } from 'immutable';
import { isArray } from 'lodash';
function init(initialState, componentValues) {
return initialState.update('collapses', list => {
if (isArray(componentValues)) {
return fromJS(
componentValues.map(() => ({
isOpen: false,
}))
);
}
return list;
});
}
export default init;

View File

@ -0,0 +1,68 @@
import { fromJS } from 'immutable';
const initialState = fromJS({ collapses: [], collapsesToOpen: [] });
function reducer(state, action) {
switch (action.type) {
case 'ADD_NEW_FIELD':
return state.update('collapses', list => {
return list
.map(obj => obj.update('isOpen', () => false))
.push(fromJS({ isOpen: true }));
});
case 'COLLAPSE_ALL':
return state
.update('collapsesToOpen', () => fromJS([]))
.update('collapses', list =>
list.map(obj => obj.update('isOpen', () => false))
);
case 'OPEN_COLLAPSES_THAT_HAVE_ERRORS':
return state
.update('collapsesToOpen', () => fromJS(action.collapsesToOpen))
.update('collapses', list => {
return list.map((obj, index) => {
if (action.collapsesToOpen.indexOf(index.toString()) !== -1) {
return obj.update('isOpen', () => true);
}
return obj.update('isOpen', () => false);
});
});
case 'TOGGLE_COLLAPSE':
return state.update('collapses', list => {
return list.map((obj, index) => {
if (index === action.index) {
return obj.update('isOpen', v => !v);
}
if (
state
.get('collapsesToOpen')
.toJS()
.indexOf(index.toString()) !== -1
) {
return obj;
}
return obj.update('isOpen', () => false);
});
});
case 'REMOVE_COLLAPSE':
return state
.removeIn(['collapses', action.index])
.update('collapses', list => list.map(obj => obj.set('isOpen', false)))
.update('collapses', list => {
if (action.shouldAddEmptyField) {
return list.push(fromJS({ isOpen: true }));
}
return list;
});
default:
return state;
}
}
export default reducer;
export { initialState };

View File

@ -3,7 +3,7 @@ import { useDragLayer } from 'react-dnd';
import ItemTypes from '../../utils/ItemTypes';
import GroupBanner from '../GroupBanner';
import ComponentBanner from '../ComponentBanner';
import RelationItem from '../SelectMany/Relation';
import { Li } from '../SelectMany/components';
import DraggedField from '../DraggedField';
@ -54,9 +54,9 @@ const CustomDragLayer = () => {
switch (itemType) {
case ItemTypes.FIELD:
return <DraggedField name={item.id} selectedItem={item.name} />;
case ItemTypes.GROUP:
case ItemTypes.COMPONENT:
return (
<GroupBanner
<ComponentBanner
{...item}
isOpen
style={{

View File

@ -18,7 +18,7 @@ const DraggedField = forwardRef(
children,
count,
goTo,
groupUid,
componentUid,
isDragging,
isDraggingSibling,
isHidden,
@ -127,7 +127,7 @@ const DraggedField = forwardRef(
</RemoveWrapper>
</SubWrapper>
)}
{type === 'group' && (
{type === 'component' && (
<FormattedMessage
id={`${pluginId}.components.FieldItem.linkToGroupLayout`}
>
@ -137,7 +137,7 @@ const DraggedField = forwardRef(
e.stopPropagation();
goTo(
`/plugins/${pluginId}/ctm-configurations/edit-settings/groups/${groupUid}`
`/plugins/${pluginId}/ctm-configurations/edit-settings/components/${componentUid}`
);
}}
>
@ -155,7 +155,7 @@ DraggedField.defaultProps = {
children: null,
count: 1,
goTo: () => {},
groupUid: null,
componentUid: null,
isDragging: false,
isDraggingSibling: false,
isHidden: false,
@ -172,7 +172,7 @@ DraggedField.propTypes = {
children: PropTypes.node,
count: PropTypes.number,
goTo: PropTypes.func,
groupUid: PropTypes.string,
componentUid: PropTypes.string,
isDragging: PropTypes.bool,
isDraggingSibling: PropTypes.bool,
isHidden: PropTypes.bool,

View File

@ -11,8 +11,8 @@ const DraggedFieldWithPreview = forwardRef(
(
{
goTo,
groupUid,
groupLayouts,
componentUid,
componentLayouts,
isDragging,
isDraggingSibling,
label,
@ -34,11 +34,18 @@ const DraggedFieldWithPreview = forwardRef(
const isFullSize = size === 12;
const display = isFullSize && dragStart ? 'none' : '';
const width = isFullSize && dragStart ? 0 : '100%';
const higherFields = ['json', 'text', 'file', 'media', 'group', 'richtext'];
const higherFields = [
'json',
'text',
'file',
'media',
'component',
'richtext',
];
const withLongerHeight = higherFields.includes(type) && !dragStart;
const groupData = get(groupLayouts, [groupUid], {});
const groupLayout = get(groupData, ['layouts', 'edit'], []);
const componentData = get(componentLayouts, [componentUid], {});
const componentLayout = get(componentData, ['layouts', 'edit'], []);
const getWrapperWitdh = colNum => `${(1 / 12) * colNum * 100}%`;
return (
@ -69,7 +76,7 @@ const DraggedFieldWithPreview = forwardRef(
<div className="sub" style={{ width, opacity }}>
<DraggedField
goTo={goTo}
groupUid={groupUid}
componentUid={componentUid}
isHidden={isHidden}
isDraggingSibling={isDraggingSibling}
label={label}
@ -82,10 +89,10 @@ const DraggedFieldWithPreview = forwardRef(
type={type}
withLongerHeight={withLongerHeight}
>
{type === 'group' &&
groupLayout.map((row, i) => {
{type === 'component' &&
componentLayout.map((row, i) => {
const marginBottom =
i === groupLayout.length - 1 ? '29px' : '';
i === componentLayout.length - 1 ? '29px' : '';
const marginTop = i === 0 ? '5px' : '';
return (
@ -99,12 +106,12 @@ const DraggedFieldWithPreview = forwardRef(
>
{row.map(field => {
const fieldType = get(
groupData,
componentData,
['schema', 'attributes', field.name, 'type'],
''
);
const label = get(
groupData,
componentData,
['metadatas', field.name, 'edit', 'label'],
''
);
@ -143,8 +150,8 @@ const DraggedFieldWithPreview = forwardRef(
DraggedFieldWithPreview.defaultProps = {
goTo: () => {},
groupLayouts: {},
groupUid: null,
componentLayouts: {},
componentUid: null,
isDragging: false,
isDraggingSibling: false,
label: '',
@ -160,8 +167,8 @@ DraggedFieldWithPreview.defaultProps = {
DraggedFieldWithPreview.propTypes = {
goTo: PropTypes.func,
groupLayouts: PropTypes.object,
groupUid: PropTypes.string,
componentLayouts: PropTypes.object,
componentUid: PropTypes.string,
isDragging: PropTypes.bool,
isDraggingSibling: PropTypes.bool,
label: PropTypes.string,

View File

@ -10,7 +10,7 @@ import DraggedFieldWithPreview from '../DraggedFieldWithPreview';
import ItemTypes from '../../utils/ItemTypes';
const Item = ({
groupUid,
componentUid,
itemIndex,
moveItem,
moveRow,
@ -22,7 +22,7 @@ const Item = ({
}) => {
const {
goTo,
groupLayouts,
componentLayouts,
metadatas,
setEditFieldToSelect,
selectedItemName,
@ -201,8 +201,8 @@ const Item = ({
return (
<DraggedFieldWithPreview
goTo={goTo}
groupUid={groupUid}
groupLayouts={groupLayouts}
componentUid={componentUid}
componentLayouts={componentLayouts}
isDragging={isDragging}
label={get(metadatas, [name, 'edit', 'label'], '')}
name={name}
@ -222,12 +222,12 @@ const Item = ({
};
Item.defaultProps = {
groupUid: '',
componentUid: '',
type: 'string',
};
Item.propTypes = {
groupUid: PropTypes.string,
componentUid: PropTypes.string,
itemIndex: PropTypes.number.isRequired,
moveItem: PropTypes.func.isRequired,
moveRow: PropTypes.func.isRequired,

View File

@ -19,9 +19,9 @@ const FieldsReorder = ({ className }) => {
onAddData,
removeField,
} = useLayoutDnd();
const getGroup = useCallback(
const getComponent = useCallback(
attributeName => {
return get(attributes, [attributeName, 'group'], '');
return get(attributes, [attributeName, 'component'], '');
},
[attributes]
);
@ -53,7 +53,7 @@ const FieldsReorder = ({ className }) => {
return (
<Item
groupUid={getGroup(name)}
componentUid={getComponent(name)}
itemIndex={index}
key={name}
moveRow={moveRow}

View File

@ -17,7 +17,13 @@ import FilterPickerOption from '../FilterPickerOption';
import init from './init';
import reducer, { initialState } from './reducer';
const NOT_ALLOWED_FILTERS = ['json', 'group', 'relation', 'media', 'richtext'];
const NOT_ALLOWED_FILTERS = [
'json',
'component',
'relation',
'media',
'richtext',
];
function FilterPicker({
actions,

View File

@ -7,8 +7,8 @@ const Wrapper = styled.div`
height: 30px;
width: 100%;
padding: 0 5px;
${({ isGroup }) => {
if (isGroup) {
${({ isComponent }) => {
if (isComponent) {
return css`
height: 36px;
border: 1px solid #e3e9f3;

View File

@ -3,19 +3,19 @@ import PropTypes from 'prop-types';
import Wrapper from './components';
const PreviewCarret = ({ isGroup, style }) => (
<Wrapper isGroup={isGroup} style={style}>
const PreviewCarret = ({ isComponent, style }) => (
<Wrapper isComponent={isComponent} style={style}>
<div />
</Wrapper>
);
PreviewCarret.defaultProps = {
isGroup: false,
isComponent: false,
style: {},
};
PreviewCarret.propTypes = {
isGroup: PropTypes.bool,
isComponent: PropTypes.bool,
style: PropTypes.object,
};

View File

@ -31,20 +31,20 @@ import { unformatLayout } from '../../utils/layout';
import getInputProps from './utils/getInputProps';
// TODO to remove when the API is available
import {
retrieveDisplayedGroups,
retrieveGroupLayoutsToFetch,
} from '../EditView/utils/groups';
retrieveDisplayedComponents,
retrieveComponentsLayoutsToFetch,
} from '../EditView/utils/components';
import reducer, { initialState } from './reducer';
const EditSettingsView = ({
deleteLayout,
groupsAndModelsMainPossibleMainFields,
componentsAndModelsMainPossibleMainFields,
history: { push },
location: { search },
slug,
}) => {
const { groupSlug, type } = useParams();
const { componentSlug, type } = useParams();
const [reducerState, dispatch] = useReducer(reducer, initialState);
const [isModalFormOpen, setIsModalFormOpen] = useState(false);
@ -55,7 +55,7 @@ const EditSettingsView = ({
const params = source === 'content-manager' && type ? {} : { source };
const {
groupLayouts,
componentLayouts,
isLoading,
initialData,
metaToEdit,
@ -107,28 +107,28 @@ const EditSettingsView = ({
const getSelectedItemSelectOptions = useCallback(
formType => {
if (formType !== 'relation' && formType !== 'group') {
if (formType !== 'relation' && formType !== 'component') {
return [];
}
const targetKey = formType === 'group' ? 'group' : 'targetModel';
const targetKey = formType === 'component' ? 'component' : 'targetModel';
const key = get(
modifiedData,
['schema', 'attributes', metaToEdit, targetKey],
''
);
return get(groupsAndModelsMainPossibleMainFields, [key], []);
return get(componentsAndModelsMainPossibleMainFields, [key], []);
},
[metaToEdit, groupsAndModelsMainPossibleMainFields, modifiedData]
[metaToEdit, componentsAndModelsMainPossibleMainFields, modifiedData]
);
useEffect(() => {
const getData = async () => {
try {
const { data } = await request(
getRequestUrl(`${type}/${slug || groupSlug}`),
getRequestUrl(`${type}/${slug || componentSlug}`),
{
method: 'GET',
params,
@ -137,21 +137,23 @@ const EditSettingsView = ({
);
// TODO temporary to remove when api available
const groups = retrieveDisplayedGroups(
const components = retrieveDisplayedComponents(
get(data, 'schema.attributes', {})
);
const groupLayoutsToGet = retrieveGroupLayoutsToFetch(groups);
const componentLayoutsToGet = retrieveComponentsLayoutsToFetch(
components
);
const groupData = await Promise.all(
groupLayoutsToGet.map(uid =>
request(`/${pluginId}/groups/${uid}`, {
const componentData = await Promise.all(
componentLayoutsToGet.map(uid =>
request(`/${pluginId}/components/${uid}`, {
method: 'GET',
signal,
})
)
);
const groupLayouts = groupData.reduce((acc, current) => {
const componentLayouts = componentData.reduce((acc, current) => {
acc[current.data.uid] = current.data;
return acc;
@ -161,7 +163,7 @@ const EditSettingsView = ({
type: 'GET_DATA_SUCCEEDED',
data,
// TODO temporary to remove when api available
groupLayouts,
componentLayouts,
});
} catch (err) {
if (err.code !== 20) {
@ -203,12 +205,12 @@ const EditSettingsView = ({
delete body.schema;
delete body.uid;
delete body.source;
delete body.isGroup;
delete body.isComponent;
await request(getRequestUrl(`${type}/${slug || groupSlug}`), {
await request(getRequestUrl(`${type}/${slug || componentSlug}`), {
method: 'PUT',
body,
params: type === 'groups' ? {} : params,
params: type === 'components' ? {} : params,
signal,
});
@ -266,7 +268,10 @@ const EditSettingsView = ({
getForm().map((meta, index) => {
const formType = get(getAttributes, [metaToEdit, 'type']);
if ((formType === 'group' || formType === 'media') && meta !== 'label') {
if (
(formType === 'component' || formType === 'media') &&
meta !== 'label'
) {
return null;
}
@ -319,7 +324,7 @@ const EditSettingsView = ({
attributes={getAttributes}
buttonData={getEditRemainingFields()}
goTo={push}
groupLayouts={groupLayouts}
componentLayouts={componentLayouts}
layout={getEditLayout()}
metadatas={get(modifiedData, ['metadatas'], {})}
moveItem={moveItem}
@ -374,7 +379,7 @@ const EditSettingsView = ({
});
}}
onConfirmSubmit={handleConfirm}
slug={slug || groupSlug}
slug={slug || componentSlug}
isEditSettings
>
<div className="row">
@ -384,7 +389,7 @@ const EditSettingsView = ({
description={`${pluginId}.containers.SettingPage.editSettings.description`}
/>
</LayoutTitle>
{type !== 'groups' && (
{type !== 'components' && (
<LayoutTitle className="col-4">
<FormTitle
title={`${pluginId}.containers.SettingPage.relations`}
@ -394,7 +399,7 @@ const EditSettingsView = ({
)}
<FieldsReorder className={fieldsReorderClassName} />
{type !== 'groups' && (
{type !== 'components' && (
<SortableList
addItem={name => {
dispatch({
@ -449,7 +454,7 @@ EditSettingsView.defaultProps = {
EditSettingsView.propTypes = {
deleteLayout: PropTypes.func.isRequired,
groupsAndModelsMainPossibleMainFields: PropTypes.object.isRequired,
componentsAndModelsMainPossibleMainFields: PropTypes.object.isRequired,
history: PropTypes.shape({
push: PropTypes.func,
}).isRequired,

View File

@ -16,7 +16,7 @@ import {
import pluginId from '../../pluginId';
import { EditViewProvider } from '../../contexts/EditView';
import Container from '../../components/Container';
import Group from '../../components/Group';
import ComponentField from '../../components/ComponentField';
import Inputs from '../../components/Inputs';
import SelectWrapper from '../../components/SelectWrapper';
import createYupSchema from './utils/schema';
@ -30,11 +30,12 @@ import {
cleanData,
mapDataKeysToFilesToUpload,
} from './utils/formatData';
import {
getDefaultGroupValues,
retrieveDisplayedGroups,
retrieveGroupLayoutsToFetch,
} from './utils/groups';
getDefaultComponentValues,
retrieveDisplayedComponents,
retrieveComponentsLayoutsToFetch,
} from './utils/components';
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
@ -49,14 +50,16 @@ function EditView({
plugins,
}) {
const { id } = useParams();
console.log({ layouts });
const abortController = new AbortController();
const { signal } = abortController;
const layout = get(layouts, [slug], {});
const isCreatingEntry = id === 'create';
const attributes = get(layout, ['schema', 'attributes'], {});
const groups = retrieveDisplayedGroups(attributes);
const groupLayoutsToGet = retrieveGroupLayoutsToFetch(groups);
const components = retrieveDisplayedComponents(attributes);
const componentLayoutsToGet = retrieveComponentsLayoutsToFetch(components);
// States
const [showWarningCancel, setWarningCancel] = useState(false);
const [showWarningDelete, setWarningDelete] = useState(false);
@ -69,7 +72,7 @@ function EditView({
const {
didCheckErrors,
errors,
groupLayoutsData,
componentLayoutsData,
initialData,
modifiedData,
isLoading,
@ -81,30 +84,33 @@ function EditView({
isLoadingForLayouts || (!isCreatingEntry && isLoading);
useEffect(() => {
const fetchGroupLayouts = async () => {
const fetchComponentLayouts = async () => {
try {
const data = await Promise.all(
groupLayoutsToGet.map(uid =>
request(`/${pluginId}/groups/${uid}`, {
componentLayoutsToGet.map(uid =>
request(`/${pluginId}/components/${uid}`, {
method: 'GET',
signal,
})
)
);
const groupLayouts = data.reduce((acc, current) => {
const componentLayouts = data.reduce((acc, current) => {
acc[current.data.uid] = current.data;
return acc;
}, {});
// Retrieve all the default values for the repeatables and init the form
const defaultGroupValues = getDefaultGroupValues(groups, groupLayouts);
const defaultComponentValues = getDefaultComponentValues(
components,
componentLayouts
);
dispatch({
type: 'GET_GROUP_LAYOUTS_SUCCEEDED',
groupLayouts,
defaultGroupValues,
type: 'GET_COMPONENT_LAYOUTS_SUCCEEDED',
componentLayouts,
defaultComponentValues,
isCreatingEntry,
});
} catch (err) {
@ -128,7 +134,7 @@ function EditView({
data,
defaultForm: setDefaultForm(get(layout, ['schema', 'attributes'])),
});
fetchGroupLayouts();
fetchComponentLayouts();
} catch (err) {
if (err.code !== 20) {
strapi.notification.error(`${pluginId}.error.record.fetch`);
@ -147,7 +153,7 @@ function EditView({
data: setDefaultForm(get(layout, ['schema', 'attributes'])),
defaultForm: setDefaultForm(get(layout, ['schema', 'attributes'])),
});
fetchGroupLayouts();
fetchComponentLayouts();
}
return () => {
@ -201,7 +207,9 @@ function EditView({
const fields = get(layout, ['layouts', 'edit'], []);
const checkFormErrors = async () => {
const schema = createYupSchema(layout, { groups: groupLayoutsData });
const schema = createYupSchema(layout, {
components: componentLayoutsData,
});
let errors = {};
try {
@ -235,7 +243,9 @@ function EditView({
const handleSubmit = async e => {
e.preventDefault();
const schema = createYupSchema(layout, { groups: groupLayoutsData });
const schema = createYupSchema(layout, {
components: componentLayoutsData,
});
try {
// Validate the form using yup
@ -244,14 +254,14 @@ function EditView({
setIsSubmitting(true);
emitEvent('willSaveEntry');
// Create an object containing all the paths of the media fields
const filesMap = getMediaAttributes(layout, groupLayoutsData);
const filesMap = getMediaAttributes(layout, componentLayoutsData);
// Create an object that maps the keys with the related files to upload
const filesToUpload = mapDataKeysToFilesToUpload(filesMap, modifiedData);
const cleanedData = cleanData(
cloneDeep(modifiedData),
layout,
groupLayoutsData
componentLayoutsData
);
const formData = new FormData();
@ -359,10 +369,10 @@ function EditView({
errors: {},
});
}}
resetGroupData={groupName => {
resetComponentData={componentName => {
dispatch({
type: 'RESET_GROUP_DATA',
groupName,
type: 'RESET_COMPONENT_DATA',
componentName,
});
}}
search={search}
@ -416,22 +426,29 @@ function EditView({
}
const [{ name }] = fieldsRow;
const group = get(layout, ['schema', 'attributes', name], {});
const groupMetas = get(
const component = get(
layout,
['schema', 'attributes', name],
{}
);
const componentMetas = get(
layout,
['metadatas', name, 'edit'],
{}
);
const groupValue = get(
const componentValue = get(
modifiedData,
[name],
group.repeatable ? [] : {}
component.repeatable ? [] : {}
);
if (fieldsRow.length === 1 && group.type === 'group') {
if (
fieldsRow.length === 1 &&
component.type === 'component'
) {
// Array containing all the keys with of the error object created by YUP
// It is used only to know if whether or not we need to apply an orange border to the n+1 field item
const groupErrorKeys = Object.keys(errors)
const componentErrorKeys = Object.keys(errors)
.filter(errorKey => errorKey.includes(name))
.map(errorKey =>
errorKey
@ -441,23 +458,23 @@ function EditView({
);
return (
<Group
{...group}
{...groupMetas}
<ComponentField
{...component}
{...componentMetas}
addField={(keys, isRepeatable = true) => {
dispatch({
type: 'ADD_FIELD_TO_GROUP',
type: 'ADD_FIELD_TO_COMPONENT',
keys: keys.split('.'),
isRepeatable,
});
}}
groupErrorKeys={groupErrorKeys}
groupValue={groupValue}
componentErrorKeys={componentErrorKeys}
componentValue={componentValue}
key={key}
isRepeatable={group.repeatable || false}
isRepeatable={component.repeatable || false}
name={name}
modifiedData={modifiedData}
moveGroupField={(dragIndex, overIndex, name) => {
moveComponentField={(dragIndex, overIndex, name) => {
dispatch({
type: 'MOVE_FIELD',
dragIndex,
@ -466,7 +483,11 @@ function EditView({
});
}}
onChange={handleChange}
layout={get(groupLayoutsData, group.group, {})}
layout={get(
componentLayoutsData,
component.component,
{}
)}
pathname={pathname}
removeField={(keys, shouldAddEmptyField) => {
dispatch({

View File

@ -4,12 +4,12 @@ const initialState = fromJS({
collapses: {},
didCheckErrors: false,
errors: {},
groupLayoutsData: {},
componentLayoutsData: {},
initialData: {},
isLoading: true,
isLoadingForLayouts: true,
modifiedData: {},
defaultGroupValues: {},
defaultComponentValues: {},
defaultForm: {},
});
@ -22,10 +22,10 @@ const getMax = arr => {
function reducer(state, action) {
switch (action.type) {
case 'ADD_FIELD_TO_GROUP':
case 'ADD_FIELD_TO_COMPONENT':
return state.updateIn(['modifiedData', ...action.keys], list => {
const defaultAttribute = state.getIn([
'defaultGroupValues',
'defaultComponentValues',
...action.keys,
'defaultRepeatable',
]);
@ -71,16 +71,16 @@ function reducer(state, action) {
.update('initialData', () => fromJS(action.data))
.update('modifiedData', () => fromJS(action.data))
.update('isLoading', () => false);
case 'GET_GROUP_LAYOUTS_SUCCEEDED': {
const addTempIdToGroupData = obj => {
const { defaultGroupValues } = action;
case 'GET_COMPONENT_LAYOUTS_SUCCEEDED': {
const addTempIdToComponentData = obj => {
const { defaultComponentValues } = action;
if (action.isCreatingEntry === true) {
return obj.keySeq().reduce((acc, current) => {
if (defaultGroupValues[current]) {
if (defaultComponentValues[current]) {
return acc.set(
current,
fromJS(defaultGroupValues[current].toSet)
fromJS(defaultComponentValues[current].toSet)
);
}
@ -88,7 +88,10 @@ function reducer(state, action) {
}, obj);
} else {
return obj.keySeq().reduce((acc, current) => {
if (defaultGroupValues[current] && List.isList(obj.get(current))) {
if (
defaultComponentValues[current] &&
List.isList(obj.get(current))
) {
const formatted = obj.get(current).reduce((acc2, curr, index) => {
return acc2.set(index, curr.set('_temp__id', index));
}, List([]));
@ -102,13 +105,15 @@ function reducer(state, action) {
};
return state
.update('groupLayoutsData', () => fromJS(action.groupLayouts))
.update('defaultGroupValues', () => fromJS(action.defaultGroupValues))
.update('componentLayoutsData', () => fromJS(action.componentLayouts))
.update('defaultComponentValues', () =>
fromJS(action.defaultComponentValues)
)
.update('modifiedData', obj => {
return addTempIdToGroupData(obj);
return addTempIdToComponentData(obj);
})
.update('initialData', obj => {
return addTempIdToGroupData(obj);
return addTempIdToComponentData(obj);
})
.update('isLoadingForLayouts', () => false);
}
@ -123,16 +128,18 @@ function reducer(state, action) {
.delete(action.dragIndex)
.insert(action.overIndex, list.get(action.dragIndex));
});
case 'ON_CHANGE': {
let newState = state;
const [nonRepeatableGroupKey] = action.keys;
const [nonRepeatableComponentKey] = action.keys;
if (
action.keys.length === 2 &&
state.getIn(['modifiedData', nonRepeatableGroupKey]) === null
state.getIn(['modifiedData', nonRepeatableComponentKey]) === null
) {
newState = state.updateIn(['modifiedData', nonRepeatableGroupKey], () =>
fromJS({})
newState = state.updateIn(
['modifiedData', nonRepeatableComponentKey],
() => fromJS({})
);
}
@ -140,13 +147,14 @@ function reducer(state, action) {
return action.value;
});
}
case 'ON_REMOVE_FIELD':
return state
.removeIn(['modifiedData', ...action.keys])
.updateIn(['modifiedData', action.keys[0]], list => {
if (action.shouldAddEmptyField) {
const defaultAttribute = state.getIn([
'defaultGroupValues',
'defaultComponentValues',
action.keys[0],
'defaultRepeatable',
]);
@ -164,13 +172,14 @@ function reducer(state, action) {
.update('modifiedData', () => state.get('initialData'))
.update('errors', () => fromJS({}))
.update('didCheckErrors', v => !v);
case 'RESET_GROUP_DATA': {
const groupPath = ['modifiedData', action.groupName];
case 'RESET_COMPONENT_DATA': {
const componentPath = ['modifiedData', action.componentName];
return state
.updateIn(
groupPath,
() => state.getIn(['defaultForm', action.groupName]) || null
componentPath,
() => state.getIn(['defaultForm', action.componentName]) || null
)
.update('errors', () => fromJS({}))
.update('didCheckErrors', v => !v);

View File

@ -0,0 +1,288 @@
import {
getDefaultComponentValues,
retrieveDisplayedComponents,
retrieveComponentsLayoutsToFetch,
} from '../utils/components';
describe('Content Manager | EditView | utils | components', () => {
describe('getDefaultComponentValues', () => {
it('should return an empty object if the args are empty', () => {
expect(getDefaultComponentValues([], {})).toEqual({});
});
it('should return the correct data', () => {
const component1 = {
schema: {
attributes: {
title: {
type: 'string',
default: 'test',
},
description: {
type: 'text',
},
},
},
};
const component2 = {
schema: {
attributes: {
otherTitle: {
type: 'string',
default: 'test',
},
otherDescription: {
type: 'text',
},
},
},
};
const component3 = {
schema: {
attributes: {
otherTitle: {
type: 'string',
},
otherDescription: {
type: 'text',
},
},
},
};
const component4 = {
schema: {
attributes: {
otherTitle: {
type: 'string',
},
otherDescription: {
type: 'text',
},
},
},
};
const component5 = {
schema: {
attributes: {
otherTitle: {
type: 'string',
},
otherDescription: {
type: 'text',
},
},
},
};
const components = [
{
key: 'component1',
component: 'component1',
},
{
key: 'component2',
component: 'component2',
repeatable: true,
min: 1,
},
{
key: 'component3',
component: 'component3',
repeatable: true,
},
{
key: 'component4',
component: 'component4',
},
{
key: 'component5',
component: 'component5',
required: true,
repeatable: true,
},
{
key: 'component6',
component: 'component5',
min: 1,
repeatable: true,
},
];
const componentLayouts = {
component1,
component2,
component3,
component4,
component5,
};
const expected = {
component1: {
toSet: {
title: 'test',
},
defaultRepeatable: {
title: 'test',
},
},
component2: {
toSet: [{ _temp__id: 0, otherTitle: 'test' }],
defaultRepeatable: {
otherTitle: 'test',
},
},
component3: {
toSet: [],
defaultRepeatable: {},
},
component4: {
toSet: {},
defaultRepeatable: {},
},
component5: {
toSet: [],
defaultRepeatable: {},
},
component6: {
toSet: [{ _temp__id: 0 }],
defaultRepeatable: {},
},
};
expect(getDefaultComponentValues(components, componentLayouts)).toEqual(
expected
);
});
});
describe('retrieveDisplayedComponents', () => {
it('should return an array with all the components', () => {
const attributes = {
name: {
maxLength: 50,
required: true,
minLength: 5,
type: 'string',
},
cover: {
model: 'file',
via: 'related',
plugin: 'upload',
},
menu: {
model: 'menu',
via: 'restaurant',
},
categories: {
collection: 'category',
},
price_range: {
enum: [
'very_cheap',
'cheap',
'average',
'expensive',
'very_expensive',
],
type: 'enumeration',
},
description: {
type: 'richtext',
required: true,
},
opening_times: {
component: 'openingtimes',
type: 'component',
required: true,
repeatable: true,
min: 1,
max: 10,
},
opening_times2: {
component: 'openingtimes',
type: 'component',
},
closing_period: {
component: 'closingperiod',
type: 'component',
},
services: {
component: 'restaurantservice',
required: true,
repeatable: true,
type: 'component',
},
address: {
model: 'address',
},
};
const expected = [
{
key: 'opening_times',
component: 'openingtimes',
repeatable: true,
min: 1,
isOpen: false,
},
{
key: 'opening_times2',
component: 'openingtimes',
isOpen: true,
min: undefined,
repeatable: undefined,
},
{
key: 'closing_period',
component: 'closingperiod',
isOpen: true,
min: undefined,
repeatable: undefined,
},
{
key: 'services',
component: 'restaurantservice',
repeatable: true,
isOpen: false,
min: undefined,
},
];
expect(retrieveDisplayedComponents(attributes)).toEqual(expected);
});
});
describe('retrieveComponentsLayoutsToFetch', () => {
it('should return a filterd array of the components to fetch', () => {
const components = [
{
key: 'opening_times',
component: 'openingtimes',
repeatable: true,
min: 1,
isOpen: false,
},
{
key: 'opening_times2',
component: 'openingtimes',
isOpen: true,
min: undefined,
repeatable: undefined,
},
{
key: 'closing_period',
component: 'closingperiod',
isOpen: true,
min: undefined,
repeatable: undefined,
},
{
key: 'services',
component: 'restaurantservice',
repeatable: true,
isOpen: false,
min: undefined,
},
];
const expected = ['openingtimes', 'closingperiod', 'restaurantservice'];
expect(retrieveComponentsLayoutsToFetch(components)).toEqual(expected);
});
});
});

View File

@ -1,7 +1,7 @@
import createDefaultForm from '../utils/createDefaultForm';
describe('Content Manager | EditView | utils | createDefaultForm', () => {
it('should return an empty object if there is no group or default value in the argument', () => {
it('should return an empty object if there is no component or default value in the argument', () => {
const attributes = {
title: {
type: 'string',
@ -69,7 +69,7 @@ describe('Content Manager | EditView | utils | createDefaultForm', () => {
});
});
it('should handle the group fields correctly', () => {
it('should handle the component fields correctly', () => {
const attributes = {
title: {
type: 'string',
@ -78,40 +78,40 @@ describe('Content Manager | EditView | utils | createDefaultForm', () => {
description: {
type: 'text',
},
group: {
type: 'group',
component: {
type: 'component',
},
group1: {
type: 'group',
component1: {
type: 'component',
required: true,
},
group2: {
type: 'group',
component2: {
type: 'component',
repeatable: true,
},
group3: {
type: 'group',
component3: {
type: 'component',
repeatable: true,
required: true,
},
group4: {
type: 'group',
component4: {
type: 'component',
repeatable: true,
required: true,
min: 2,
},
group5: {
type: 'group',
component5: {
type: 'component',
repeatable: true,
min: 2,
},
};
expect(createDefaultForm(attributes)).toEqual({
title: 'test',
group1: {},
group3: [],
group4: [{ _temp__id: 0 }, { _temp__id: 1 }],
group5: [{ _temp__id: 0 }, { _temp__id: 1 }],
component1: {},
component3: [],
component4: [{ _temp__id: 0 }, { _temp__id: 1 }],
component5: [{ _temp__id: 0 }, { _temp__id: 1 }],
});
});
});

View File

@ -8,14 +8,14 @@ const ctLayout = {
enum: { type: 'enumeration', enum: ['un', 'deux'] },
fb_cta: {
required: true,
type: 'group',
group: 'cta_facebook',
type: 'component',
component: 'cta_facebook',
repeatable: false,
},
id: { type: 'integer' },
ingredients: {
type: 'group',
group: 'ingredients',
type: 'component',
component: 'ingredients',
repeatable: true,
min: 1,
max: 10,
@ -31,8 +31,8 @@ const ctLayout = {
type: 'relation',
},
mainIngredient: {
type: 'group',
group: 'ingredients',
type: 'component',
component: 'ingredients',
repeatable: false,
},
mainTag: {
@ -67,7 +67,7 @@ const ctLayout = {
},
};
const groupLayouts = {
const componentLayouts = {
cta_facebook: {
schema: {
attributes: {
@ -123,4 +123,4 @@ const simpleCtLayout = {
settings: {},
};
export { ctLayout, groupLayouts, simpleCtLayout };
export { ctLayout, componentLayouts, simpleCtLayout };

View File

@ -3,17 +3,17 @@ import {
getMediaAttributes,
helperCleanData,
} from '../utils/formatData';
import { ctLayout, groupLayouts, simpleCtLayout } from './data';
import { ctLayout, componentLayouts, simpleCtLayout } from './data';
describe('Content Manager | EditView | utils | cleanData', () => {
let simpleContentTypeLayout;
let contentTypeLayout;
let grpLayouts;
let cpLayouts;
beforeEach(() => {
simpleContentTypeLayout = simpleCtLayout;
contentTypeLayout = ctLayout;
grpLayouts = groupLayouts;
cpLayouts = componentLayouts;
});
it('should format de data correctly if the content type has no group and no file has been added', () => {
@ -60,7 +60,7 @@ describe('Content Manager | EditView | utils | cleanData', () => {
pictures: [1, 2],
};
expect(cleanData(data, simpleContentTypeLayout, grpLayouts)).toEqual(
expect(cleanData(data, simpleContentTypeLayout, cpLayouts)).toEqual(
expected
);
});
@ -148,7 +148,9 @@ describe('Content Manager | EditView | utils | cleanData', () => {
title: 'test',
};
expect(cleanData(data, contentTypeLayout, groupLayouts)).toEqual(expected);
expect(cleanData(data, contentTypeLayout, componentLayouts)).toEqual(
expected
);
});
});
@ -202,36 +204,40 @@ describe('Content Manager | EditView | utils | helperCleanData', () => {
describe('Content Manager | EditView | utils | getMediasAttributes', () => {
let contentTypeLayout;
let grpLayouts;
let cpLayouts;
beforeEach(() => {
contentTypeLayout = ctLayout;
grpLayouts = groupLayouts;
cpLayouts = componentLayouts;
});
it('should return an array containing the paths of all the medias attributes', () => {
const expected = {
'ingredients.testMultiple': {
multiple: true,
isGroup: true,
isComponent: true,
repeatable: true,
},
'ingredients.test': {
multiple: false,
isComponent: true,
repeatable: true,
},
'ingredients.test': { multiple: false, isGroup: true, repeatable: true },
'mainIngredient.testMultiple': {
multiple: true,
isGroup: true,
isComponent: true,
repeatable: false,
},
'mainIngredient.test': {
multiple: false,
isGroup: true,
isComponent: true,
repeatable: false,
},
pic: { multiple: false, isGroup: false, repeatable: false },
pictures: { multiple: true, isGroup: false, repeatable: false },
pic: { multiple: false, isComponent: false, repeatable: false },
pictures: { multiple: true, isComponent: false, repeatable: false },
};
expect(getMediaAttributes(contentTypeLayout, grpLayouts)).toMatchObject(
expect(getMediaAttributes(contentTypeLayout, cpLayouts)).toMatchObject(
expected
);
});

View File

@ -0,0 +1,72 @@
import { get } from 'lodash';
import setDefaultForm from './createDefaultForm';
// Retrieve all the default values for the repeatables and init the form
const getDefaultComponentValues = (components, componentLayouts) => {
const defaultComponentValues = components.reduce((acc, current) => {
const defaultForm = setDefaultForm(
get(componentLayouts, [current.component, 'schema', 'attributes'], {})
);
const arr = [];
if (current.min && current.repeatable === true) {
for (let i = 0; i < current.min; i++) {
arr.push({ ...defaultForm, _temp__id: i });
}
}
acc[current.key] = {
toSet: arr,
defaultRepeatable: defaultForm,
};
if (current.repeatable !== true) {
acc[current.key] = {
toSet: defaultForm,
defaultRepeatable: defaultForm,
};
}
return acc;
}, {});
return defaultComponentValues;
};
const retrieveDisplayedComponents = attributes => {
return Object.keys(attributes).reduce((acc, current) => {
const { component, repeatable, type, min } = get(attributes, [current], {
component: '',
type: '',
repeatable,
});
if (type === 'component') {
acc.push({
key: current,
component,
repeatable,
isOpen: !repeatable,
min,
});
}
return acc;
}, []);
};
const retrieveComponentsLayoutsToFetch = components => {
return components
.filter(
(current, index) =>
components.findIndex(el => el.component === current.component) === index
)
.map(({ component }) => component);
};
export {
getDefaultComponentValues,
retrieveDisplayedComponents,
retrieveComponentsLayoutsToFetch,
};

View File

@ -23,7 +23,7 @@ const setDefaultForm = attributes => {
acc[current] = defaultValue;
}
if (type === 'group') {
if (type === 'component') {
if (required === true) {
acc[current] = repeatable === true ? [] : {};
}

View File

@ -1,6 +1,6 @@
import { get, isArray, isEmpty, isObject } from 'lodash';
export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
export const cleanData = (retrievedData, ctLayout, componentLayouts) => {
const getType = (schema, attrName) =>
get(schema, ['attributes', attrName, 'type'], '');
const getOtherInfos = (schema, arr) =>
@ -10,7 +10,7 @@ export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
return Object.keys(data).reduce((acc, current) => {
const attrType = getType(layout.schema, current);
const value = get(data, current);
const group = getOtherInfos(layout.schema, [current, 'group']);
const component = getOtherInfos(layout.schema, [current, 'component']);
const isRepeatable = getOtherInfos(layout.schema, [
current,
'repeatable',
@ -40,14 +40,14 @@ export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
get(value, 0) instanceof File ? null : get(value, 'id', null);
}
break;
case 'group':
case 'component':
if (isRepeatable) {
cleanedData = value
? value.map(data => {
delete data._temp__id;
const subCleanedData = recursiveCleanData(
data,
groupLayouts[group]
componentLayouts[component]
);
return subCleanedData;
@ -55,7 +55,7 @@ export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
: value;
} else {
cleanedData = value
? recursiveCleanData(value, groupLayouts[group])
? recursiveCleanData(value, componentLayouts[component])
: value;
}
break;
@ -72,11 +72,11 @@ export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
return recursiveCleanData(retrievedData, ctLayout);
};
export const getMediaAttributes = (ctLayout, groupLayouts) => {
export const getMediaAttributes = (ctLayout, componentLayouts) => {
const getMedia = (
layout,
prefix = '',
isGroupType = false,
isComponentType = false,
repeatable = false
) => {
const attributes = get(layout, ['schema', 'attributes'], {});
@ -85,21 +85,26 @@ export const getMediaAttributes = (ctLayout, groupLayouts) => {
const type = get(attributes, [current, 'type']);
const multiple = get(attributes, [current, 'multiple'], false);
const isRepeatable = get(attributes, [current, 'repeatable']);
const isGroup = type === 'group';
const isComponent = type === 'component';
if (isGroup) {
const group = get(attributes, [current, 'group']);
if (isComponent) {
const component = get(attributes, [current, 'component']);
return {
...acc,
...getMedia(groupLayouts[group], current, isGroup, isRepeatable),
...getMedia(
componentLayouts[component],
current,
isComponent,
isRepeatable
),
};
}
if (type === 'media') {
const path = prefix !== '' ? `${prefix}.${current}` : current;
acc[path] = { multiple, isGroup: isGroupType, repeatable };
acc[path] = { multiple, isComponent: isComponentType, repeatable };
}
return acc;
@ -123,7 +128,7 @@ export const mapDataKeysToFilesToUpload = (filesMap, data) => {
return Object.keys(filesMap).reduce((acc, current) => {
const keys = current.split('.');
const isMultiple = get(filesMap, [current, 'multiple'], false);
const isGroup = get(filesMap, [current, 'isGroup'], false);
const isComponent = get(filesMap, [current, 'isComponent'], false);
const isRepeatable = get(filesMap, [current, 'repeatable'], false);
const getFilesToUpload = path => {
@ -152,10 +157,10 @@ export const mapDataKeysToFilesToUpload = (filesMap, data) => {
}
}
if (isGroup && isRepeatable) {
if (isComponent && isRepeatable) {
const [key, targetKey] = current.split('.');
const groupData = get(data, [key], []);
const groupFiles = groupData.reduce((acc1, current, index) => {
const componentData = get(data, [key], []);
const componentFiles = componentData.reduce((acc1, current, index) => {
const files = isMultiple
? getFilesToUpload([key, index, targetKey])
: getFileToUpload([key, index, targetKey]);
@ -167,7 +172,7 @@ export const mapDataKeysToFilesToUpload = (filesMap, data) => {
return acc1;
}, {});
return { ...acc, ...groupFiles };
return { ...acc, ...componentFiles };
}
return acc;

View File

@ -20,13 +20,13 @@ yup.addMethod(yup.mixed, 'defined', function() {
const getAttributes = data => get(data, ['schema', 'attributes'], {});
const createYupSchema = (model, { groups }) => {
const createYupSchema = (model, { components }) => {
const attributes = getAttributes(model);
return yup.object().shape(
Object.keys(attributes).reduce((acc, current) => {
const attribute = attributes[current];
if (attribute.type !== 'relation' && attribute.type !== 'group') {
if (attribute.type !== 'relation' && attribute.type !== 'component') {
const formatted = createYupSchemaAttribute(attribute.type, attribute);
acc[current] = formatted;
}
@ -43,32 +43,35 @@ const createYupSchema = (model, { groups }) => {
: yup.array().nullable();
}
if (attribute.type === 'group') {
const groupFieldSchema = createYupSchema(groups[attribute.group], {
groups,
});
if (attribute.type === 'component') {
const componentFieldSchema = createYupSchema(
components[attribute.component],
{
components,
}
);
if (attribute.repeatable === true) {
const groupSchema =
const componentSchema =
attribute.required === true
? yup
.array()
.of(groupFieldSchema)
.of(componentFieldSchema)
.defined()
: yup
.array()
.of(groupFieldSchema)
.of(componentFieldSchema)
.nullable();
acc[current] = groupSchema;
acc[current] = componentSchema;
return acc;
} else {
const groupSchema = yup.lazy(obj => {
const componentSchema = yup.lazy(obj => {
if (obj !== undefined) {
return attribute.required === true
? groupFieldSchema.defined()
: groupFieldSchema.nullable();
? componentFieldSchema.defined()
: componentFieldSchema.nullable();
}
return attribute.required === true
@ -76,7 +79,7 @@ const createYupSchema = (model, { groups }) => {
: yup.object().nullable();
});
acc[current] = groupSchema;
acc[current] = componentSchema;
return acc;
}

View File

@ -93,7 +93,7 @@ const ListSettingsView = ({ deleteLayout, location: { search }, slug }) => {
.filter(key => {
const type = get(attributes, [key, 'type'], '');
return !['json', 'relation', 'group'].includes(type) && !!type;
return !['json', 'relation', 'component'].includes(type) && !!type;
})
.filter(field => {
return !getListDisplayedFields().includes(field);

View File

@ -124,7 +124,7 @@ function ListView({
Object.keys(getMetaDatas())
.filter(
key =>
!['json', 'group', 'relation', 'richtext'].includes(
!['json', 'component', 'relation', 'richtext'].includes(
get(layouts, [slug, 'schema', 'attributes', key, 'type'], '')
)
)

View File

@ -29,10 +29,10 @@ export function getData() {
};
}
export function getDataSucceeded(groups, models, mainFields) {
export function getDataSucceeded(components, models, mainFields) {
return {
type: GET_DATA_SUCCEEDED,
groups,
components,
models: models.filter(model => model.isDisplayed === true),
mainFields,
};

View File

@ -25,8 +25,8 @@ function Main({
deleteLayout,
getData,
getLayout,
groups,
groupsAndModelsMainPossibleMainFields,
components,
componentsAndModelsMainPossibleMainFields,
isLoading,
layouts,
location: { pathname, search },
@ -72,9 +72,9 @@ function Main({
currentEnvironment={currentEnvironment}
deleteLayout={deleteLayout}
emitEvent={emitEvent}
groups={groups}
groupsAndModelsMainPossibleMainFields={
groupsAndModelsMainPossibleMainFields
components={components}
componentsAndModelsMainPossibleMainFields={
componentsAndModelsMainPossibleMainFields
}
layouts={layouts}
models={models}
@ -84,7 +84,7 @@ function Main({
);
const routes = [
{
path: 'ctm-configurations/edit-settings/:type/:groupSlug',
path: 'ctm-configurations/edit-settings/:type/:componentSlug',
comp: EditSettingsView,
},
{ path: ':slug', comp: RecursivePath },
@ -114,8 +114,8 @@ Main.propTypes = {
currentEnvironment: PropTypes.string.isRequired,
plugins: PropTypes.object,
}),
groups: PropTypes.array.isRequired,
groupsAndModelsMainPossibleMainFields: PropTypes.object.isRequired,
components: PropTypes.array.isRequired,
componentsAndModelsMainPossibleMainFields: PropTypes.object.isRequired,
isLoading: PropTypes.bool,
layouts: PropTypes.object.isRequired,
location: PropTypes.shape({

View File

@ -15,8 +15,8 @@ import {
} from './constants';
export const initialState = fromJS({
groupsAndModelsMainPossibleMainFields: {},
groups: [],
componentsAndModelsMainPossibleMainFields: {},
components: [],
initialLayouts: {},
isLoading: true,
layouts: {},
@ -31,9 +31,9 @@ function mainReducer(state = initialState, action) {
return state.update('layouts', () => fromJS({}));
case GET_DATA_SUCCEEDED:
return state
.update('groups', () => fromJS(action.groups))
.update('components', () => fromJS(action.components))
.update('models', () => fromJS(action.models))
.update('groupsAndModelsMainPossibleMainFields', () =>
.update('componentsAndModelsMainPossibleMainFields', () =>
fromJS(action.mainFields)
)
.update('isLoading', () => false);

View File

@ -9,13 +9,13 @@ import { GET_DATA, GET_LAYOUT } from './constants';
const getRequestUrl = path => `/${pluginId}/${path}`;
const createPossibleMainFieldsForModelsAndGroups = array => {
const createPossibleMainFieldsForModelsAndComponents = array => {
return array.reduce((acc, current) => {
const attributes = get(current, ['schema', 'attributes'], {});
const possibleMainFields = Object.keys(attributes).filter(attr => {
return ![
'boolean',
'group',
'component',
'json',
'media',
'password',
@ -33,16 +33,16 @@ const createPossibleMainFieldsForModelsAndGroups = array => {
function* getData() {
try {
const [{ data: groups }, { data: models }] = yield all(
['groups', 'content-types'].map(endPoint =>
const [{ data: components }, { data: models }] = yield all(
['components', 'content-types'].map(endPoint =>
call(request, getRequestUrl(endPoint), { method: 'GET' })
)
);
yield put(
getDataSucceeded(groups, models, {
...createPossibleMainFieldsForModelsAndGroups(groups),
...createPossibleMainFieldsForModelsAndGroups(models),
getDataSucceeded(components, models, {
...createPossibleMainFieldsForModelsAndComponents(components),
...createPossibleMainFieldsForModelsAndComponents(models),
})
);
} catch (err) {

View File

@ -12,8 +12,8 @@ describe('Content Manager | Main | reducer', () => {
beforeEach(() => {
state = {
groupsAndModelsMainPossibleMainFields: {},
groups: [],
componentsAndModelsMainPossibleMainFields: {},
components: [],
initialLayouts: {
test: {
layouts: {

View File

@ -5,10 +5,6 @@ const EditView = lazy(() => import('../EditView'));
const EditSettingsView = lazy(() => import('../EditSettingsView'));
const ListView = lazy(() => import('../ListView'));
const ListSettingsView = lazy(() => import('../ListSettingsView'));
// import EditView from '../EditView';
// import EditSettingsView from '../EditSettingsView';
// import ListSettingsView from '../ListSettingsView';
// import ListView from '../ListView';
const RecursivePath = props => {
const { url } = useRouteMatch();

View File

@ -1,7 +1,7 @@
export default {
COMPONENT: 'component',
EDIT_FIELD: 'editField',
EDIT_RELATION: 'editRelation',
FIELD: 'field',
GROUP: 'group',
RELATION: 'relation',
};

View File

@ -2,7 +2,7 @@
exports[`<Loader /> should not crash 1`] = `
<div
className="sc-jnlKLf dzQmya"
className="sc-fYxtnH jGsbTZ"
>
<div
className="centered"