refactor: merges the FlexibleRow and RepeaterRow into a single DraggableSection component

This commit is contained in:
Jarrod Flesch
2020-03-27 15:48:02 -04:00
parent d64e690c9b
commit dfd2af1499
9 changed files with 112 additions and 279 deletions

View File

@@ -3,16 +3,26 @@ 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';
import RenderFields from '../RenderFields'; // eslint-disable-line import/no-cycle
import IconButton from '../../controls/IconButton';
import './index.scss';
const baseClass = 'repeater-row';
const baseClass = 'draggable-section';
const DraggableSection = (props) => {
const {
addRow,
removeRow,
rowIndex,
parentName,
renderFields,
defaultValue,
dispatchCollapsibleStates,
collapsibleStates,
singularName,
} = props;
const RepeaterRow = ({
addRow, removeRow, rowIndex, parentName, fields, defaultValue, dispatchCollapsibleStates, collapsibleStates,
}) => {
const handleCollapseClick = () => {
dispatchCollapsibleStates({
type: 'UPDATE_COLLAPSIBLE_STATUS',
@@ -34,12 +44,15 @@ const RepeaterRow = ({
>
<div className={`${baseClass}__header`}>
<div
{...providedDrag.dragHandleProps}
className={`${baseClass}__header__drag-handle`}
{...providedDrag.dragHandleProps}
onClick={handleCollapseClick}
role="button"
tabIndex={0}
/>
<div className={`${baseClass}__header__row-index`}>
{`${rowIndex + 1 > 9 ? rowIndex + 1 : `0${rowIndex + 1}`}`}
{`${singularName} ${rowIndex + 1}`}
</div>
<div className={`${baseClass}__header__controls`}>
@@ -55,13 +68,6 @@ const RepeaterRow = ({
onClick={removeRow}
size="small"
/>
<IconButton
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${collapsibleStates[rowIndex] ? 'open' : 'closed'}`}
iconName="arrow"
onClick={handleCollapseClick}
size="small"
/>
</div>
</div>
@@ -72,7 +78,7 @@ const RepeaterRow = ({
>
<RenderFields
key={rowIndex}
fields={fields.map((field) => {
fields={renderFields.map((field) => {
const fieldName = `${parentName}.${rowIndex}.${field.name}`;
return ({
...field,
@@ -89,22 +95,24 @@ const RepeaterRow = ({
);
};
RepeaterRow.defaultProps = {
DraggableSection.defaultProps = {
rowCount: null,
defaultValue: null,
collapsibleStates: [],
singularName: '',
};
RepeaterRow.propTypes = {
DraggableSection.propTypes = {
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
rowIndex: PropTypes.number.isRequired,
parentName: PropTypes.string.isRequired,
fields: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
singularName: PropTypes.string,
renderFields: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
rowCount: PropTypes.number,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.shape({})]),
dispatchCollapsibleStates: PropTypes.func.isRequired,
collapsibleStates: PropTypes.arrayOf(PropTypes.bool),
};
export default RepeaterRow;
export default DraggableSection;

View File

@@ -1,11 +1,17 @@
@import '../../../../../scss/styles.scss';
@import '../../../scss/styles.scss';
.flexible-row {
.draggable-section {
background: $light-gray;
flex-direction: column;
position: relative;
margin-top: base(.5);
.field-type,
.missing-field {
padding-left: 0;
padding-right: 0;
}
&:hover {
@include shadow-sm;
}
@@ -74,7 +80,18 @@
@include color-svg($primary);
&:hover {
background: lighten($primary, 50%);
background: $primary;
@include color-svg($black);
}
}
.icon-button--crossOut {
border-color: $black;
@include color-svg($black);
&:hover {
background: $black;
@include color-svg(white);
}
}
}

View File

@@ -1,125 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import AnimateHeight from 'react-animate-height';
import { Draggable } from 'react-beautiful-dnd';
// eslint-disable-next-line import/no-cycle
import RenderFields from '../../../RenderFields';
import IconButton from '../../../../controls/IconButton';
import './index.scss';
const baseClass = 'flexible-row';
const FlexibleRow = (props) => {
const {
addRow,
removeRow,
rowIndex,
parentName,
block,
defaultValue,
dispatchCollapsibleStates,
collapsibleStates,
} = props;
const handleCollapseClick = () => {
dispatchCollapsibleStates({
type: 'UPDATE_COLLAPSIBLE_STATUS',
collapsibleIndex: rowIndex,
});
};
return (
<Draggable
draggableId={`row-${rowIndex}`}
index={rowIndex}
>
{(providedDrag) => {
return (
<div
ref={providedDrag.innerRef}
className={baseClass}
{...providedDrag.draggableProps}
>
<div className={`${baseClass}__header`}>
<div
{...providedDrag.dragHandleProps}
className={`${baseClass}__header__drag-handle`}
/>
<div className={`${baseClass}__header__row-index`}>
{`${block.labels.singular} ${rowIndex + 1 > 9 ? rowIndex + 1 : `0${rowIndex + 1}`}`}
</div>
<div className={`${baseClass}__header__controls`}>
<IconButton
iconName="crosshair"
onClick={addRow}
size="small"
/>
<IconButton
iconName="crossOut"
onClick={removeRow}
size="small"
/>
<IconButton
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${collapsibleStates[rowIndex] ? 'open' : 'closed'}`}
iconName="arrow"
onClick={handleCollapseClick}
size="small"
/>
</div>
</div>
<AnimateHeight
className={`${baseClass}__content`}
height={collapsibleStates[rowIndex] ? 'auto' : 0}
duration={0}
>
<RenderFields
key={rowIndex}
fields={block.fields.map((field) => {
const fieldName = `${parentName}.${rowIndex}.${field.name}`;
return ({
...field,
name: fieldName,
defaultValue: defaultValue?.[field.name],
});
})}
/>
</AnimateHeight>
</div>
);
}}
</Draggable>
);
};
FlexibleRow.defaultProps = {
defaultValue: null,
collapsibleStates: [],
};
FlexibleRow.propTypes = {
block: PropTypes.shape({
labels: PropTypes.shape({
singular: PropTypes.string,
}),
fields: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
slug: PropTypes.string,
}).isRequired,
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
rowIndex: PropTypes.number.isRequired,
parentName: PropTypes.string.isRequired,
fieldState: PropTypes.shape({}).isRequired,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.shape({})]),
dispatchCollapsibleStates: PropTypes.func.isRequired,
collapsibleStates: PropTypes.arrayOf(PropTypes.bool),
};
export default FlexibleRow;

View File

@@ -7,9 +7,9 @@ import { ModalContext } from '@trbl/react-modal';
import FormContext from '../../Form/Context';
import Section from '../../../layout/Section';
import FlexibleRow from './FlexibleRow'; // eslint-disable-line import/no-cycle
import AddRowModal from './AddRowModal';
import collapsibleReducer from './reducer';
import DraggableSection from '../../DraggableSection'; // eslint-disable-line import/no-cycle
import './index.scss';
@@ -113,14 +113,14 @@ const Flexible = (props) => {
const blockToRender = blocks.find(block => block.slug === blockType);
return (
<FlexibleRow
<DraggableSection
key={rowIndex}
parentName={name}
addRow={() => openAddRowModal(rowIndex)}
removeRow={() => removeRow(rowIndex)}
rowIndex={rowIndex}
fieldState={fieldState}
block={blockToRender}
renderFields={blockToRender.fields}
defaultValue={defaultValue[rowIndex]}
dispatchCollapsibleStates={dispatchCollapsibleStates}
collapsibleStates={collapsibleStates}

View File

@@ -1,90 +0,0 @@
@import '../../../../../scss/styles.scss';
.repeater-row {
background: $light-gray;
flex-direction: column;
position: relative;
margin-top: base(.5);
&:hover {
@include shadow-sm;
}
&__collapse__icon {
&--open {
svg {
transform: rotate(90deg);
}
}
&--closed {
svg {
transform: rotate(-90deg);
}
}
}
&__header {
padding: base(.75) base(1);
display: flex;
align-items: center;
position: relative;
&__drag-handle {
width: 100%;
height: 100%;
position: absolute;
left: 0;
z-index: 1;
}
// elements above the drag handle
&__controls,
&__header__row-index {
position: relative;
z-index: 2;
}
&__row-index {
font-family: $font-body;
font-size: base(.5);
}
&__heading {
font-family: $font-body;
margin: 0;
font-size: base(.65);
}
&__controls {
margin-left: auto;
.btn {
margin-top: 0;
margin-bottom: 0;
}
.icon-button--crossOut,
.icon-button--crosshair {
margin-right: base(.25);
}
.icon-button--crosshair {
border-color: $primary;
@include color-svg($primary);
&:hover {
background: lighten($primary, 50%);
}
}
}
}
&__content {
box-shadow: inset 0px 1px 0px white;
> div {
padding: base(.75) base(1);
}
}
}

View File

@@ -6,7 +6,7 @@ import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import FormContext from '../../Form/Context';
import Section from '../../../layout/Section';
import RepeaterRow from './RepeaterRow'; // eslint-disable-line import/no-cycle
import DraggableSection from '../../DraggableSection'; // eslint-disable-line import/no-cycle
import collapsibleReducer from './reducer';
import './index.scss';
@@ -24,6 +24,7 @@ const Repeater = (props) => {
name,
fields,
defaultValue,
singularName,
} = props;
const addRow = (rowIndex) => {
@@ -82,7 +83,6 @@ const Repeater = (props) => {
<div className={baseClass}>
<Section
heading={label}
className="repeater"
rowCount={rowCount}
addRow={() => addRow(0)}
useAddRowButton
@@ -94,22 +94,24 @@ const Repeater = (props) => {
{...provided.droppableProps}
>
{rowCount !== 0
&& Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
return (
<RepeaterRow
key={rowIndex}
parentName={name}
addRow={() => addRow(rowIndex)}
removeRow={() => removeRow(rowIndex)}
rowIndex={rowIndex}
fields={fields}
rowCount={rowCount}
defaultValue={defaultValue[rowIndex]}
dispatchCollapsibleStates={dispatchCollapsibleStates}
collapsibleStates={collapsibleStates}
/>
);
})
&& Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
return (
<DraggableSection
key={rowIndex}
parentName={name}
singularName={singularName}
addRow={() => addRow(rowIndex)}
removeRow={() => removeRow(rowIndex)}
rowIndex={rowIndex}
fieldState={fieldState}
renderFields={fields}
rowCount={rowCount}
defaultValue={defaultValue[rowIndex]}
dispatchCollapsibleStates={dispatchCollapsibleStates}
collapsibleStates={collapsibleStates}
/>
);
})
}
{provided.placeholder}
</div>
@@ -124,6 +126,7 @@ const Repeater = (props) => {
Repeater.defaultProps = {
label: '',
singularName: '',
defaultValue: [],
};
@@ -135,6 +138,7 @@ Repeater.propTypes = {
PropTypes.shape({}),
).isRequired,
label: PropTypes.string,
singularName: PropTypes.string,
name: PropTypes.string.isRequired,
};

View File

@@ -1,3 +1,5 @@
@import '../../../../scss/styles.scss';
.field-type.repeater {
background: white;
}

View File

@@ -4,6 +4,7 @@ import AnimateHeight from 'react-animate-height';
import './index.scss';
import IconButton from '../../controls/IconButton';
import Button from '../../controls/Button';
const baseClass = 'section';
@@ -28,25 +29,13 @@ const Section = (props) => {
<section className={classes}>
{heading
&& (
<header>
<header
className={`${baseClass}__collapsible-header`}
onClick={() => setIsSectionOpen(state => !state)}
role="button"
tabIndex={0}
>
<h2 className={`${baseClass}__heading`}>{heading}</h2>
<div className={`${baseClass}__controls`}>
{(rowCount === 0 && useAddRowButton)
&& (
<IconButton
className={`${baseClass}__add-row-button`}
size="small"
iconName="crosshair"
onClick={() => addInitialRow()}
/>
)}
<IconButton
className={`${baseClass}__collapse-icon ${baseClass}__collapse-icon--${isSectionOpen ? 'open' : 'closed'}`}
size="small"
iconName="arrow"
onClick={() => setIsSectionOpen(state => !state)}
/>
</div>
</header>
)}
{children
@@ -54,8 +43,19 @@ const Section = (props) => {
<AnimateHeight
className={`${baseClass}__content ${baseClass}__content--is-${isSectionOpen ? 'open' : 'closed'}`}
height={isSectionOpen ? 'auto' : 0}
duration={150}
duration={0}
>
{(rowCount === 0 && useAddRowButton)
&& (
<div className={`${baseClass}__add-button-wrap`}>
<Button
onClick={addInitialRow}
type="secondary"
>
Add Row
</Button>
</div>
)}
{children}
</AnimateHeight>
)}

View File

@@ -3,18 +3,35 @@
section.section {
@include shadow;
margin: base(1) 0;
transition: 300ms ease;
header {
&:hover {
box-shadow: 0 22px 65px rgba(0,0,0,.15);
}
.section__collapsible-header {
border-bottom: 1px solid $light-gray;
display: flex;
align-items: center;
@include pad;
outline: 0;
&:hover {
cursor: pointer;
}
* {
margin-bottom: 0;
}
}
.section__add-button-wrap {
.btn {
margin: 0;
margin-top: base(.5);
}
}
&__content {
display: flex;
flex-wrap: wrap;