diff --git a/mailpoet/assets/css/src/components-automation-editor/_automation.scss b/mailpoet/assets/css/src/components-automation-editor/_automation.scss index a476b67696..7c1de9d1e8 100644 --- a/mailpoet/assets/css/src/components-automation-editor/_automation.scss +++ b/mailpoet/assets/css/src/components-automation-editor/_automation.scss @@ -5,12 +5,25 @@ .mailpoet-automation-editor-automation-wrapper { display: grid; + justify-content: center; + overflow: hidden; padding: 50px 20px; } +.mailpoet-automation-editor-automation-flow { + margin: auto; + width: max-content; +} + +.mailpoet-automation-editor-automation-row { + display: grid; + grid-auto-flow: column; +} + .mailpoet-automation-editor-automation-end { background: #8c8f94; border-radius: 999999px; + display: block; fill: white; height: 18px; margin: 4px auto; diff --git a/mailpoet/assets/css/src/components-automation-editor/_separator.scss b/mailpoet/assets/css/src/components-automation-editor/_separator.scss index 5b0057eb33..b9cccc8273 100644 --- a/mailpoet/assets/css/src/components-automation-editor/_separator.scss +++ b/mailpoet/assets/css/src/components-automation-editor/_separator.scss @@ -1,3 +1,5 @@ +$separator-width: 1px; + .mailpoet-automation-editor-separator { align-items: center; background: #c3c4c7; @@ -5,5 +7,69 @@ height: 64px; justify-content: center; margin: auto; - width: 1px; + width: $separator-width; +} + +.mailpoet-automation-editor-separator-curve-root { + display: flex; +} + +.mailpoet-automation-editor-separator-curve-root-left, +.mailpoet-automation-editor-separator-curve-root-right { + border-bottom: $separator-width solid #c3c4c7; + border-bottom-right-radius: 70px 30px; + border-right: $separator-width solid #c3c4c7; + height: 20px; + justify-self: end; + width: 100%; + //box-shadow: 0 0 1px transparent; + + &.mailpoet-automation-editor-separator-curve-root-left { + margin-right: calc(-1 * $separator-width / 2); + transform: scaleX(1); + } + + &.mailpoet-automation-editor-separator-curve-root-right { + margin-left: calc(-1 * $separator-width / 2); + transform: scaleX(-1); + } +} + +.mailpoet-automation-editor-separator-curve-leaf-left, +.mailpoet-automation-editor-separator-curve-leaf-right { + $width: 70px; + + &.mailpoet-automation-editor-separator-curve-leaf-left { + transform: scaleX(1); + } + + &.mailpoet-automation-editor-separator-curve-leaf-right { + transform: scaleX(-1); + } + + // cover rest of full-width line coming from curve root + &:before { + background: #fbfbfb; + content: ''; + display: block; + height: 20px; + position: absolute; + right: calc(50% - $width); + top: 0; + width: calc(100% - $width); + z-index: -1; + } + + // add curve leaf ending rounded to the bottom + &:after { + border-left: $separator-width solid #c3c4c7; + border-top: $separator-width solid #c3c4c7; + border-top-left-radius: 35px 20px; + content: ''; + display: block; + height: 16px; + margin: -$separator-width auto 0 calc(50% - $separator-width / 2); + transform-origin: left; + width: calc($width + $separator-width / 2); + } } diff --git a/mailpoet/assets/css/src/components-automation-editor/_step.scss b/mailpoet/assets/css/src/components-automation-editor/_step.scss index d3b9dda194..b13ba52af0 100644 --- a/mailpoet/assets/css/src/components-automation-editor/_step.scss +++ b/mailpoet/assets/css/src/components-automation-editor/_step.scss @@ -1,5 +1,5 @@ .mailpoet-automation-editor-step-wrapper { - margin: auto; + margin: 0 auto; position: relative; width: 280px; } @@ -27,10 +27,10 @@ grid-gap: 12px; grid-template-columns: auto 1fr; line-height: 1.4; - margin: 4px auto; + margin: 4px; padding: 12px; text-align: left; - width: 100%; + width: calc(100% - 8px); &.is-unknown-step { background: #f0f0f1; diff --git a/mailpoet/assets/css/src/components-automation/_statistics.scss b/mailpoet/assets/css/src/components-automation/_statistics.scss index 41d572bb8f..a1d77537e1 100644 --- a/mailpoet/assets/css/src/components-automation/_statistics.scss +++ b/mailpoet/assets/css/src/components-automation/_statistics.scss @@ -1,7 +1,8 @@ .mailpoet-automation-stats { display: grid; + gap: 8px; grid-auto-flow: column; - justify-content: space-between; + justify-content: center; } .mailpoet-automation-stats-item { diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/add-step-button.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/add-step-button.tsx index 037be8c47e..dea4926b47 100644 --- a/mailpoet/assets/js/src/automation/editor/components/automation/add-step-button.tsx +++ b/mailpoet/assets/js/src/automation/editor/components/automation/add-step-button.tsx @@ -6,9 +6,14 @@ import { AutomationCompositeContext } from './context'; type Props = { onClick?: (element: HTMLButtonElement) => void; previousStepId: string; + index: number; }; -export function AddStepButton({ onClick, previousStepId }: Props): JSX.Element { +export function AddStepButton({ + onClick, + previousStepId, + index, +}: Props): JSX.Element { const compositeState = useContext(AutomationCompositeContext); return ( { event.stopPropagation(); const button = (event.target as HTMLElement).closest('button'); diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/add-trigger.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/add-trigger.tsx index 12e27c7bc3..621679b518 100644 --- a/mailpoet/assets/js/src/automation/editor/components/automation/add-trigger.tsx +++ b/mailpoet/assets/js/src/automation/editor/components/automation/add-trigger.tsx @@ -3,16 +3,17 @@ import { __unstableCompositeItem as CompositeItem } from '@wordpress/components' import { Icon, plus } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; -import { AutomationCompositeContext } from './context'; +import { AutomationContext, AutomationCompositeContext } from './context'; import { Step } from './types'; import { storeName } from '../../store'; type Props = { step: Step; - context: 'edit' | 'view'; + index: number; }; -export function AddTrigger({ step, context }: Props): JSX.Element { +export function AddTrigger({ step, index }: Props): JSX.Element { + const { context } = useContext(AutomationContext); const compositeState = useContext(AutomationCompositeContext); const { setInserterPopover } = useDispatch(storeName); @@ -22,6 +23,7 @@ export function AddTrigger({ step, context }: Props): JSX.Element { role="treeitem" className="mailpoet-automation-add-trigger" data-previous-step-id={step.id} + data-index={index} focusable onClick={ context === 'edit' diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/context.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/context.tsx index 43491fb103..497468867e 100644 --- a/mailpoet/assets/js/src/automation/editor/components/automation/context.tsx +++ b/mailpoet/assets/js/src/automation/editor/components/automation/context.tsx @@ -1,5 +1,10 @@ import { __unstableUseCompositeState as useCompositeState } from '@wordpress/components'; import { createContext } from '@wordpress/element'; +type AutomationContextType = { context: 'edit' | 'view' }; + +export const AutomationContext = + createContext(undefined); + export const AutomationCompositeContext = createContext>(undefined); diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/flow-ending.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/flow-ending.tsx new file mode 100644 index 0000000000..f01623f9b0 --- /dev/null +++ b/mailpoet/assets/js/src/automation/editor/components/automation/flow-ending.tsx @@ -0,0 +1,20 @@ +import { check, Icon } from '@wordpress/icons'; +import { FlowSeparator } from './flow-separator'; +import { Step as StepData } from './types'; + +type Props = { + stepData: StepData; + index: number; +}; + +export function FlowEnding({ stepData, index }: Props): JSX.Element { + return ( +
+ + +
+ ); +} diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/flow-separator.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/flow-separator.tsx new file mode 100644 index 0000000000..ea065dd9f5 --- /dev/null +++ b/mailpoet/assets/js/src/automation/editor/components/automation/flow-separator.tsx @@ -0,0 +1,39 @@ +import { useContext, useMemo } from 'react'; +import { Hooks } from 'wp-js-hooks'; +import { AutomationContext } from './context'; +import { Separator } from './separator'; +import { Step as StepData } from './types'; +import { RenderStepSeparatorType } from '../../../types/filters'; + +type Props = { + stepData: StepData; + index: number; +}; + +export function FlowSeparator(props: Props): JSX.Element { + const { context } = useContext(AutomationContext); + const renderSeparator = useMemo( + (): RenderStepSeparatorType => + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + Hooks.applyFilters( + 'mailpoet.automation.render_step_separator', + (previousStepData: StepData, index: number) => ( + <> + {previousStepData.next_steps.length > 1 && ( +
+ )} + + + ), + context, + ), + [context], + ); + return renderSeparator(props.stepData, props.index); +} diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/flow-step.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/flow-step.tsx new file mode 100644 index 0000000000..c325aaa6cc --- /dev/null +++ b/mailpoet/assets/js/src/automation/editor/components/automation/flow-step.tsx @@ -0,0 +1,45 @@ +import { useContext, useMemo } from 'react'; +import { useSelect } from '@wordpress/data'; +import { Hooks } from 'wp-js-hooks'; +import { AddTrigger } from './add-trigger'; +import { AutomationContext } from './context'; +import { Step } from './step'; +import { Step as StepData } from './types'; +import { storeName } from '../../store'; +import { RenderStepType } from '../../../types/filters'; + +type Props = { + stepData: StepData; + index: number; +}; + +export function FlowStep(props: Props): JSX.Element { + const { context } = useContext(AutomationContext); + const selectedStep = useSelect( + (select) => select(storeName).getSelectedStep(), + [], + ); + + const renderStep = useMemo( + (): RenderStepType => + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + Hooks.applyFilters( + 'mailpoet.automation.render_step', + (stepData: StepData, index: number) => ( + <> + {stepData.type === 'root' ? ( + + ) : ( + + )} + + ), + context, + ), + [selectedStep, context], + ); + return renderStep(props.stepData, props.index); +} diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/flow.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/flow.tsx new file mode 100644 index 0000000000..34d4d804d0 --- /dev/null +++ b/mailpoet/assets/js/src/automation/editor/components/automation/flow.tsx @@ -0,0 +1,55 @@ +import { useSelect } from '@wordpress/data'; +import { FlowEnding } from './flow-ending'; +import { FlowSeparator } from './flow-separator'; +import { FlowStep } from './flow-step'; +import { Step as StepData } from './types'; +import { storeName } from '../../store'; + +type Props = { + stepData: StepData; + row: number; +}; + +export function Flow({ stepData, row }: Props): JSX.Element { + const stepMap = useSelect( + (select) => select(storeName).getAutomationData()?.steps, + [], + ); + + const nextSteps = + stepData.next_steps.length === 0 ? [{ id: null }] : stepData.next_steps; + + return ( + <> + {nextSteps.length > 1 && ( +
+
+
+
+ )} + +
+ {nextSteps.map(({ id }, i) => { + const nextStep = stepMap[id]; + + // when step under root is not a trigger, insert "add trigger" placeholder + const nextStepData = + row === 0 && nextStep?.type !== 'trigger' + ? { ...stepMap.root, next_steps: [{ id }] } + : nextStep; + + return nextStepData ? ( +
+ {row > 0 && } + + +
+ ) : ( + // eslint-disable-next-line react/no-array-index-key + + ); + })} +
+ + ); +} diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/index.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/index.tsx index e1ec75f117..79a0f7b2d5 100644 --- a/mailpoet/assets/js/src/automation/editor/components/automation/index.tsx +++ b/mailpoet/assets/js/src/automation/editor/components/automation/index.tsx @@ -1,137 +1,59 @@ -import { Fragment, useMemo } from 'react'; +import { useMemo } from 'react'; import { __unstableComposite as Composite, __unstableUseCompositeState as useCompositeState, } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { Icon, check } from '@wordpress/icons'; -import { Hooks } from 'wp-js-hooks'; -import { AutomationCompositeContext } from './context'; +import { AutomationCompositeContext, AutomationContext } from './context'; import { EmptyAutomation } from './empty-automation'; -import { Separator } from './separator'; -import { Step } from './step'; -import { Step as StepData } from './types'; import { InserterPopover } from '../inserter-popover'; import { storeName } from '../../store'; -import { AddTrigger } from './add-trigger'; import { Statistics } from './statistics'; -import { - RenderStepSeparatorType, - RenderStepType, -} from '../../../types/filters'; +import { Flow } from './flow'; type AutomationProps = { context: 'edit' | 'view'; }; + export function Automation({ context }: AutomationProps): JSX.Element { - const { automationData, selectedStep } = useSelect( - (select) => ({ - automationData: select(storeName).getAutomationData(), - selectedStep: select(storeName).getSelectedStep(), - }), + const automationData = useSelect( + (select) => select(storeName).getAutomationData(), [], ); + const automationContext = useMemo(() => ({ context }), [context]); + const compositeState = useCompositeState({ orientation: 'vertical', wrap: 'horizontal', shift: true, }); - const stepMap = automationData?.steps ?? undefined; - - // serialize steps (for now, we support only one trigger and linear automations) - const steps = useMemo(() => { - const stepArray = [stepMap.root]; - - // eslint-disable-next-line no-constant-condition - while (true) { - const lastStep = stepArray[stepArray.length - 1]; - if (!lastStep || lastStep.next_steps.length === 0) { - break; - } - stepArray.push(stepMap[lastStep.next_steps[0].id]); - } - return stepArray.slice(1); - }, [stepMap]); - - const renderStep = useMemo( - (): RenderStepType => - Hooks.applyFilters( - 'mailpoet.automation.render_step', - (stepData: StepData) => - stepData.type === 'root' ? ( - - ) : ( - - ), - context, - ), - [selectedStep, context], - ); - - const renderSeparator = useMemo( - (): RenderStepSeparatorType => - Hooks.applyFilters( - 'mailpoet.automation.render_step_separator', - (previousStepData: StepData) => ( - - ), - context, - ), - [context], - ); - if (!automationData) { return ; } return ( - - -
- - {stepMap.root.next_steps.length === 0 ? ( - <> - {renderStep(stepMap.root)} - {renderSeparator(stepMap.root)} - - ) : ( - stepMap.root.next_steps.map( - ({ id }) => - stepMap[id]?.type !== 'trigger' && ( - - {renderStep(stepMap.root)} - {renderSeparator(stepMap.root)} - - ), - ) - )} - {steps.map((step) => ( - - {renderStep(step)} - {renderSeparator(step)} - - ))} - -
-
- - - + + + +
+ +
+ +
+
+
+ + + + ); } diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/separator.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/separator.tsx index 9bd1ac0cd8..d5315ee865 100644 --- a/mailpoet/assets/js/src/automation/editor/components/automation/separator.tsx +++ b/mailpoet/assets/js/src/automation/editor/components/automation/separator.tsx @@ -4,9 +4,10 @@ import { storeName } from '../../store'; type Props = { previousStepId: string; + index: number; }; -export function Separator({ previousStepId }: Props): JSX.Element { +export function Separator({ previousStepId, index }: Props): JSX.Element { const { setInserterPopover } = dispatch(storeName); return ( @@ -16,6 +17,7 @@ export function Separator({ previousStepId }: Props): JSX.Element { setInserterPopover({ anchor: button, type: 'steps' }) } previousStepId={previousStepId} + index={index} />
); diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/step-more-menu.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/step-more-menu.tsx index f49a82575c..a0aee256de 100644 --- a/mailpoet/assets/js/src/automation/editor/components/automation/step-more-menu.tsx +++ b/mailpoet/assets/js/src/automation/editor/components/automation/step-more-menu.tsx @@ -1,18 +1,19 @@ -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { DropdownMenu } from '@wordpress/components'; import { moreVertical, trash } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { Hooks } from 'wp-js-hooks'; import { PremiumModal } from 'common/premium-modal'; +import { AutomationContext } from './context'; import { Step as StepData } from './types'; import { StepMoreControlsType } from '../../../types/filters'; type Props = { step: StepData; - context: 'edit' | 'view'; }; -export function StepMoreMenu({ step, context }: Props): JSX.Element { +export function StepMoreMenu({ step }: Props): JSX.Element { + const { context } = useContext(AutomationContext); const [showModal, setShowModal] = useState(false); const moreControls: StepMoreControlsType = Hooks.applyFilters( diff --git a/mailpoet/assets/js/src/automation/editor/components/automation/step.tsx b/mailpoet/assets/js/src/automation/editor/components/automation/step.tsx index 0200e575e1..588aec9233 100644 --- a/mailpoet/assets/js/src/automation/editor/components/automation/step.tsx +++ b/mailpoet/assets/js/src/automation/editor/components/automation/step.tsx @@ -5,7 +5,7 @@ import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { blockMeta } from '@wordpress/icons'; import { __, _x } from '@wordpress/i18n'; import { Hooks } from 'wp-js-hooks'; -import { AutomationCompositeContext } from './context'; +import { AutomationContext, AutomationCompositeContext } from './context'; import { StepFilters } from './step-filters'; import { StepMoreMenu } from './step-more-menu'; import { Step as StepData } from './types'; @@ -43,10 +43,9 @@ const getUnknownStepType = (step: StepData): StepType => { type Props = { step: StepData; isSelected: boolean; - context: 'edit' | 'view'; }; -export function Step({ step, isSelected, context }: Props): JSX.Element { +export function Step({ step, isSelected }: Props): JSX.Element { const { stepType, error } = useSelect( (select) => ({ stepType: select(storeName).getStepType(step.key), @@ -55,6 +54,7 @@ export function Step({ step, isSelected, context }: Props): JSX.Element { [step], ); const { openSidebar, selectStep } = useDispatch(storeName); + const { context } = useContext(AutomationContext); const compositeState = useContext(AutomationCompositeContext); const { batch } = useRegistry(); @@ -80,7 +80,7 @@ export function Step({ step, isSelected, context }: Props): JSX.Element { return (
- + ; export type AddStepCallbackType = (item?: Item) => void; // mailpoet.automation.render_step -export type RenderStepType = (step: Step) => JSX.Element; +export type RenderStepType = (step: Step, index: number) => JSX.Element; // mailpoet.automation.step.more export type StepMoreType = JSX.Element | null; @@ -39,7 +39,10 @@ export type StepMoreType = JSX.Element | null; export type RenderStepFooterType = JSX.Element | null; // mailpoet.automation.render_step_separator -export type RenderStepSeparatorType = (step: Step) => JSX.Element; +export type RenderStepSeparatorType = ( + step: Step, + index: number, +) => JSX.Element; // mailpoet.automation.editor.create_store export type EditorStoreConfigType = EditorStoreConfig;