Implement rendering for multiple automation branches
[MAILPOET-5586]
This commit is contained in:
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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');
|
||||
|
@@ -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'
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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(
|
||||
|
@@ -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"
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user