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 (
);
-};
+});
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 {