merges repeater-design, implements field policy config in admin panel
This commit is contained in:
@@ -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,
|
||||
|
||||
9
src/client/assets/images/generic-block-image.svg
Normal file
9
src/client/assets/images/generic-block-image.svg
Normal 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 |
56
src/client/components/elements/Popup/PopupButton/index.js
Normal file
56
src/client/components/elements/Popup/PopupButton/index.js
Normal 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;
|
||||
@@ -0,0 +1,5 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.popup-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
@@ -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']),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{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
|
||||
{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;
|
||||
@@ -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
|
||||
{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
|
||||
{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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
@import '../../../../../scss/styles.scss';
|
||||
|
||||
.block-selector {
|
||||
|
||||
@include mid-break {
|
||||
min-width: 80vw;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
.field-type.flexible {
|
||||
|
||||
&__add-button-wrap {
|
||||
|
||||
.btn {
|
||||
color: $color-gray;
|
||||
margin: 0;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
> * {
|
||||
margin-left: base(.5);
|
||||
margin-right: base(.5);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
44
src/client/components/graphics/DefaultBlockImage/index.js
Normal file
44
src/client/components/graphics/DefaultBlockImage/index.js
Normal 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;
|
||||
34
src/client/components/graphics/Search/index.js
Normal file
34
src/client/components/graphics/Search/index.js
Normal 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;
|
||||
@@ -136,6 +136,10 @@
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__sidebar {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user