mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 09:56:44 +00:00 
			
		
		
		
	Created Repeatable component with nested component support
This commit is contained in:
		
							parent
							
								
									e51bdba352
								
							
						
					
					
						commit
						fc6bbbfadc
					
				| @ -11,6 +11,11 @@ | ||||
|     }, | ||||
|     "time": { | ||||
|       "type": "string" | ||||
|     }, | ||||
|     "dish": { | ||||
|       "component": "default.dish", | ||||
|       "type": "component", | ||||
|       "repeatable": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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> | ||||
|   ); | ||||
|  | ||||
| @ -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} | ||||
|                   /> | ||||
|                 ); | ||||
|               } | ||||
|  | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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 }; | ||||
| @ -189,6 +189,8 @@ const EditViewDataManagerProvider = ({ | ||||
| 
 | ||||
|   const showLoader = !isCreatingEntry && isLoading; | ||||
| 
 | ||||
|   console.log({ modifiedData }); | ||||
| 
 | ||||
|   return ( | ||||
|     <EditViewDataManagerContext.Provider | ||||
|       value={{ | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 soupette
						soupette