Created Repeatable component with nested component support

This commit is contained in:
soupette 2019-11-05 15:12:48 +01:00 committed by Alexandre Bodin
parent e51bdba352
commit fc6bbbfadc
14 changed files with 519 additions and 33 deletions

View File

@ -11,6 +11,11 @@
},
"time": {
"type": "string"
},
"dish": {
"component": "default.dish",
"type": "component",
"repeatable": true
}
}
}

View File

@ -6,20 +6,14 @@ import pluginId from '../../pluginId';
import useDataManager from '../../hooks/useDataManager';
import useEditView from '../../hooks/useEditView';
import ComponentInitializer from '../ComponentInitializer';
import AddFieldButton from './AddFieldButton';
import EmptyComponent from './EmptyComponent';
import NonRepeatableComponent from '../NonRepeatableComponent';
import RepeatableComponent from '../RepeatableComponent';
import Label from './Label';
import Reset from './ResetComponent';
import Wrapper from './Wrapper';
import NonRepeatableComponent from '../NonRepeatableComponent';
const FieldComponent = ({ componentUid, isRepeatable, label, name }) => {
const {
addRepeatableComponentToField,
modifiedData,
removeComponentFromField,
} = useDataManager();
const { modifiedData, removeComponentFromField } = useDataManager();
const { allLayoutData } = useEditView();
const componentValue = get(modifiedData, name, null);
const componentValueLength = size(componentValue);
@ -31,7 +25,6 @@ const FieldComponent = ({ componentUid, isRepeatable, label, name }) => {
{}
);
const displayedFields = get(currentComponentSchema, ['layouts', 'edit'], []);
console.log({ componentValue });
return (
<Wrapper className="col-12">
@ -61,27 +54,14 @@ const FieldComponent = ({ componentUid, isRepeatable, label, name }) => {
/>
)}
{isRepeatable && (
<div>
{componentValueLength === 0 && (
<EmptyComponent>
<FormattedMessage id={`${pluginId}.components.empty-repeatable`}>
{msg => <p>{msg}</p>}
</FormattedMessage>
</EmptyComponent>
)}
<AddFieldButton
withBorderRadius={false}
type="button"
onClick={() => {
// TODO min max validations
// TODO add componentUID
addRepeatableComponentToField(name);
}}
>
<i className="fa fa-plus" />
<FormattedMessage id={`${pluginId}.containers.EditView.add.new`} />
</AddFieldButton>
</div>
<RepeatableComponent
componentValue={componentValue}
componentValueLength={componentValueLength}
componentUid={componentUid}
fields={displayedFields}
name={name}
schema={currentComponentSchema}
/>
)}
</Wrapper>
);

View File

@ -28,9 +28,10 @@ const NonRepeatableComponent = ({ fields, name, schema }) => {
return (
<FieldComponent
key={field.name}
name={keys}
label={metas.label}
componentUid={componentUid}
isRepeatable={currentField.repeatable}
label={metas.label}
name={keys}
/>
);
}

View File

@ -0,0 +1,39 @@
import styled, { css } from 'styled-components';
const Button = styled.button`
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;
background-color: #fff;
> i {
margin-right: 10px;
}
`;
export default Button;

View File

@ -0,0 +1,54 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import { Grab } from '@buffetjs/icons';
import pluginId from '../../pluginId';
import BannerWrapper from './BannerWrapper';
import CarretTop from './CarretTop';
const Banner = ({ displayedValue, isOpen, onClickToggle }) => {
return (
<BannerWrapper type="button" isOpen={isOpen} onClick={onClickToggle}>
<span className="img-wrapper">
<CarretTop />
</span>
<FormattedMessage
id={`${pluginId}.containers.Edit.pluginHeader.title.new`}
>
{msg => {
return <span>{displayedValue || msg}</span>;
}}
</FormattedMessage>
<div className="cta-wrapper">
<span
className="trash-icon"
style={{ marginRight: 13 }}
// onClick={removeField}
>
<i className="fa fa-trash" />
</span>
<span className="grab">
<Grab />
</span>
</div>
</BannerWrapper>
);
};
Banner.defaultProps = {
displayedValue: null,
isOpen: false,
};
Banner.propTypes = {
displayedValue: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
isOpen: PropTypes.bool,
onClickToggle: PropTypes.func.isRequired,
};
export default Banner;

View File

@ -0,0 +1,106 @@
import styled from 'styled-components';
const BannerWrapper = styled.button`
display: flex;
height: 36px;
width: 100%;
padding: 0 15px;
border: 1px solid
${({ hasErrors, isOpen }) => {
if (hasErrors) {
return '#FFA784';
} else if (isOpen) {
return '#AED4FB';
} else {
return 'rgba(227, 233, 243, 0.75)';
}
}};
${({ isFirst }) => {
if (isFirst) {
return `
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 `
color: #f64d0a;
font-weight: 600;
`;
}
if (isOpen) {
return `
color: #007eff;
font-weight: 600;
`;
}
}}
${({ isOpen }) => {
if (isOpen) {
return `
&.trash-icon i {
color: #007eff;
}
`;
}
}}
span, div, button {
line-height: 34px;
}
.img-wrapper {
width: 19px;
height: 19px;
align-self: center;
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)'}
}
.cta-wrapper {
margin-left: auto;
> button {
padding: 0;
}
.grab {
cursor: move;
}
}
webkit-font-smoothing: antialiased;
`;
export default BannerWrapper;

View File

@ -0,0 +1,15 @@
import React from 'react';
const CarretTop = () => {
return (
<svg width="7" height="5" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 .469c0 .127.043.237.13.33l3.062 3.28a.407.407 0 0 0 .616 0L6.87.8A.467.467 0 0 0 7 .468a.467.467 0 0 0-.13-.33A.407.407 0 0 0 6.563 0H.438A.407.407 0 0 0 .13.14.467.467 0 0 0 0 .468z"
fill="#007EFF"
fillRule="nonzero"
/>
</svg>
);
};
export default CarretTop;

View File

@ -0,0 +1,99 @@
import React from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { Collapse } from 'reactstrap';
import useDataManager from '../../hooks/useDataManager';
import Inputs from '../Inputs';
import FieldComponent from '../FieldComponent';
import Banner from './Banner';
import FormWrapper from './FormWrapper';
const DraggedItem = ({
componentFieldName,
fields,
isOpen,
onClickToggle,
schema,
}) => {
const { modifiedData } = useDataManager();
const mainField = get(schema, ['settings', 'mainField'], 'id');
const displayedValue = get(
modifiedData,
[...componentFieldName.split('.'), mainField],
null
);
const getField = fieldName =>
get(schema, ['schema', 'attributes', fieldName], {});
const getMeta = fieldName =>
get(schema, ['metadatas', fieldName, 'edit'], {});
console.log({ fields });
return (
<>
<Banner
displayedValue={displayedValue}
isOpen={isOpen}
onClickToggle={onClickToggle}
/>
<Collapse isOpen={isOpen} style={{ backgroundColor: '#FAFAFB' }}>
<FormWrapper isOpen={isOpen}>
{fields.map((fieldRow, key) => {
return (
<div className="row" key={key}>
{fieldRow.map(field => {
const currentField = getField(field.name);
const isComponent =
get(currentField, 'type', '') === 'component';
const keys = `${componentFieldName}.${field.name}`;
if (isComponent) {
const componentUid = currentField.component;
const metas = getMeta(field.name);
console.log({ componentUid, currentField });
return (
<FieldComponent
componentUid={componentUid}
isRepeatable={currentField.repeatable}
key={field.name}
label={metas.label}
name={keys}
/>
);
}
return (
<div key={field.name} className={`col-${field.size}`}>
<Inputs
autoFocus={false}
keys={keys}
layout={schema}
name={field.name}
onChange={() => {}}
/>
</div>
);
})}
</div>
);
})}
</FormWrapper>
</Collapse>
</>
);
};
DraggedItem.defaultProps = {
fields: [],
isOpen: false,
};
DraggedItem.propTypes = {
componentFieldName: PropTypes.string.isRequired,
fields: PropTypes.array,
isOpen: PropTypes.bool,
onClickToggle: PropTypes.func.isRequired,
schema: PropTypes.object.isRequired,
};
export default DraggedItem;

View File

@ -0,0 +1,20 @@
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;
background-color: #fff;
> p {
color: #9ea7b8;
font-size: 13px;
font-weight: 500;
}
`;
export default EmptyComponent;

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,96 @@
import React, { useReducer } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
// import { get } from 'lodash';
import pluginId from '../../pluginId';
import useDataManager from '../../hooks/useDataManager';
import Button from './AddFieldButton';
import DraggedItem from './DraggedItem';
import EmptyComponent from './EmptyComponent';
import init from './init';
import reducer, { initialState } from './reducer';
const RepeatableComponent = ({
componentValue,
componentValueLength,
// componentUid,
fields,
name,
schema,
}) => {
const {
addRepeatableComponentToField,
// modifiedData,
// removeComponentFromField,
} = useDataManager();
const [state, dispatch] = useReducer(reducer, initialState, () =>
init(initialState, componentValue)
);
const { collapses } = state.toJS();
console.log({ state: state.toJS(), fields, schema });
return (
<div>
{componentValueLength === 0 && (
<EmptyComponent>
<FormattedMessage id={`${pluginId}.components.empty-repeatable`}>
{msg => <p>{msg}</p>}
</FormattedMessage>
</EmptyComponent>
)}
{componentValueLength > 0 &&
componentValue.map((data, index) => {
const componentFieldName = `${name}.${index}`;
console.log({ componentFieldName });
return (
<DraggedItem
fields={fields}
isOpen={collapses[index].isOpen}
key={data._temp__id}
onClickToggle={() => {
dispatch({
type: 'TOGGLE_COLLAPSE',
index,
});
}}
schema={schema}
componentFieldName={componentFieldName}
/>
);
})}
<Button
withBorderRadius={false}
type="button"
onClick={() => {
// TODO min max validations
// TODO add componentUID
addRepeatableComponentToField(name);
dispatch({
type: 'ADD_NEW_FIELD',
});
}}
>
<i className="fa fa-plus" />
<FormattedMessage id={`${pluginId}.containers.EditView.add.new`} />
</Button>
</div>
);
};
RepeatableComponent.defaultProps = {
componentValue: null,
componentValueLength: 0,
fields: [],
};
RepeatableComponent.propTypes = {
componentValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
componentValueLength: PropTypes.number,
fields: PropTypes.array,
name: PropTypes.string.isRequired,
schema: PropTypes.object.isRequired,
};
export default RepeatableComponent;

View File

@ -0,0 +1,20 @@
import { fromJS } from 'immutable';
import { isArray } from 'lodash';
// Initialize all the fields of the component is the isOpen key to false
// The key will be used to control the open close state of the banner
function init(initialState, componentValue) {
return initialState.update('collapses', list => {
if (isArray(componentValue)) {
return fromJS(
componentValue.map(() => ({
isOpen: false,
}))
);
}
return list;
});
}
export default init;

View File

@ -0,0 +1,29 @@
import { fromJS } from 'immutable';
const initialState = fromJS({ collapses: [] });
const 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 'TOGGLE_COLLAPSE':
return state.update('collapses', list => {
return list.map((obj, index) => {
if (index === action.index) {
return obj.update('isOpen', v => !v);
}
return obj.update('isOpen', () => false);
});
});
default:
return state;
}
};
export default reducer;
export { initialState };

View File

@ -189,6 +189,8 @@ const EditViewDataManagerProvider = ({
const showLoader = !isCreatingEntry && isLoading;
console.log({ modifiedData });
return (
<EditViewDataManagerContext.Provider
value={{