feat: blocks drawer #1909

This commit is contained in:
Jacob Fletcher
2023-01-20 13:47:57 -05:00
parent 8b08e5a1f9
commit 339cee416a
25 changed files with 236 additions and 236 deletions

View File

@@ -39,7 +39,6 @@ export const DocumentDrawerToggler: React.FC<DocumentTogglerProps> = ({
return (
<DrawerToggler
slug={drawerSlug}
formatSlug={false}
className={[
className,
`${baseClass}__toggler`,
@@ -59,7 +58,6 @@ export const DocumentDrawer: React.FC<DocumentDrawerProps> = (props) => {
return (
<Drawer
slug={drawerSlug}
formatSlug={false}
className={baseClass}
>
<DocumentDrawerContent {...props} />

View File

@@ -20,7 +20,6 @@ export const formatDrawerSlug = ({
export const DrawerToggler: React.FC<TogglerProps> = ({
slug,
formatSlug,
children,
className,
onClick,
@@ -28,12 +27,11 @@ export const DrawerToggler: React.FC<TogglerProps> = ({
...rest
}) => {
const { openModal } = useModal();
const drawerDepth = useEditDepth();
const handleClick = useCallback((e) => {
openModal(formatSlug !== false ? formatDrawerSlug({ slug, depth: drawerDepth }) : slug);
openModal(slug);
if (typeof onClick === 'function') onClick(e);
}, [openModal, drawerDepth, slug, onClick, formatSlug]);
}, [openModal, slug, onClick]);
return (
<button
@@ -50,7 +48,6 @@ export const DrawerToggler: React.FC<TogglerProps> = ({
export const Drawer: React.FC<Props> = ({
slug,
formatSlug,
children,
className,
}) => {
@@ -60,11 +57,10 @@ export const Drawer: React.FC<Props> = ({
const drawerDepth = useEditDepth();
const [isOpen, setIsOpen] = useState(false);
const [animateIn, setAnimateIn] = useState(false);
const [modalSlug] = useState(() => (formatSlug !== false ? formatDrawerSlug({ slug, depth: drawerDepth }) : slug));
useEffect(() => {
setIsOpen(modalState[modalSlug]?.isOpen);
}, [modalSlug, modalState]);
setIsOpen(modalState[slug]?.isOpen);
}, [slug, modalState]);
useEffect(() => {
setAnimateIn(isOpen);
@@ -75,7 +71,7 @@ export const Drawer: React.FC<Props> = ({
return (
<Modal
slug={modalSlug}
slug={slug}
className={[
className,
baseClass,
@@ -90,9 +86,9 @@ export const Drawer: React.FC<Props> = ({
)}
<button
className={`${baseClass}__close`}
id={`close-drawer__${modalSlug}`}
id={`close-drawer__${slug}`}
type="button"
onClick={() => closeModal(modalSlug)}
onClick={() => closeModal(slug)}
style={{
width: `calc(${midBreak ? 'var(--gutter-h)' : 'var(--nav-width)'} + ${drawerDepth - 1} * 25px)`,
}}

View File

@@ -2,14 +2,12 @@ import { HTMLAttributes } from 'react';
export type Props = {
slug: string
formatSlug?: boolean
children: React.ReactNode
className?: string
}
export type TogglerProps = HTMLAttributes<HTMLButtonElement> & {
slug: string
formatSlug?: boolean
children: React.ReactNode
className?: string
disabled?: boolean

View File

@@ -0,0 +1,12 @@
import { useId } from 'react';
import { formatDrawerSlug } from '.';
import { useEditDepth } from '../../utilities/EditDepth';
export const useDrawerSlug = (slug: string): string => {
const uuid = useId();
const editDepth = useEditDepth();
return formatDrawerSlug({
slug: `${slug}-${uuid}`,
depth: editDepth,
});
};

View File

@@ -27,7 +27,6 @@ export const ListDrawerToggler: React.FC<ListTogglerProps> = ({
return (
<DrawerToggler
slug={drawerSlug}
formatSlug={false}
className={[
className,
`${baseClass}__toggler`,
@@ -46,7 +45,6 @@ export const ListDrawer: React.FC<ListDrawerProps> = (props) => {
return (
<Drawer
slug={drawerSlug}
formatSlug={false}
className={baseClass}
>
<ListDrawerContent {...props} />

View File

@@ -1,14 +0,0 @@
@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,26 +0,0 @@
import React from 'react';
import { Props } from './types';
import BlockSelection from '../BlockSelection';
import './index.scss';
const baseClass = 'blocks-container';
const BlocksContainer: React.FC<Props> = (props) => {
const { blocks, ...remainingProps } = props;
return (
<div className={baseClass}>
{blocks?.map((block, index) => (
<BlockSelection
key={index}
block={block}
{...remainingProps}
/>
))}
</div>
);
};
export default BlocksContainer;

View File

@@ -1,8 +0,0 @@
import { Block } from '../../../../../../../fields/config/types';
export type Props = {
blocks: Block[]
close: () => void
addRow: (i: number, block: string) => void
addRowIndex: number
}

View File

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

View File

@@ -1,47 +0,0 @@
import React, { useState, useEffect } from 'react';
import BlockSearch from './BlockSearch';
import BlocksContainer from './BlocksContainer';
import { Props } from './types';
const baseClass = 'block-selector';
const BlockSelector: React.FC<Props> = (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}
onMouseEnter={() => setBlockSelectorHovered(true)}
onMouseLeave={() => setBlockSelectorHovered(false)}
>
<BlockSearch setSearchTerm={setSearchTerm} />
<BlocksContainer
blocks={filteredBlocks}
close={close}
{...remainingProps}
/>
</div>
);
};
export default BlockSelector;

View File

@@ -1,10 +0,0 @@
import { Block } from '../../../../../../fields/config/types';
export type Props = {
blocks: Block[]
addRow: (index: number, blockType?: string) => void
watchParentHover?: boolean
parentIsHovered?: boolean
close: () => void
addRowIndex: number
}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { getTranslation } from '../../../../../../../utilities/getTranslation';
import DefaultBlockImage from '../../../../../graphics/DefaultBlockImage';
@@ -10,7 +10,10 @@ const baseClass = 'block-selection';
const BlockSelection: React.FC<Props> = (props) => {
const {
addRow, addRowIndex, block, close,
onClick,
addRow,
addRowIndex,
block,
} = props;
const { i18n } = useTranslation();
@@ -19,10 +22,12 @@ const BlockSelection: React.FC<Props> = (props) => {
labels, slug, imageURL, imageAltText,
} = block;
const handleBlockSelection = () => {
close();
const handleBlockSelection = useCallback(() => {
addRow(addRowIndex, slug);
};
if (typeof onClick === 'function') {
onClick();
}
}, [onClick, addRow, addRowIndex, slug]);
return (
<button
@@ -32,14 +37,12 @@ const BlockSelection: React.FC<Props> = (props) => {
onClick={handleBlockSelection}
>
<div className={`${baseClass}__image`}>
{imageURL
? (
<img
src={imageURL}
alt={imageAltText}
/>
)
: <DefaultBlockImage />}
{imageURL ? (
<img
src={imageURL}
alt={imageAltText}
/>
) : <DefaultBlockImage />}
</div>
<div className={`${baseClass}__label`}>{getTranslation(labels.singular, i18n)}</div>
</button>

View File

@@ -4,5 +4,5 @@ export type Props = {
addRow: (i: number, block: string) => void
addRowIndex: number
block: Block
close: () => void
onClick?: () => void
}

View File

@@ -0,0 +1,10 @@
@import '../../../../../scss/styles.scss';
.blocks-drawer {
margin-top: base(2.5);
width: 100%;
@include mid-break {
margin-top: base(1.5);
}
}

View File

@@ -0,0 +1,61 @@
import React, { useState, useEffect } from 'react';
import { useModal } from '@faceless-ui/modal';
import { useTranslation } from 'react-i18next';
import BlockSearch from './BlockSearch';
import { Props } from './types';
import BlockSelection from './BlockSelection';
import { Drawer } from '../../../../elements/Drawer';
import { Gutter } from '../../../../elements/Gutter';
import { getTranslation } from '../../../../../../utilities/getTranslation';
import './index.scss';
const baseClass = 'blocks-drawer';
export const BlocksDrawer: React.FC<Props> = (props) => {
const {
blocks,
addRow,
addRowIndex,
drawerSlug,
labels,
} = props;
const [searchTerm, setSearchTerm] = useState('');
const [filteredBlocks, setFilteredBlocks] = useState(blocks);
const { closeModal } = useModal();
const { t, i18n } = useTranslation('fields');
useEffect(() => {
const matchingBlocks = blocks.reduce((matchedBlocks, block) => {
if (block.slug.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1) matchedBlocks.push(block);
return matchedBlocks;
}, []);
setFilteredBlocks(matchingBlocks);
}, [searchTerm, blocks]);
return (
<Drawer slug={drawerSlug}>
<Gutter className={baseClass}>
<h2>
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
</h2>
<BlockSearch setSearchTerm={setSearchTerm} />
<div className={baseClass}>
{filteredBlocks?.map((block, index) => (
<BlockSelection
key={index}
block={block}
onClick={() => {
closeModal(drawerSlug);
}}
addRow={addRow}
addRowIndex={addRowIndex}
/>
))}
</div>
</Gutter>
</Drawer>
);
};

View File

@@ -0,0 +1,9 @@
import { Block, Labels } from '../../../../../../fields/config/types';
export type Props = {
drawerSlug: string
blocks: Block[]
addRow: (index: number, blockType?: string) => void
addRowIndex: number
labels: Labels
}

View File

@@ -0,0 +1,61 @@
import { useModal } from '@faceless-ui/modal';
import React from 'react';
import { Block, Labels } from '../../../../../fields/config/types';
import { ArrayAction } from '../../../elements/ArrayAction';
import { useDrawerSlug } from '../../../elements/Drawer/useDrawerSlug';
import { Row } from '../rowReducer';
import { BlocksDrawer } from './BlocksDrawer';
export const RowActions: React.FC<{
addRow: (rowIndex: number, blockType: string) => void
duplicateRow: (rowIndex: number, blockType: string) => void
removeRow: (rowIndex: number) => void
moveRow: (fromIndex: number, toIndex: number) => void
labels: Labels
blocks: Block[]
rowIndex: number
rows: Row[]
blockType: string
}> = (props) => {
const {
addRow,
duplicateRow,
removeRow,
moveRow,
labels,
blocks,
rowIndex,
rows,
blockType,
} = props;
const { openModal, closeModal } = useModal();
const drawerSlug = useDrawerSlug('blocks-drawer');
return (
<React.Fragment>
<BlocksDrawer
drawerSlug={drawerSlug}
blocks={blocks}
addRow={(index, rowBlockType) => {
if (typeof addRow === 'function') {
addRow(index, rowBlockType);
}
closeModal(drawerSlug);
}}
addRowIndex={rowIndex}
labels={labels}
/>
<ArrayAction
rowCount={rows.length}
addRow={() => {
openModal(drawerSlug);
}}
duplicateRow={() => duplicateRow(rowIndex, blockType)}
moveRow={moveRow}
removeRow={removeRow}
index={rowIndex}
/>
</React.Fragment>
);
};

View File

@@ -62,7 +62,11 @@
position: relative;
}
&__add-button-wrap {
&__drawer-toggler {
background-color: transparent;
margin: 0;
padding: 0;
border: none;
.btn {
color: var(--theme-elevation-400);

View File

@@ -1,27 +1,23 @@
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import React, { Fragment, useCallback, useEffect, useReducer } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../../utilities/Auth';
import { usePreferences } from '../../../utilities/Preferences';
import { useLocale } from '../../../utilities/Locale';
import withCondition from '../../withCondition';
import Button from '../../../elements/Button';
import reducer, { Row } from '../rowReducer';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
import { useForm } from '../../Form/context';
import buildStateFromSchema from '../../Form/buildStateFromSchema';
import Error from '../../Error';
import useField from '../../useField';
import Popup from '../../../elements/Popup';
import BlockSelector from './BlockSelector';
import { BlocksDrawer } from './BlocksDrawer';
import { blocks as blocksValidator } from '../../../../../fields/validations';
import Banner from '../../../elements/Banner';
import FieldDescription from '../../FieldDescription';
import { Props } from './types';
import { useOperation } from '../../../utilities/OperationProvider';
import { Collapsible } from '../../../elements/Collapsible';
import { ArrayAction } from '../../../elements/ArrayAction';
import RenderFields from '../../RenderFields';
import SectionTitle from './SectionTitle';
import Pill from '../../../elements/Pill';
@@ -31,6 +27,10 @@ import { getTranslation } from '../../../../../utilities/getTranslation';
import { NullifyField } from '../../NullifyField';
import { useConfig } from '../../../utilities/Config';
import { createNestedFieldPath } from '../../Form/createNestedFieldPath';
import { DrawerToggler } from '../../../elements/Drawer';
import { useDrawerSlug } from '../../../elements/Drawer/useDrawerSlug';
import Button from '../../../elements/Button';
import { RowActions } from './RowActions';
import './index.scss';
@@ -38,15 +38,13 @@ const baseClass = 'blocks-field';
const BlocksField: React.FC<Props> = (props) => {
const { t, i18n } = useTranslation('fields');
const {
label,
name,
path: pathFromProps,
blocks,
labels = {
singular: t('block'),
plural: t('blocks'),
},
labels: labelsFromProps,
fieldTypes,
maxRows,
minRows,
@@ -75,8 +73,14 @@ const BlocksField: React.FC<Props> = (props) => {
const locale = useLocale();
const operation = useOperation();
const { dispatchFields, setModified } = formContext;
const [selectorIndexOpen, setSelectorIndexOpen] = useState<number>();
const { localization } = useConfig();
const drawerSlug = useDrawerSlug('blocks-drawer');
const labels = {
singular: t('block'),
plural: t('blocks'),
...labelsFromProps,
};
const checkSkipValidation = useCallback((value) => {
const defaultLocale = (localization && localization.defaultLocale) ? localization.defaultLocale : 'en';
@@ -102,12 +106,6 @@ const BlocksField: React.FC<Props> = (props) => {
disableFormData: rows?.length > 0,
});
const onAddPopupToggle = useCallback((open) => {
if (!open) {
setSelectorIndexOpen(undefined);
}
}, []);
const addRow = useCallback(async (rowIndex: number, blockType: string) => {
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale, t });
@@ -262,12 +260,10 @@ const BlocksField: React.FC<Props> = (props) => {
description={description}
/>
</header>
<NullifyField
path={path}
fieldValue={value}
/>
<Droppable
droppableId="blocks-drop"
isDropDisabled={readOnly}
@@ -321,32 +317,17 @@ const BlocksField: React.FC<Props> = (props) => {
</div>
)}
actions={!readOnly ? (
<React.Fragment>
<Popup
key={`${blockType}-${i}`}
forceOpen={selectorIndexOpen === i}
onToggleOpen={onAddPopupToggle}
buttonType="none"
size="large"
horizontalAlign="right"
render={({ close }) => (
<BlockSelector
blocks={blocks}
addRow={addRow}
addRowIndex={i}
close={close}
/>
)}
/>
<ArrayAction
rowCount={rows.length}
duplicateRow={() => duplicateRow(i, blockType)}
addRow={() => setSelectorIndexOpen(i)}
moveRow={moveRow}
removeRow={removeRow}
index={i}
/>
</React.Fragment>
<RowActions
addRow={addRow}
removeRow={removeRow}
duplicateRow={duplicateRow}
moveRow={moveRow}
rows={rows}
blockType={blockType}
blocks={blocks}
labels={labels}
rowIndex={i}
/>
) : undefined}
>
<HiddenInput
@@ -375,7 +356,6 @@ const BlocksField: React.FC<Props> = (props) => {
return null;
})}
{!checkSkipValidation(value) && (
<React.Fragment>
{(rows.length < minRows || (required && rows.length === 0)) && (
@@ -397,33 +377,30 @@ const BlocksField: React.FC<Props> = (props) => {
</div>
)}
</Droppable>
{(!readOnly && !hasMaxRows) && (
<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"
>
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
</Button>
)}
render={({ close }) => (
<BlockSelector
blocks={blocks}
addRow={addRow}
addRowIndex={value}
close={close}
/>
)}
<Fragment>
<DrawerToggler
slug={drawerSlug}
className={`${baseClass}__drawer-toggler`}
>
<Button
el="span"
icon="plus"
buttonStyle="icon-label"
iconPosition="left"
iconStyle="with-border"
>
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
</Button>
</DrawerToggler>
<BlocksDrawer
drawerSlug={drawerSlug}
blocks={blocks}
addRow={addRow}
addRowIndex={value}
labels={labels}
/>
</div>
</Fragment>
)}
</div>
</DragDropContext>

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useId, useState } from 'react';
import React, { Fragment, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactEditor, useSlate } from 'slate-react';
import { Transforms, Range, Editor } from 'slate';
@@ -9,8 +9,6 @@ import reduceFieldsToValues from '../../../../../Form/reduceFieldsToValues';
import { useConfig } from '../../../../../../utilities/Config';
import isElementActive from '../../isActive';
import { unwrapLink } from '../utilities';
import { useEditDepth } from '../../../../../../utilities/EditDepth';
import { formatDrawerSlug } from '../../../../../../elements/Drawer';
import { getBaseFields } from '../LinkDrawer/baseFields';
import { LinkDrawer } from '../LinkDrawer';
import { Field } from '../../../../../../../../fields/config/types';
@@ -19,6 +17,7 @@ import buildStateFromSchema from '../../../../../Form/buildStateFromSchema';
import { useAuth } from '../../../../../../utilities/Auth';
import { Fields } from '../../../../../Form/types';
import { useLocale } from '../../../../../../utilities/Locale';
import { useDrawerSlug } from '../../../../../../elements/Drawer/useDrawerSlug';
const insertLink = (editor, fields) => {
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
@@ -93,12 +92,7 @@ export const LinkButton: React.FC<{
});
const { openModal, closeModal } = useModal();
const uuid = useId();
const editDepth = useEditDepth();
const drawerSlug = formatDrawerSlug({
slug: `rich-text-link-${uuid}`,
depth: editDepth,
});
const drawerSlug = useDrawerSlug('rich-text-link');
return (
<Fragment>

View File

@@ -1,4 +1,4 @@
import React, { HTMLAttributes, useCallback, useEffect, useId, useState } from 'react';
import React, { HTMLAttributes, useCallback, useEffect, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react';
import { Transforms, Node, Editor } from 'slate';
import { useModal } from '@faceless-ui/modal';
@@ -17,10 +17,9 @@ import reduceFieldsToValues from '../../../../../Form/reduceFieldsToValues';
import deepCopyObject from '../../../../../../../../utilities/deepCopyObject';
import Button from '../../../../../../elements/Button';
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
import { useEditDepth } from '../../../../../../utilities/EditDepth';
import { formatDrawerSlug } from '../../../../../../elements/Drawer';
import { Props as RichTextFieldProps } from '../../../types';
import './index.scss';
import { useDrawerSlug } from '../../../../../../elements/Drawer/useDrawerSlug';
const baseClass = 'rich-text-link';
@@ -103,13 +102,7 @@ export const LinkElement: React.FC<{
return fields;
});
const uuid = useId();
const editDepth = useEditDepth();
const drawerSlug = formatDrawerSlug({
slug: `rich-text-link-${uuid}`,
depth: editDepth,
});
const drawerSlug = useDrawerSlug('rich-text-link');
const handleTogglePopup = useCallback((render) => {
if (!render) {

View File

@@ -26,7 +26,6 @@ export const LinkDrawer: React.FC<Props> = ({
return (
<Drawer
slug={drawerSlug}
formatSlug={false}
className={baseClass}
>
<Gutter className={`${baseClass}__template`}>