merges repeater-design, implements field policy config in admin panel

This commit is contained in:
James
2020-07-01 23:57:07 -04:00
41 changed files with 1131 additions and 454 deletions

View File

@@ -13,11 +13,11 @@ const policies = async (args) => {
const isLoggedIn = !!(user);
const userCollectionConfig = (user && user.collection) ? config.collections.find(collection => collection.slug === user.collection) : null;
const createPolicyPromise = async (obj, policy, operation) => {
const createPolicyPromise = async (obj, policy, operation, disableWhere = false) => {
const updatedObj = obj;
const result = await policy({ req });
if (typeof result === 'object') {
if (typeof result === 'object' && !disableWhere) {
updatedObj[operation] = {
permission: true,
where: result,
@@ -37,7 +37,7 @@ const policies = async (args) => {
if (!updatedObj[field.name]) updatedObj[field.name] = {};
if (field.policies && typeof field.policies[operation] === 'function') {
promises.push(createPolicyPromise(updatedObj[field.name], field.policies[operation], operation));
promises.push(createPolicyPromise(updatedObj[field.name], field.policies[operation], operation, true));
} else {
updatedObj[field.name][operation] = {
permission: isLoggedIn,

View File

@@ -0,0 +1,9 @@
<svg width="82" height="53" viewBox="0 0 82 53" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="0.713013" width="80.574" height="52.7791" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0" transform="scale(0.00387597 0.00591716)"/>
</pattern>
<image id="image0" width="258" height="169" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import './index.scss';
const baseClass = 'popup-button';
const PopupButton = (props) => {
const {
buttonType,
button,
setActive,
active,
} = props;
const classes = [
baseClass,
`${baseClass}--${buttonType}`,
].filter(Boolean).join(' ');
if (buttonType === 'custom') {
return (
<div
role="button"
tabIndex="0"
onClick={() => setActive(!active)}
className={classes}
>
{button}
</div>
);
}
return (
<button
type="button"
onClick={() => setActive(!active)}
className={classes}
>
{button}
</button>
);
};
PopupButton.defaultProps = {
buttonType: null,
};
PopupButton.propTypes = {
buttonType: PropTypes.oneOf(['custom', 'default']),
button: PropTypes.node.isRequired,
setActive: PropTypes.func.isRequired,
active: PropTypes.bool.isRequired,
};
export default PopupButton;

View File

@@ -0,0 +1,5 @@
@import '../../../../scss/styles.scss';
.popup-button {
display: inline-flex;
}

View File

@@ -2,48 +2,27 @@ import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useWindowInfo } from '@trbl/react-window-info';
import { useScrollInfo } from '@trbl/react-scroll-info';
import useThrottledEffect from '../../../hooks/useThrottledEffect';
import PopupButton from './PopupButton';
import './index.scss';
const baseClass = 'popup';
const ClickableButton = ({
buttonType, button, setActive, active,
}) => {
if (buttonType === 'custom') {
return (
<div
role="button"
tabIndex="0"
onClick={() => setActive(!active)}
>
{button}
</div>
);
}
return (
<button
type="button"
onClick={() => setActive(!active)}
>
{button}
</button>
);
};
const Popup = (props) => {
const {
render, align, size, color, pointerAlignment, button, buttonType, children, showOnHover,
render, align, size, color, button, buttonType, children, showOnHover, horizontalAlign,
} = props;
const [active, setActive] = useState(false);
const [verticalAlign, setVerticalAlign] = useState('top');
const { height: windowHeight } = useWindowInfo();
const { y: scrollY } = useScrollInfo();
const buttonRef = useRef(null);
const contentRef = useRef(null);
const [active, setActive] = useState(false);
const [verticalAlign, setVerticalAlign] = useState('top');
const [forceHorizontalAlign, setForceHorizontalAlign] = useState(null);
const { y: scrollY } = useScrollInfo();
const { height: windowHeight } = useWindowInfo();
const handleClickOutside = (e) => {
if (contentRef.current.contains(e.target)) {
@@ -55,10 +34,29 @@ const Popup = (props) => {
useThrottledEffect(() => {
if (contentRef.current && buttonRef.current) {
const { height: contentHeight } = contentRef.current.getBoundingClientRect();
const { y: buttonOffsetTop } = buttonRef.current.getBoundingClientRect();
const {
height: contentHeight,
width: contentWidth,
right: contentRightEdge,
} = contentRef.current.getBoundingClientRect();
const { y: buttonYCoord } = buttonRef.current.getBoundingClientRect();
if (buttonOffsetTop > contentHeight) {
const windowWidth = window.innerWidth;
const distanceToRightEdge = windowWidth - contentRightEdge;
const distanceToLeftEdge = contentRightEdge - contentWidth;
if (horizontalAlign === 'left' && distanceToRightEdge <= 0) {
setForceHorizontalAlign('right');
} else if (horizontalAlign === 'right' && distanceToLeftEdge <= 0) {
setForceHorizontalAlign('left');
} else if (horizontalAlign === 'center' && (distanceToLeftEdge <= contentWidth / 2 || distanceToRightEdge <= contentWidth / 2)) {
if (distanceToRightEdge > distanceToLeftEdge) setForceHorizontalAlign('left');
else setForceHorizontalAlign('right');
} else {
setForceHorizontalAlign(null);
}
if (buttonYCoord > contentHeight) {
setVerticalAlign('top');
} else {
setVerticalAlign('bottom');
@@ -83,14 +81,18 @@ const Popup = (props) => {
`${baseClass}--align-${align}`,
`${baseClass}--size-${size}`,
`${baseClass}--color-${color}`,
`${baseClass}--pointer-alignment-${pointerAlignment}`,
`${baseClass}--vertical-align-${verticalAlign}`,
`${baseClass}--v-align-${verticalAlign}`,
`${baseClass}--h-align-${horizontalAlign}`,
forceHorizontalAlign && `${baseClass}--force-h-align-${forceHorizontalAlign}`,
active && `${baseClass}--active`,
].filter(Boolean).join(' ');
return (
<div className={classes}>
<div ref={buttonRef}>
<div
ref={buttonRef}
className={`${baseClass}__wrapper`}
>
{showOnHover
? (
<div
@@ -98,7 +100,7 @@ const Popup = (props) => {
onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)}
>
<ClickableButton
<PopupButton
buttonType={buttonType}
button={button}
setActive={setActive}
@@ -107,7 +109,7 @@ const Popup = (props) => {
</div>
)
: (
<ClickableButton
<PopupButton
buttonType={buttonType}
button={button}
setActive={setActive}
@@ -136,19 +138,19 @@ Popup.defaultProps = {
align: 'center',
size: 'small',
color: 'light',
pointerAlignment: 'left',
children: undefined,
render: undefined,
buttonType: 'default',
button: undefined,
showOnHover: false,
horizontalAlign: 'left',
};
Popup.propTypes = {
render: PropTypes.func,
children: PropTypes.node,
align: PropTypes.oneOf(['left', 'center', 'right']),
pointerAlignment: PropTypes.oneOf(['left', 'center', 'right']),
horizontalAlign: PropTypes.oneOf(['left', 'center', 'right']),
size: PropTypes.oneOf(['small', 'large', 'wide']),
color: PropTypes.oneOf(['light', 'dark']),
buttonType: PropTypes.oneOf(['default', 'custom']),

View File

@@ -9,11 +9,14 @@
opacity: 0;
visibility: hidden;
z-index: $z-modal;
max-width: calc(100vw - #{$baseline});
&:after {
content: ' ';
position: absolute;
top: calc(100% - 1px);
border: 12px solid transparent;
border-top-color: white;
}
}
@@ -22,8 +25,9 @@
}
&__scroll {
width: calc(100% + #{$baseline});
padding: base(1);
overflow-y: scroll;
white-space: nowrap;
}
&:focus,
@@ -32,21 +36,12 @@
}
////////////////////////////////
// SIZES
// SIZE
////////////////////////////////
&--size-small {
.popup__content {
@include shadow-sm;
&:after {
border: 12px solid transparent;
border-top-color: white;
}
}
.popup__scroll {
padding: base(1) base(2) base(1) base(1);
}
&.popup--align-left {
@@ -60,6 +55,16 @@
}
}
&--size-large {
.popup__content {
@include shadow-lg;
}
.popup__scroll {
padding: base(1) base(1.5);
}
}
&--size-wide {
.popup__content {
@include shadow-sm;
@@ -85,29 +90,79 @@
}
}
&--color-dark {
////////////////////////////////
// HORIZONTAL ALIGNMENT
////////////////////////////////
&--h-align-left {
.popup__content {
background: $color-dark-gray;
color: white;
left: - base(1.75);
&:after {
border-top-color: $color-dark-gray;
left: base(1.75);
}
}
}
&--h-align-center {
.popup__content {
left: 50%;
transform: translateX(-50%);
&:after {
left: 50%;
transform: translateX(-50%);
}
}
}
&--h-align-right {
.popup__content {
right: - base(1.75);
&:after {
right: base(1.75);
}
}
}
&--force-h-align-left {
.popup__content {
left: - base(1.75);
&:after {
left: base(1.75);
right: unset;
transform: unset;
}
}
}
&--force-h-align-right {
.popup__content {
right: - base(1.75);
&:after {
right: base(1.75);
left: unset;
transform: unset;
}
}
}
////////////////////////////////
// VERTICAL ALIGNMENTS
// VERTICAL ALIGNMENT
////////////////////////////////
&--vertical-align-top {
&--v-align-top {
.popup__content {
bottom: calc(100% + #{$baseline});
}
}
&--vertical-align-bottom {
&--v-align-bottom {
.popup__content {
@include shadow-lg-top;
top: calc(100% + #{$baseline});
&:after {
@@ -120,16 +175,16 @@
}
////////////////////////////////
// POINTER POSITION
// COLOR
////////////////////////////////
&--pointer-alignment-center {
&--color-dark {
.popup__content {
left: 50%;
transform: translateX(-50%);
background: $color-dark-gray;
color: white;
&:after {
left: 50%;
transform: translateX(-50%);
border-top-color: $color-dark-gray;
}
}
}
@@ -144,4 +199,71 @@
visibility: visible;
}
}
@include mid-break {
&__scroll,
&--size-large .popup__scroll{
padding: base(.75);
}
&--h-align-left {
.popup__content {
left: - base(.5);
&:after {
left: base(.5);
}
}
}
&--h-align-center {
.popup__content {
left: 50%;
transform: translateX(-50%);
&:after {
left: 50%;
transform: translateX(-50%);
}
}
}
&--h-align-right {
.popup__content {
right: - base(.5);
&:after {
right: base(.5);
}
}
}
&--force-h-align-left {
.popup__content {
left: - base(.5);
right: unset;
transform: unset;
&:after {
left: base(.5);
right: unset;
transform: unset;
}
}
}
&--force-h-align-right {
.popup__content {
right: - base(.5);
left: unset;
transform: unset;
&:after {
right: base(.5);
left: unset;
transform: unset;
}
}
}
}
}

View File

@@ -1,86 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../../../elements/Button';
import Popup from '../../../elements/Popup';
import './index.scss';
const baseClass = 'action-handle';
const ActionHandle = (props) => {
const {
addRow, removeRow, singularLabel, verticalAlignment,
} = props;
const classes = [
baseClass,
`${baseClass}--vertical-alignment-${verticalAlignment}`,
].filter(Boolean).join(' ');
return (
<div className={classes}>
<div className={`${baseClass}__controls-container`}>
<div className={`${baseClass}__controls`}>
<Popup
showOnHover
size="wide"
color="dark"
pointerAlignment="center"
buttonType="custom"
button={(
<Button
className={`${baseClass}__remove-row`}
round
buttonStyle="none"
icon="x"
iconPosition="left"
iconStyle="with-border"
onClick={() => removeRow()}
/>
)}
>
Remove&nbsp;
{singularLabel}
</Popup>
<Popup
showOnHover
size="wide"
color="dark"
pointerAlignment="center"
buttonType="custom"
button={(
<Button
className={`${baseClass}__add-row`}
round
buttonStyle="none"
icon="plus"
iconPosition="left"
iconStyle="with-border"
onClick={() => addRow()}
/>
)}
>
Add&nbsp;
{singularLabel}
</Popup>
</div>
</div>
</div>
);
};
ActionHandle.defaultProps = {
singularLabel: 'Row',
verticalAlignment: 'center',
};
ActionHandle.propTypes = {
singularLabel: PropTypes.string,
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
verticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
};
export default ActionHandle;

View File

@@ -0,0 +1,130 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../../../elements/Button';
import Popup from '../../../elements/Popup';
import BlockSelector from '../../field-types/Flexible/BlockSelector';
import './index.scss';
const baseClass = 'action-panel';
const ActionPanel = (props) => {
const {
addRow,
removeRow,
singularLabel,
verticalAlignment,
blockType,
blocks,
rowIndex,
isHovered,
} = props;
const classes = [
baseClass,
`${baseClass}--vertical-alignment-${verticalAlignment}`,
].filter(Boolean).join(' ');
return (
<div className={classes}>
<div className={`${baseClass}__controls-container`}>
<div className={`${baseClass}__controls`}>
<Popup
showOnHover
size="wide"
color="dark"
horizontalAlign="center"
buttonType="custom"
button={(
<Button
className={`${baseClass}__remove-row`}
round
buttonStyle="none"
icon="x"
iconPosition="left"
iconStyle="with-border"
onClick={removeRow}
/>
)}
>
Remove&nbsp;
{singularLabel}
</Popup>
{blockType === 'flexible'
? (
<Popup
buttonType="custom"
size="large"
horizontalAlign="right"
button={(
<Button
className={`${baseClass}__add-row`}
round
buttonStyle="none"
icon="plus"
iconPosition="left"
iconStyle="with-border"
/>
)}
render={({ close }) => (
<BlockSelector
blocks={blocks}
addRow={addRow}
addRowIndex={rowIndex}
close={close}
parentIsHovered={isHovered}
watchParentHover
/>
)}
/>
)
: (
<Popup
showOnHover
size="wide"
color="dark"
horizontalAlign="right"
buttonType="custom"
button={(
<Button
className={`${baseClass}__add-row`}
round
buttonStyle="none"
icon="plus"
iconPosition="left"
iconStyle="with-border"
onClick={addRow}
/>
)}
>
Add&nbsp;
{singularLabel}
</Popup>
)
}
</div>
</div>
</div>
);
};
ActionPanel.defaultProps = {
singularLabel: 'Row',
verticalAlignment: 'center',
blockType: null,
isHovered: false,
};
ActionPanel.propTypes = {
singularLabel: PropTypes.string,
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
blockType: PropTypes.oneOf(['flexible', 'repeater']),
verticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
isHovered: PropTypes.bool,
rowIndex: PropTypes.number.isRequired,
};
export default ActionPanel;

View File

@@ -1,9 +1,13 @@
@import '../../../../scss/styles';
.action-handle {
padding: base(.5);
.action-panel {
padding: 0 base(.5);
padding-left: base(.75);
margin-bottom: base(.5);
margin-bottom: base(1);
&:hover {
z-index: $z-nav;
}
&__controls-container {
position: relative;
@@ -16,16 +20,18 @@
height: 100%;
flex-direction: column;
color: $color-gray;
opacity: 0;
visibility: hidden;
}
&--vertical-alignment-top {
.action-handle__controls {
.action-panel__controls {
justify-content: flex-start;
}
}
&--vertical-alignment-sticky {
.action-handle__controls {
.action-panel__controls {
position: sticky;
top: 120px;
height: unset;
@@ -34,11 +40,22 @@
&__remove-row {
margin: 0 0 base(.3);
opacity: 0;
}
&__add-row {
margin: base(.3) 0 0;
opacity: 0;
}
@include mid-break {
&__controls {
opacity: 1;
visibility: visible;
}
&--vertical-alignment-sticky {
.action-panel__controls {
top: 100px;
}
}
}
}

View File

@@ -5,11 +5,11 @@ import Button from '../../../elements/Button';
import './index.scss';
const baseClass = 'position-handle';
const baseClass = 'position-panel';
const PositionHandle = (props) => {
const PositionPanel = (props) => {
const {
dragHandleProps, moveRow, positionIndex, verticalAlignment,
dragHandleProps, moveRow, positionIndex, verticalAlignment, rowCount,
} = props;
const adjustedIndex = positionIndex + 1;
@@ -26,8 +26,9 @@ const PositionHandle = (props) => {
>
<div className={`${baseClass}__controls-container`}>
<div className={`${baseClass}__controls`}>
<Button
className={`${baseClass}__move-backward`}
className={`${baseClass}__move-backward ${positionIndex === 0 ? 'first-row' : ''}`}
buttonStyle="none"
icon="chevron"
round
@@ -37,27 +38,29 @@ const PositionHandle = (props) => {
<div className={`${baseClass}__current-position`}>{adjustedIndex >= 10 ? adjustedIndex : `0${adjustedIndex}`}</div>
<Button
className={`${baseClass}__move-forward`}
className={`${baseClass}__move-forward ${(positionIndex === rowCount - 1) ? 'last-row' : ''}`}
buttonStyle="none"
icon="chevron"
round
onClick={() => moveRow(positionIndex, positionIndex + 1)}
/>
</div>
</div>
</div>
);
};
PositionHandle.defaultProps = {
PositionPanel.defaultProps = {
verticalAlignment: 'center',
};
PositionHandle.propTypes = {
PositionPanel.propTypes = {
dragHandleProps: PropTypes.shape({}).isRequired,
positionIndex: PropTypes.number.isRequired,
moveRow: PropTypes.func.isRequired,
verticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
rowCount: PropTypes.number.isRequired,
};
export default PositionHandle;
export default PositionPanel;

View File

@@ -1,17 +1,19 @@
@import '../../../../scss/styles';
.position-handle {
$controls-top-adjustment: base(.1);
.position-panel {
padding-right: base(1);
margin-bottom: base(.5);
margin-bottom: base(1);
&__controls-container {
position: relative;
height: 100%;
min-height: 100%;
box-shadow: #{$style-stroke-width-s} 0px 0px 0px $color-light-gray;
}
&__controls {
padding-right: base(.75);
padding-right: base(.65);
display: flex;
flex-direction: column;
justify-content: center;
@@ -19,13 +21,13 @@
}
&--vertical-alignment-top {
.position-handle__controls {
.position-panel__controls {
justify-content: flex-start;
}
}
&--vertical-alignment-sticky {
.position-handle__controls {
.position-panel__controls {
position: sticky;
top: 120px;
height: unset;
@@ -34,12 +36,12 @@
&__move-backward {
transform: rotate(.5turn);
margin: 0 0 base(.25);
margin: 0;
opacity: 0;
}
&__move-forward {
margin: base(.25) 0 0;
margin: 0;
opacity: 0;
}
@@ -47,4 +49,26 @@
text-align: center;
color: $color-gray;
}
@include large-break {
padding-right: base(1);
&__controls {
padding-right: base(.75);
}
}
}
// External scopes
.field-type.flexible {
.position-panel {
&__controls-container {
min-height: calc(100% + #{$controls-top-adjustment});
}
&__controls {
margin-top: - $controls-top-adjustment;
}
}
}

View File

@@ -1,14 +1,15 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
// import AnimateHeight from 'react-animate-height';
import AnimateHeight from 'react-animate-height';
import { Draggable } from 'react-beautiful-dnd';
import ActionHandle from './ActionHandle';
import ActionPanel from './ActionPanel';
import SectionTitle from './SectionTitle';
import PositionHandle from './PositionHandle';
import PositionPanel from './PositionPanel';
import RenderFields from '../RenderFields';
import './index.scss';
import Button from '../../elements/Button';
const baseClass = 'draggable-section';
@@ -18,35 +19,27 @@ const DraggableSection = (props) => {
addRow,
removeRow,
rowIndex,
rowCount,
parentPath,
fieldSchema,
initialData,
// dispatchRows,
singularLabel,
blockType,
fieldTypes,
customComponentsPath,
isOpen,
id,
positionHandleVerticalAlignment,
actionHandleVerticalAlignment,
positionPanelVerticalAlignment,
actionPanelVerticalAlignment,
toggleRowCollapse,
permissions,
} = props;
// const draggableRef = useRef(null);
const [isHovered, setIsHovered] = useState(false);
// const handleCollapseClick = () => {
// draggableRef.current.focus();
// dispatchRows({
// type: 'UPDATE_COLLAPSIBLE_STATUS',
// index: rowIndex,
// });
// };
const classes = [
baseClass,
isOpen && 'is-open',
isOpen ? 'is-open' : 'is-closed',
isHovered && 'is-hovered',
].filter(Boolean).join(' ');
@@ -60,50 +53,71 @@ const DraggableSection = (props) => {
<div
ref={providedDrag.innerRef}
className={classes}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onMouseOver={() => setIsHovered(true)}
onFocus={() => setIsHovered(true)}
{...providedDrag.draggableProps}
>
<PositionHandle
dragHandleProps={providedDrag.dragHandleProps}
moveRow={moveRow}
positionIndex={rowIndex}
verticalAlignment={positionHandleVerticalAlignment}
/>
<div className={`${baseClass}__content-wrapper`}>
<PositionPanel
dragHandleProps={providedDrag.dragHandleProps}
moveRow={moveRow}
rowCount={rowCount}
positionIndex={rowIndex}
verticalAlignment={positionPanelVerticalAlignment}
/>
<div className={`${baseClass}__render-fields-wrapper`}>
<div className={`${baseClass}__render-fields-wrapper`}>
{blockType === 'flexible' && (
<SectionTitle
label={singularLabel}
initialData={initialData?.blockName}
path={`${parentPath}.${rowIndex}.blockName`}
/>
)}
{blockType === 'flexible' && (
<div className={`${baseClass}__section-header`}>
<SectionTitle
label={singularLabel}
initialData={initialData?.blockName}
path={`${parentPath}.${rowIndex}.blockName`}
/>
{/* Render fields */}
<RenderFields
initialData={initialData}
customComponentsPath={customComponentsPath}
fieldTypes={fieldTypes}
key={rowIndex}
fieldSchema={fieldSchema.map((field) => {
return ({
...field,
path: `${parentPath}.${rowIndex}${field.name ? `.${field.name}` : ''}`,
});
})}
<Button
icon="chevron"
onClick={toggleRowCollapse}
buttonStyle="icon-label"
className={`toggle-collapse toggle-collapse--is-${isOpen ? 'open' : 'closed'}`}
round
/>
</div>
)}
<AnimateHeight
height={isOpen ? 'auto' : 0}
duration={0}
>
<RenderFields
initialData={initialData}
customComponentsPath={customComponentsPath}
fieldTypes={fieldTypes}
key={rowIndex}
permissions={permissions}
fieldSchema={fieldSchema.map((field) => {
return ({
...field,
path: `${parentPath}.${rowIndex}${field.name ? `.${field.name}` : ''}`,
});
})}
/>
</AnimateHeight>
</div>
<ActionPanel
rowIndex={rowIndex}
addRow={addRow}
removeRow={removeRow}
singularLabel={singularLabel}
verticalAlignment={actionPanelVerticalAlignment}
isHovered={isHovered}
{...props}
/>
</div>
<ActionHandle
removeRow={removeRow}
addRow={addRow}
rowIndex={rowIndex}
singularLabel={singularLabel}
verticalAlignment={actionHandleVerticalAlignment}
/>
</div>
);
}}
@@ -112,34 +126,37 @@ const DraggableSection = (props) => {
};
DraggableSection.defaultProps = {
toggleRowCollapse: undefined,
rowCount: null,
initialData: undefined,
singularLabel: '',
blockType: '',
customComponentsPath: '',
isOpen: true,
positionHandleVerticalAlignment: 'center',
actionHandleVerticalAlignment: 'center',
positionPanelVerticalAlignment: 'sticky',
actionPanelVerticalAlignment: 'sticky',
permissions: {},
};
DraggableSection.propTypes = {
moveRow: PropTypes.func.isRequired,
addRow: PropTypes.func.isRequired,
removeRow: PropTypes.func.isRequired,
toggleRowCollapse: PropTypes.func,
rowIndex: PropTypes.number.isRequired,
parentPath: PropTypes.string.isRequired,
singularLabel: PropTypes.string,
fieldSchema: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
rowCount: PropTypes.number,
initialData: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.shape({})]),
dispatchRows: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
blockType: PropTypes.string,
fieldTypes: PropTypes.shape({}).isRequired,
customComponentsPath: PropTypes.string,
id: PropTypes.string.isRequired,
positionHandleVerticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
actionHandleVerticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
positionPanelVerticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
actionPanelVerticalAlignment: PropTypes.oneOf(['top', 'center', 'sticky']),
permissions: PropTypes.shape({}),
};
export default DraggableSection;

View File

@@ -4,25 +4,25 @@
// HELPER MIXINS
//////////////////////
@mixin realtively-position-handles {
.position-handle {
@mixin realtively-position-panels {
.position-panel {
position: relative;
right: 0;
}
.action-handle {
.action-panel {
position: relative;
left: 0;
}
}
@mixin absolutely-position-handles {
.position-handle {
@mixin absolutely-position-panels {
.position-panel {
position: absolute;
top: 0; right: 100%; bottom: 0;
}
.action-handle {
.action-panel {
position: absolute;
top: 0; bottom: 0; left: 100%;
}
@@ -33,43 +33,96 @@
//////////////////////
.draggable-section {
display: flex;
position: relative;
padding-top: base(.75);
padding-bottom: base(.75);
margin-bottom: base(.75);
padding-bottom: base(.5);
.draggable-section {
padding-bottom: 0;
}
&__content-wrapper {
display: flex;
position: relative;
padding-top: base(.75);
}
&.is-closed {
.draggable-section__content-wrapper {
margin-bottom: base(1.75);
}
}
&__section-header {
display: flex;
.toggle-collapse {
margin: 0 0 0 auto;
transform: rotate(.5turn);
&--is-closed {
transform: rotate(0turn);
}
}
}
&__render-fields-wrapper {
width: 100%;
}
&.is-hovered,
&:focus-within {
> .position-handle {
.position-handle__controls-container {
&.is-hovered > div {
> .position-panel {
.position-panel__controls-container {
box-shadow: #{$style-stroke-width-m} 0px 0px 0px $color-dark-gray;
}
.position-handle__move-forward,
.position-handle__move-backward {
.position-panel__move-forward,
.position-panel__move-backward {
opacity: 1;
&.first-row,
&.last-row {
opacity: .15;
pointer-events: none;
}
}
.position-handle__current-position {
.position-panel__current-position {
color: $color-dark-gray;
}
}
> .action-handle {
.action-handle__add-row,
.action-handle__remove-row {
> .action-panel {
.action-panel__controls {
opacity: 1;
visibility: visible;
z-index: $z-nav;
}
}
.toggle-collapse {
@include color-svg(white);
.btn__icon {
background-color: $color-gray;
&:hover {
background-color: $color-dark-gray;
}
}
}
}
label.field-label {
line-height: 1;
padding-bottom: base(.75)
}
@include mid-break {
@include realtively-position-handles();
@include realtively-position-panels();
.position-panel__move-forward,
.position-panel__move-backward {
opacity: 1;
}
}
}
@@ -78,12 +131,23 @@
//////////////////////
.collection-edit {
@include absolutely-position-handles();
@include absolutely-position-panels();
@include mid-break {
@include realtively-position-panels();
}
}
.field-type.repeater .field-type.repeater {
@include realtively-position-handles();
@include realtively-position-panels();
}
// remove padding above repeater rows to level
// the line with the top of the input label
.field-type.repeater {
.draggable-section {
&__content-wrapper {
padding-top: 0;
}
}
}

View File

@@ -17,16 +17,17 @@ const RenderFields = (props) => {
filter,
permissions,
readOnly: readOnlyOverride,
operation,
operation: operationFromProps,
} = props;
const { customComponentsPath: customComponentsPathFromContext } = useRenderedFields();
const { customComponentsPath: customComponentsPathFromContext, operation: operationFromContext } = useRenderedFields();
const customComponentsPath = customComponentsPathFromProps || customComponentsPathFromContext;
const operation = operationFromProps || operationFromContext;
if (fieldSchema) {
return (
<RenderedFieldContext.Provider value={{ customComponentsPath }}>
<RenderedFieldContext.Provider value={{ customComponentsPath, operation }}>
{fieldSchema.map((field, i) => {
if (field?.hidden !== 'api' && field?.hidden !== true) {
if ((filter && typeof filter === 'function' && filter(field)) || !filter) {
@@ -117,7 +118,6 @@ RenderFields.propTypes = {
filter: PropTypes.func,
permissions: PropTypes.shape({}),
readOnly: PropTypes.bool,
operation: PropTypes.oneOf(['create', 'read', 'update', 'delete']),
};
export default RenderFields;

View File

@@ -1,22 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import SearchIcon from '../../../../../graphics/Search';
import './index.scss';
const baseClass = 'block-search';
const BlockSearch = (props) => {
const {} = props;
const { setSearchTerm } = props;
const handleChange = (e) => {
setSearchTerm(e.target.value);
};
return (
<div className={baseClass}>
Search...
<input
className={`${baseClass}__input`}
placeholder="Search for a block"
onChange={handleChange}
/>
<SearchIcon />
</div>
);
};
BlockSearch.defaultProps = {};
BlockSearch.propTypes = {};
BlockSearch.propTypes = {
setSearchTerm: PropTypes.func.isRequired,
};
export default BlockSearch;

View File

@@ -1 +1,30 @@
@import '../../../../../../scss/styles';
@import '../../../shared.scss';
$icon-width: base(1);
$icon-margin: base(.25);
.block-search {
position: sticky;
top: 0;
display: flex;
align-items: center;
z-index: 1;
&__input {
@include formInput;
padding-right: calc(#{$icon-width} + #{$icon-margin} * 2);
}
.search {
position: absolute;
right: 0;
width: $icon-width;
margin: 0 $icon-margin;
}
@include mid-break {
&__input {
margin-bottom: 0;
}
}
}

View File

@@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
import DefaultBlockImage from '../../../../../graphics/DefaultBlockImage';
import './index.scss';
const baseClass = 'block-selection';
const BlockSelection = (props) => {
const {
addRow, addRowIndex, block, close,
} = props;
const {
labels, slug, blockImage, blockImageAltText,
} = block;
const handleBlockSelection = () => {
console.log('adding');
close();
addRow(addRowIndex, slug);
};
return (
<div
className={baseClass}
role="button"
tabIndex={0}
onClick={handleBlockSelection}
>
<div className={`${baseClass}__image`}>
{blockImage
? (
<img
src={blockImage}
alt={blockImageAltText}
/>
)
: <DefaultBlockImage />
}
</div>
<div className={`${baseClass}__label`}>{labels.singular}</div>
</div>
);
};
BlockSelection.defaultProps = {
addRow: undefined,
addRowIndex: 0,
};
BlockSelection.propTypes = {
addRow: PropTypes.func,
addRowIndex: PropTypes.number,
block: PropTypes.shape({
labels: PropTypes.shape({
singular: PropTypes.string,
}),
slug: PropTypes.string,
blockImage: PropTypes.string,
blockImageAltText: PropTypes.string,
}).isRequired,
close: PropTypes.func.isRequired,
};
export default BlockSelection;

View File

@@ -0,0 +1,33 @@
@import '../../../../../../scss/styles';
.block-selection {
display: inline-flex;
flex-direction: column;
flex-wrap: wrap;
width: 33%;
padding: base(.75) base(.5);
cursor: pointer;
align-items: center;
&:hover {
background-color: $color-background-gray;
}
&__image {
svg,
img {
max-width: 100%;
}
}
&__label {
margin-top: base(.25);
font-weight: 600;
text-align: center;
white-space: initial;
}
@include mid-break {
width: unset;
}
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import SelectableBlock from '../SelectableBlock';
import BlockSelection from '../BlockSelection';
import './index.scss';
@@ -14,14 +14,13 @@ const BlocksContainer = (props) => {
<div className={baseClass}>
{blocks?.map((block, index) => {
return (
<SelectableBlock
<BlockSelection
key={index}
block={block}
{...remainingProps}
/>
);
})}
Blocks to choose from...
</div>
);
};

View File

@@ -1 +1,14 @@
@import '../../../../../../scss/styles';
.blocks-container {
width: 100%;
margin-top: base(1);
margin-bottom: base(.5);
display: flex;
flex-wrap: wrap;
align-items: center;
min-width: 450px;
max-width: 80vw;
max-height: 300px;
position: relative;
}

View File

@@ -1,32 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const baseClass = 'selectable-block';
const SelectableBlock = (props) => {
const { addRow, addRowIndex, block } = props;
const { labels, slug } = block;
return (
<div
className={baseClass}
role="button"
onClick={() => addRow(addRowIndex, slug)}
>
{labels.singular}
</div>
);
};
SelectableBlock.defaultProps = {
addRow: undefined,
addRowIndex: 0,
};
SelectableBlock.propTypes = {
addRow: PropTypes.func,
addRowIndex: PropTypes.number,
};
export default SelectableBlock;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import BlockSearch from './BlockSearch';
@@ -7,16 +7,54 @@ import BlocksContainer from './BlocksContainer';
const baseClass = 'block-selector';
const BlockSelector = (props) => {
const {
blocks, close, parentIsHovered, watchParentHover, ...remainingProps
} = props;
const [searchTerm, setSearchTerm] = useState('');
const [filteredBlocks, setFilteredBlocks] = useState(blocks);
const [isBlockSelectorHovered, setBlockSelectorHovered] = useState(false);
useEffect(() => {
const matchingBlocks = blocks.reduce((matchedBlocks, block) => {
if (block.slug.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1) matchedBlocks.push(block);
return matchedBlocks;
}, []);
setFilteredBlocks(matchingBlocks);
}, [searchTerm, blocks]);
useEffect(() => {
if (!parentIsHovered && !isBlockSelectorHovered && close && watchParentHover) close();
}, [isBlockSelectorHovered, parentIsHovered, close, watchParentHover]);
return (
<div className={baseClass}>
<BlockSearch />
<BlocksContainer {...props} />
<div
className={baseClass}
onMouseEnter={() => setBlockSelectorHovered(true)}
onMouseLeave={() => setBlockSelectorHovered(false)}
>
<BlockSearch setSearchTerm={setSearchTerm} />
<BlocksContainer
blocks={filteredBlocks}
close={close}
{...remainingProps}
/>
</div>
);
};
BlockSelector.defaultProps = {};
BlockSelector.defaultProps = {
close: null,
parentIsHovered: false,
watchParentHover: false,
};
BlockSelector.propTypes = {};
BlockSelector.propTypes = {
blocks: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
close: PropTypes.func,
watchParentHover: PropTypes.bool,
parentIsHovered: PropTypes.bool,
};
export default BlockSelector;

View File

@@ -0,0 +1,8 @@
@import '../../../../../scss/styles.scss';
.block-selector {
@include mid-break {
min-width: 80vw;
}
}

View File

@@ -1,5 +1,5 @@
import React, {
useEffect, useReducer, useState, useCallback,
useEffect, useReducer, useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
@@ -14,7 +14,7 @@ import { useRenderedFields } from '../../RenderFields';
import Error from '../../Error';
import useFieldType from '../../useFieldType';
import Popup from '../../../elements/Popup';
import BlocksContainer from './BlockSelector/BlocksContainer';
import BlockSelector from './BlockSelector';
import { flexible } from '../../../../../fields/validations';
import './index.scss';
@@ -65,14 +65,11 @@ const Flexible = (props) => {
});
const dataToInitialize = initialData || defaultValue;
const [addRowIndex, setAddRowIndex] = useState(null);
const [rows, dispatchRows] = useReducer(reducer, []);
const { customComponentsPath } = useRenderedFields();
const { getDataByPath } = useForm();
const addRow = (index, blockType) => {
setAddRowIndex(current => current + 1);
const data = getDataByPath(path);
dispatchRows({
@@ -102,6 +99,12 @@ const Flexible = (props) => {
});
};
const toggleCollapse = (index) => {
dispatchRows({
type: 'TOGGLE_COLLAPSE', index, rows,
});
};
const onDragEnd = (result) => {
if (!result.destination) return;
const sourceIndex = result.source.index;
@@ -121,102 +124,106 @@ const Flexible = (props) => {
},
]), []),
});
}, [dataToInitialize, setValue]);
}, [dataToInitialize]);
return (
<>
<DragDropContext onDragEnd={onDragEnd}>
<div className={baseClass}>
<header className={`${baseClass}__header`}>
<h3>{label}</h3>
<Error
showError={showError}
message={errorMessage}
/>
</header>
<Droppable droppableId="flexible-drop">
{provided => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{rows.length > 0 && rows.map((row, i) => {
let { blockType } = row.data;
<DragDropContext onDragEnd={onDragEnd}>
<div className={baseClass}>
<header className={`${baseClass}__header`}>
<h3>{label}</h3>
if (!blockType) {
blockType = dataToInitialize?.[i]?.blockType;
}
<Error
showError={showError}
message={errorMessage}
/>
</header>
const blockToRender = blocks.find(block => block.slug === blockType);
if (blockToRender) {
return (
<DraggableSection
isOpen={row.open}
fieldTypes={fieldTypes}
key={row.key}
id={row.key}
parentPath={path}
moveRow={moveRow}
addRow={() => addRow(i, blockType)}
removeRow={() => removeRow(i)}
rowIndex={i}
permissions={permissions.fields}
fieldSchema={[
...blockToRender.fields,
{
name: 'blockType',
type: 'text',
hidden: 'admin',
}, {
name: 'blockName',
type: 'text',
hidden: 'admin',
},
]}
singularLabel={blockToRender?.labels?.singular}
initialData={row.data}
dispatchRows={dispatchRows}
blockType="flexible"
customComponentsPath={`${customComponentsPath}${name}.fields.`}
positionHandleVerticalAlignment="sticky"
actionHandleVerticalAlignment="sticky"
/>
);
}
return null;
})
}
{provided.placeholder}
</div>
)}
</Droppable>
<div className={`${baseClass}__add-button-wrap`}>
<Popup
buttonType="custom"
button={(
<Button
buttonStyle="icon-label"
icon="plus"
iconPosition="left"
iconStyle="with-border"
>
{`Add ${singularLabel}`}
</Button>
)}
<Droppable droppableId="flexible-drop">
{provided => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
<BlocksContainer
{rows.length > 0 && rows.map((row, i) => {
let { blockType } = row.data;
if (!blockType) {
blockType = dataToInitialize?.[i]?.blockType;
}
const blockToRender = blocks.find(block => block.slug === blockType);
if (blockToRender) {
return (
<DraggableSection
key={row.key}
id={row.key}
blockType="flexible"
blocks={blocks}
singularLabel={blockToRender?.labels?.singular}
isOpen={row.open}
rowCount={rows.length}
rowIndex={i}
addRow={addRow}
removeRow={() => removeRow(i)}
moveRow={moveRow}
toggleRowCollapse={() => toggleCollapse(i)}
parentPath={path}
initialData={row.data}
customComponentsPath={`${customComponentsPath}${name}.fields.`}
fieldTypes={fieldTypes}
permissions={permissions.fields}
fieldSchema={[
...blockToRender.fields,
{
name: 'blockType',
type: 'text',
hidden: 'admin',
}, {
name: 'blockName',
type: 'text',
hidden: 'admin',
},
]}
/>
);
}
return null;
})
}
{provided.placeholder}
</div>
)}
</Droppable>
<div className={`${baseClass}__add-button-wrap`}>
<Popup
buttonType="custom"
size="large"
horizontalAlign="left"
button={(
<Button
buttonStyle="icon-label"
icon="plus"
iconPosition="left"
iconStyle="with-border"
>
{`Add ${singularLabel}`}
</Button>
)}
render={({ close }) => (
<BlockSelector
blocks={blocks}
addRow={addRow}
addRowIndex={addRowIndex}
addRowIndex={value}
close={close}
/>
</Popup>
</div>
)}
/>
</div>
</DragDropContext>
</>
</div>
</DragDropContext>
);
};

View File

@@ -3,6 +3,7 @@
.field-type.flexible {
&__add-button-wrap {
.btn {
color: $color-gray;
margin: 0;

View File

@@ -25,12 +25,15 @@ const Repeater = (props) => {
fields,
defaultValue,
initialData,
singularLabel,
fieldTypes,
validate,
required,
maxRows,
minRows,
labels: {
singular: singularLabel,
},
permissions,
} = props;
const dataToInitialize = initialData || defaultValue;
@@ -129,23 +132,23 @@ const Repeater = (props) => {
{rows.length > 0 && rows.map((row, i) => {
return (
<DraggableSection
isOpen={row.open}
fieldTypes={fieldTypes}
key={row.key}
id={row.key}
parentPath={path}
blockType="repeater"
singularLabel={singularLabel}
addRow={() => addRow(i)}
moveRow={moveRow}
removeRow={() => removeRow(i)}
isOpen={row.open}
rowCount={rows.length}
rowIndex={i}
fieldSchema={fields}
addRow={() => addRow(i)}
removeRow={() => removeRow(i)}
moveRow={moveRow}
parentPath={path}
initialData={row.data}
initNull={row.initNull}
dispatchRows={dispatchRows}
customComponentsPath={`${customComponentsPath}${name}.fields.`}
positionHandleVerticalAlignment="sticky"
actionHandleVerticalAlignment="sticky"
fieldTypes={fieldTypes}
fieldSchema={fields}
permissions={permissions.fields}
/>
);
})
@@ -173,13 +176,16 @@ const Repeater = (props) => {
Repeater.defaultProps = {
label: '',
singularLabel: 'Row',
defaultValue: [],
initialData: [],
validate: repeater,
required: false,
maxRows: undefined,
minRows: undefined,
labels: {
singular: 'Row',
},
permissions: {},
};
Repeater.propTypes = {
@@ -193,7 +199,9 @@ Repeater.propTypes = {
PropTypes.shape({}),
).isRequired,
label: PropTypes.string,
singularLabel: PropTypes.string,
labels: PropTypes.shape({
singular: PropTypes.string,
}),
name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
fieldTypes: PropTypes.shape({}).isRequired,
@@ -201,6 +209,9 @@ Repeater.propTypes = {
required: PropTypes.bool,
maxRows: PropTypes.number,
minRows: PropTypes.number,
permissions: PropTypes.shape({
fields: PropTypes.shape({}),
}),
};
export default withCondition(Repeater);

View File

@@ -4,7 +4,7 @@
background: white;
&__add-button-wrap {
margin-left: 0;
margin-left: base(0);
.btn {
color: $color-gray;
@@ -28,7 +28,11 @@
.field-type.repeater {
.field-type.repeater__add-button-wrap {
margin-left: base(2.25);
margin-left: base(2.65);
}
.field-type.repeater__header {
display: none;
}
}
}

View File

@@ -7,14 +7,13 @@ import './index.scss';
const Row = (props) => {
const {
fields, fieldTypes, initialData, path: pathFromProps, name,
fields, fieldTypes, initialData, path, permissions,
} = props;
const path = pathFromProps || name;
return (
<div className="field-type row">
<RenderFields
permissions={permissions}
initialData={initialData}
fieldTypes={fieldTypes}
fieldSchema={fields.map((field) => {
@@ -31,6 +30,7 @@ const Row = (props) => {
Row.defaultProps = {
path: '',
initialData: undefined,
permissions: {},
};
Row.propTypes = {
@@ -39,8 +39,8 @@ Row.propTypes = {
).isRequired,
fieldTypes: PropTypes.shape({}).isRequired,
path: PropTypes.string,
name: PropTypes.string.isRequired,
initialData: PropTypes.shape({}),
permissions: PropTypes.shape({}),
};
export default withCondition(Row);

View File

@@ -9,7 +9,6 @@
> * {
margin-left: base(.5);
margin-right: base(.5);
margin-bottom: 0;
}
@include mid-break {

View File

@@ -11,6 +11,10 @@ const reducer = (currentState, action) => {
case 'SET_ALL':
return rows;
case 'TOGGLE_COLLAPSE':
stateCopy[index].open = !stateCopy[index].open;
return stateCopy;
case 'ADD':
stateCopy.splice(index + 1, 0, {
open: true,
@@ -33,10 +37,6 @@ const reducer = (currentState, action) => {
stateCopy.splice(index, 1);
return stateCopy;
case 'UPDATE_COLLAPSIBLE_STATUS':
stateCopy[index].open = !stateCopy[index].open;
return stateCopy;
case 'MOVE': {
const stateCopyWithNewData = stateCopy.map((row, i) => {
return {

View File

@@ -0,0 +1,44 @@
import React, { useState } from 'react';
import { v4 as uuid } from 'uuid';
const DefaultBlockImage = () => {
const [patternID] = useState(`pattern${uuid()}`);
const [imageID] = useState(`image${uuid()}`);
return (
<svg
width="82"
height="53"
viewBox="0 0 82 53"
fill="none"
>
<rect
x="0.713013"
width="80.574"
height="52.7791"
fill={`url(#${patternID})`}
/>
<defs>
<pattern
id={`${patternID}`}
patternContentUnits="objectBoundingBox"
width="1"
height="1"
>
<use
xlinkHref={`#${imageID}`}
transform="scale(0.00387597 0.00591716)"
/>
</pattern>
<image
id={imageID}
width="258"
height="169"
xlinkHref="data:image/png;base64,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"
/>
</defs>
</svg>
);
};
export default DefaultBlockImage;

View File

@@ -0,0 +1,34 @@
import React from 'react';
const Search = () => {
return (
<svg
width="25"
height="26"
viewBox="0 0 25 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="search"
>
<circle
cx="11.2069"
cy="10.9135"
r="5"
stroke="#333333"
strokeWidth="2"
className="stroke"
/>
<line
x1="14.914"
y1="14.2063"
x2="20.5002"
y2="19.7925"
stroke="#333333"
strokeWidth="2"
className="stroke"
/>
</svg>
);
};
export default Search;

View File

@@ -136,6 +136,10 @@
}
@include mid-break {
&__sidebar {
width: unset;
}
&__form {
display: block;
}

View File

@@ -61,6 +61,14 @@ $style-stroke-width-m : 2px;
box-shadow: 0 2px 3px 0 rgba(0, 2, 4, 0.1), 0 6px 4px -4px rgba(0, 2, 4, 0.02);
}
@mixin shadow-lg {
box-shadow: 0 2px 20px 7px rgba(0, 2, 4, 0.1), 0 6px 4px -4px rgba(0, 2, 4, 0.02);
}
@mixin shadow-lg-top {
box-shadow: 0 -2px 20px 7px rgba(0, 2, 4, 0.1), 0 6px 4px -4px rgba(0, 2, 4, 0.02);
}
@mixin shadow {
box-shadow: 0 12px 45px rgba(0,0,0,.03);