Implement rendering for multiple automation branches

[MAILPOET-5586]
This commit is contained in:
Jan Jakes
2023-09-18 14:51:20 +02:00
committed by Aschepikov
parent d0726e348e
commit 524298ff41
16 changed files with 305 additions and 125 deletions

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 (
<CompositeItem
@@ -17,6 +22,7 @@ export function AddStepButton({ onClick, previousStepId }: Props): JSX.Element {
className="mailpoet-automation-editor-add-step-button"
focusable
data-previous-step-id={previousStepId}
data-index={index}
onClick={(event) => {
event.stopPropagation();
const button = (event.target as HTMLElement).closest('button');

View File

@@ -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'

View File

@@ -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<AutomationContextType>(undefined);
export const AutomationCompositeContext =
createContext<ReturnType<typeof useCompositeState>>(undefined);

View File

@@ -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 (
<div className="mailpoet-automation-editor-step-wrapper">
<FlowSeparator stepData={stepData} index={index} />
<Icon
className="mailpoet-automation-editor-automation-end"
icon={check}
/>
</div>
);
}

View File

@@ -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 && (
<div
className={
index < previousStepData.next_steps.length / 2
? 'mailpoet-automation-editor-separator-curve-leaf-left'
: 'mailpoet-automation-editor-separator-curve-leaf-right'
}
/>
)}
<Separator previousStepId={previousStepData.id} index={index} />
</>
),
context,
),
[context],
);
return renderSeparator(props.stepData, props.index);
}

View File

@@ -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' ? (
<AddTrigger step={stepData} index={index} />
) : (
<Step
step={stepData}
isSelected={selectedStep && stepData.id === selectedStep.id}
/>
)}
</>
),
context,
),
[selectedStep, context],
);
return renderStep(props.stepData, props.index);
}

View File

@@ -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 && (
<div className="mailpoet-automation-editor-separator-curve-root">
<div className="mailpoet-automation-editor-separator-curve-root-left" />
<div className="mailpoet-automation-editor-separator-curve-root-right" />
</div>
)}
<div className="mailpoet-automation-editor-automation-row">
{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 ? (
<div key={id}>
{row > 0 && <FlowSeparator stepData={stepData} index={i} />}
<FlowStep stepData={nextStepData} index={i} />
<Flow stepData={nextStepData} row={row + 1} />
</div>
) : (
// eslint-disable-next-line react/no-array-index-key
<FlowEnding key={i} stepData={stepData} index={i} />
);
})}
</div>
</>
);
}

View File

@@ -1,97 +1,41 @@
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' ? (
<AddTrigger step={stepData} context={context} />
) : (
<Step
step={stepData}
isSelected={selectedStep && stepData.id === selectedStep.id}
context={context}
/>
),
context,
),
[selectedStep, context],
);
const renderSeparator = useMemo(
(): RenderStepSeparatorType =>
Hooks.applyFilters(
'mailpoet.automation.render_step_separator',
(previousStepData: StepData) => (
<Separator previousStepId={previousStepData.id} />
),
context,
),
[context],
);
if (!automationData) {
return <EmptyAutomation />;
}
return (
<AutomationContext.Provider value={automationContext}>
<AutomationCompositeContext.Provider value={compositeState}>
<Composite
state={compositeState}
@@ -102,36 +46,14 @@ export function Automation({ context }: AutomationProps): JSX.Element {
>
<div className="mailpoet-automation-editor-automation-wrapper">
<Statistics />
{stepMap.root.next_steps.length === 0 ? (
<>
{renderStep(stepMap.root)}
{renderSeparator(stepMap.root)}
</>
) : (
stepMap.root.next_steps.map(
({ id }) =>
stepMap[id]?.type !== 'trigger' && (
<Fragment key={`root-${id}`}>
{renderStep(stepMap.root)}
{renderSeparator(stepMap.root)}
</Fragment>
),
)
)}
{steps.map((step) => (
<Fragment key={step.id}>
{renderStep(step)}
{renderSeparator(step)}
</Fragment>
))}
<Icon
className="mailpoet-automation-editor-automation-end"
icon={check}
/>
<div className="mailpoet-automation-editor-automation-flow">
<Flow stepData={automationData.steps.root} row={0} />
</div>
<div />
</div>
<InserterPopover />
</Composite>
</AutomationCompositeContext.Provider>
</AutomationContext.Provider>
);
}

View File

@@ -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}
/>
</div>
);

View File

@@ -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(

View File

@@ -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 (
<div className="mailpoet-automation-editor-step-wrapper">
<StepMoreMenu step={step} context={context} />
<StepMoreMenu step={step} />
<CompositeItem
state={compositeState}
role="treeitem"

View File

@@ -30,7 +30,7 @@ export type StepMoreControlsType = Record<string, MoreControlType>;
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;