From cb284d97a34b1fdaa4e7bad67b8c66709bcf32b0 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Tue, 17 Mar 2020 18:17:00 -0400 Subject: [PATCH] adds the start of dnd functionality --- package.json | 1 + .../components/controls/IconButton/index.js | 18 +-- src/client/components/forms/Form/reducer.js | 41 ++++-- .../Repeater/RepeaterSection/index.js | 118 +++++++++++------- .../Repeater/RepeaterSection/index.scss | 4 - .../forms/field-types/Repeater/index.js | 87 +++++++++---- .../forms/field-types/Repeater/index.scss | 7 ++ src/client/components/layout/Section/index.js | 22 ++-- .../components/layout/Section/index.scss | 4 +- 9 files changed, 202 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 85bb78d0cd..f0e29334a8 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "qs": "^6.9.1", "react": "^16.12.0", "react-animate-height": "^2.0.20", + "react-beautiful-dnd": "^13.0.0", "react-document-meta": "^3.0.0-beta.2", "react-dom": "^16.12.0", "react-router-dom": "^5.1.2", diff --git a/src/client/components/controls/IconButton/index.js b/src/client/components/controls/IconButton/index.js index 83c257cf3c..3198fbcf1f 100644 --- a/src/client/components/controls/IconButton/index.js +++ b/src/client/components/controls/IconButton/index.js @@ -9,7 +9,7 @@ import './index.scss'; const baseClass = 'icon-button'; -const IconButton = ({ iconName, className, ...rest }) => { +const IconButton = React.forwardRef(({ iconName, className, ...rest }, ref) => { const classes = [ baseClass, className && className, @@ -25,14 +25,16 @@ const IconButton = ({ iconName, className, ...rest }) => { const Icon = icons[iconName] || icons.arrow; return ( - + + + ); -}; +}); IconButton.defaultProps = { className: '', diff --git a/src/client/components/forms/Form/reducer.js b/src/client/components/forms/Form/reducer.js index 2576f6c9c4..ecc9e26dd3 100644 --- a/src/client/components/forms/Form/reducer.js +++ b/src/client/components/forms/Form/reducer.js @@ -4,22 +4,22 @@ const splitRowsFromState = (state, name) => { // Take a copy of state const remainingState = { ...state }; - const rowObject = {}; + const rowsFromStateObject = {}; // Loop over all keys from state // If the key begins with the name of the parent field, - // Add value to rowObject and delete it from remaining state + // Add value to rowsFromStateObject and delete it from remaining state Object.keys(state).forEach((key) => { if (key.indexOf(`${name}.`) === 0) { - rowObject[key] = state[key]; + rowsFromStateObject[key] = state[key]; delete remainingState[key]; } }); - const rows = unflatten(rowObject); + const rowsFromState = unflatten(rowsFromStateObject); return { - rows: rows[name] || [], + rowsFromState: rowsFromState[name] || [], remainingState, }; }; @@ -33,29 +33,46 @@ function fieldReducer(state, action) { case 'REMOVE_ROW': { const { rowIndex, name } = action; - const { rows, remainingState } = splitRowsFromState(state, name); + const { rowsFromState, remainingState } = splitRowsFromState(state, name); - rows.splice(rowIndex, 1); + rowsFromState.splice(rowIndex, 1); return { ...remainingState, - ...(flatten({ [name]: rows }, { maxDepth: 3 })), + ...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })), }; } case 'ADD_ROW': { const { rowIndex, name, fields } = action; - const { rows, remainingState } = splitRowsFromState(state, name); + const { rowsFromState, remainingState } = splitRowsFromState(state, name); // Get names of sub fields const subFields = fields.reduce((acc, field) => ({ ...acc, [field.name]: {} }), {}); - // Add new object containing subfield names to rows array - rows.splice(rowIndex + 1, 0, subFields); + // Add new object containing subfield names to rowsFromState array + rowsFromState.splice(rowIndex + 1, 0, subFields); return { ...remainingState, - ...(flatten({ [name]: rows }, { maxDepth: 3 })), + ...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })), + }; + } + + case 'MOVE_ROW': { + const { moveFromIndex, moveToIndex, name } = action; + const { rowsFromState, remainingState } = splitRowsFromState(state, name); + + // copy the row to move + const copyOfMovingRow = rowsFromState[moveFromIndex]; + // delete the row by index + rowsFromState.splice(moveFromIndex, 1); + // insert row copyOfMovingRow back in + rowsFromState.splice(moveToIndex, 0, copyOfMovingRow); + + return { + ...remainingState, + ...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })), }; } diff --git a/src/client/components/forms/field-types/Repeater/RepeaterSection/index.js b/src/client/components/forms/field-types/Repeater/RepeaterSection/index.js index bcc70ef1b0..d504bb4052 100644 --- a/src/client/components/forms/field-types/Repeater/RepeaterSection/index.js +++ b/src/client/components/forms/field-types/Repeater/RepeaterSection/index.js @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import AnimateHeight from 'react-animate-height'; +import { Draggable } from 'react-beautiful-dnd'; import RenderFields from '../../../RenderFields'; import IconButton from '../../../../controls/IconButton'; @@ -11,63 +12,89 @@ import './index.scss'; const baseClass = 'repeater-section'; const RepeaterSection = ({ - addRow, removeRow, rowIndex, parentName, fields, fieldState, + addRow, removeRow, rowIndex, parentName, fields, fieldState, forceContentCollapse, }) => { const [isSectionOpen, setIsSectionOpen] = useState(true); + useEffect(() => { + if (forceContentCollapse) setIsSectionOpen(false); + }, [forceContentCollapse]); + return ( -
-
- - {parentName} - -

Title Goes Here

+ {(providedDrag, snapshot) => ( +
+
+ + {parentName} + +

Title Goes Here

-
- +
- + - setIsSectionOpen(state => !state)} - size="small" - /> + + + + + setIsSectionOpen(state => !state)} + size="small" + /> +
+
+ + + { + const fieldName = `${parentName}.${rowIndex}.${field.name}`; + return ({ + ...field, + name: fieldName, + defaultValue: fieldState?.[fieldName]?.value, + }); + })} + /> +
-
- - { - const fieldName = `${parentName}.${rowIndex}.${field.name}`; - return ({ - ...field, - name: fieldName, - defaultValue: fieldState?.[fieldName]?.value, - }); - })} - /> - -
+ )} + ); }; +RepeaterSection.defaultProps = { + forceContentCollapse: false, +}; + RepeaterSection.propTypes = { addRow: PropTypes.func.isRequired, removeRow: PropTypes.func.isRequired, @@ -75,6 +102,7 @@ RepeaterSection.propTypes = { parentName: PropTypes.string.isRequired, fields: PropTypes.arrayOf(PropTypes.shape({})).isRequired, fieldState: PropTypes.shape({}).isRequired, + forceContentCollapse: PropTypes.bool, }; export default RepeaterSection; diff --git a/src/client/components/forms/field-types/Repeater/RepeaterSection/index.scss b/src/client/components/forms/field-types/Repeater/RepeaterSection/index.scss index 7f47121f86..ae74be73bf 100644 --- a/src/client/components/forms/field-types/Repeater/RepeaterSection/index.scss +++ b/src/client/components/forms/field-types/Repeater/RepeaterSection/index.scss @@ -6,10 +6,6 @@ position: relative; border: $stroke-width solid lighten($gray, 20%); - + .repeater-section { - margin-top: base(.5); - } - &__collapse__icon { svg { transition: 150ms linear; diff --git a/src/client/components/forms/field-types/Repeater/index.js b/src/client/components/forms/field-types/Repeater/index.js index f1749f3b61..d43b6e3e6c 100644 --- a/src/client/components/forms/field-types/Repeater/index.js +++ b/src/client/components/forms/field-types/Repeater/index.js @@ -1,5 +1,6 @@ import React, { useContext, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { DragDropContext, Droppable } from 'react-beautiful-dnd'; import FormContext from '../../Form/Context'; import Section from '../../../layout/Section'; @@ -13,6 +14,7 @@ const Repeater = (props) => { const [rowCount, setRowCount] = useState(0); const formContext = useContext(FormContext); const { fields: fieldState, dispatchFields } = formContext; + const [forceContentCollapse, setForceContentCollapse] = useState(false); const { label, @@ -37,32 +39,75 @@ const Repeater = (props) => { setRowCount(rowCount - 1); }; + const moveRow = (rowIndex, moveToIndex) => { + dispatchFields({ + type: 'MOVE_ROW', rowIndex, moveToIndex, name, + }); + }; + useEffect(() => { setRowCount(defaultValue.length); }, [defaultValue]); - return ( -
-
- {rowCount > 0 && Array.from(Array(rowCount).keys()).map((_, rowIndex) => { - return ( - addRow(rowIndex)} - removeRow={() => removeRow(rowIndex)} - rowIndex={rowIndex} - fieldState={fieldState} - fields={fields} - parentName={name} - /> - ); - })} + function onBeforeCapture(result) { + setForceContentCollapse(true); + } -
-
+ function onDragEnd(result) { + if (!result.destination) { + return; + } + + const moveFromIndex = result.source.index; + const moveToIndex = result.destination.index; + dispatchFields({ + type: 'MOVE_ROW', moveFromIndex, moveToIndex, name, + }); + } + + return ( + +
+
addRow(0)} + > + {rowCount !== 0 + && ( + + {(provided) => { + return Array.from(Array(rowCount).keys()).map((_, rowIndex) => { + return ( +
+ addRow(rowIndex)} + removeRow={() => removeRow(rowIndex)} + moveRow={() => moveRow(rowIndex)} + rowIndex={rowIndex} + fieldState={fieldState} + fields={fields} + parentName={name} + forceContentCollapse={forceContentCollapse} + /> +
+ ); + }); + }} +
+ )} +
+
+
); }; diff --git a/src/client/components/forms/field-types/Repeater/index.scss b/src/client/components/forms/field-types/Repeater/index.scss index 3a3f6096a2..4c8ba1d688 100644 --- a/src/client/components/forms/field-types/Repeater/index.scss +++ b/src/client/components/forms/field-types/Repeater/index.scss @@ -1,4 +1,11 @@ +@import '../../../../scss/styles.scss'; + .field-repeater { + &__row { + + .field-repeater__row { + margin-top: base(.5); + } + } .content { flex-direction: column; } diff --git a/src/client/components/layout/Section/index.js b/src/client/components/layout/Section/index.js index 52b7d527af..57306ce5f8 100644 --- a/src/client/components/layout/Section/index.js +++ b/src/client/components/layout/Section/index.js @@ -1,14 +1,17 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import AnimateHeight from 'react-animate-height'; +import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import './index.scss'; import IconButton from '../../controls/IconButton'; const baseClass = 'section'; -const Section = (props) => { - const { className, heading, children } = props; +const Section = React.forwardRef((props, ref) => { + const { + className, heading, children, rowCount, addInitialRow, + } = props; const classes = [ baseClass, @@ -17,18 +20,21 @@ const Section = (props) => { const [isSectionOpen, setIsSectionOpen] = useState(true); + const iconProps = {}; + iconProps.iconName = `${rowCount === 0 ? 'crosshair' : 'arrow'}`; + iconProps.onClick = rowCount === 0 ? () => addInitialRow() : () => setIsSectionOpen(state => !state); + return (
{heading && ( -
+

{heading}

setIsSectionOpen(state => !state)} + {...iconProps} />
@@ -40,14 +46,12 @@ const Section = (props) => { height={isSectionOpen ? 'auto' : 0} duration={150} > -
- {children} -
+ {children} )}
); -}; +}); Section.defaultProps = { className: '', diff --git a/src/client/components/layout/Section/index.scss b/src/client/components/layout/Section/index.scss index f59165de66..846e70b1f6 100644 --- a/src/client/components/layout/Section/index.scss +++ b/src/client/components/layout/Section/index.scss @@ -28,7 +28,9 @@ section.section { } &__content { - @include pad; + > div { + @include pad; + } } &__controls {