Implement workflow error list popover in header
[MAILPOET-4659]
This commit is contained in:
@ -0,0 +1,32 @@
|
||||
.mailpoet-automation-errors {
|
||||
padding: 8px 0;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-errors-header {
|
||||
font-weight: 600;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.mailpoet-automation-step-error {
|
||||
align-items: center;
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: 9px 12px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: #f6f7f7;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 0 0 0 1.5px #2271b1;
|
||||
outline: none;
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
@import './components-automation-editor/chip';
|
||||
@import './components-automation-editor/dropdown';
|
||||
@import './components-automation-editor/empty-workflow';
|
||||
@import './components-automation-editor/errors';
|
||||
@import './components-automation-editor/panel';
|
||||
@import './components-automation-editor/separator';
|
||||
@import './components-automation-editor/status';
|
||||
|
@ -0,0 +1,169 @@
|
||||
import { ComponentType, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
__unstableComposite as Composite,
|
||||
__unstableCompositeItem as CompositeItem,
|
||||
__unstableUseCompositeState as useCompositeState,
|
||||
Button,
|
||||
Popover as WpPopover,
|
||||
} from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { createContext } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Chip } from '../chip';
|
||||
import { ColoredIcon } from '../icons';
|
||||
import {
|
||||
StepError as StepErrorType,
|
||||
stepSidebarKey,
|
||||
storeName,
|
||||
} from '../../store';
|
||||
|
||||
// properties "offset" and "placement" are missing in WpPopover type definition
|
||||
const Popover: ComponentType<
|
||||
WpPopover.Props & {
|
||||
offset?: number;
|
||||
placement?: string;
|
||||
}
|
||||
> = WpPopover;
|
||||
|
||||
export const ErrorsCompositeContext =
|
||||
createContext<ReturnType<typeof useCompositeState>>(undefined);
|
||||
|
||||
type StepErrorProps = {
|
||||
stepId: string;
|
||||
};
|
||||
|
||||
function StepError({ stepId }: StepErrorProps): JSX.Element {
|
||||
const compositeState = useContext(ErrorsCompositeContext);
|
||||
|
||||
const { steps, workflowData } = useSelect(
|
||||
(select) => ({
|
||||
steps: select(storeName).getSteps(),
|
||||
workflowData: select(storeName).getWorkflowData(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { openSidebar, selectStep } = useDispatch(storeName);
|
||||
|
||||
const stepData = workflowData.steps[stepId];
|
||||
const step = steps.find(({ key }) => key === stepData.key);
|
||||
|
||||
return (
|
||||
<CompositeItem
|
||||
className="mailpoet-automation-step-error"
|
||||
role="listitem"
|
||||
state={compositeState}
|
||||
onClick={() => {
|
||||
openSidebar(stepSidebarKey);
|
||||
selectStep(stepData);
|
||||
}}
|
||||
>
|
||||
<ColoredIcon
|
||||
icon={step.icon}
|
||||
foreground={step.foreground}
|
||||
background={step.background}
|
||||
width="23px"
|
||||
height="23px"
|
||||
/>
|
||||
{step.title}
|
||||
</CompositeItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function Errors(): JSX.Element | null {
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
|
||||
const compositeState = useCompositeState({
|
||||
orientation: 'vertical',
|
||||
shift: true,
|
||||
});
|
||||
|
||||
const { errors, workflowData } = useSelect(
|
||||
(select) => ({
|
||||
errors: select(storeName).getErrors(),
|
||||
workflowData: select(storeName).getWorkflowData(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// walk the steps tree (breadth first) to produce stable error order
|
||||
const stepErrors = useMemo(() => {
|
||||
if (!errors) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const visited = new Map<string, StepErrorType | undefined>();
|
||||
const ids = workflowData.steps.root.next_steps.map(({ id }) => id);
|
||||
while (ids.length > 0) {
|
||||
const id = ids.shift();
|
||||
if (!visited.has(id)) {
|
||||
visited.set(id, errors.steps[id]);
|
||||
workflowData.steps[id]?.next_steps?.forEach((step) =>
|
||||
ids.push(step.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
return [...visited.values()].filter((error) => !!error);
|
||||
}, [errors, workflowData]);
|
||||
|
||||
// automatically open the popover when errors appear
|
||||
const hasErrors = stepErrors.length > 0;
|
||||
useEffect(() => {
|
||||
if (hasErrors) {
|
||||
setShowPopover(true);
|
||||
}
|
||||
}, [hasErrors]);
|
||||
|
||||
if (stepErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() =>
|
||||
setShowPopover((prevState) =>
|
||||
prevState === undefined ? false : !prevState,
|
||||
)
|
||||
}
|
||||
onMouseDown={() =>
|
||||
// Catch and mark a mouse down event from an open popover with "undefined" to avoid closing it
|
||||
// (automatically via click outside) and reopening it right after (via the onClick handler).
|
||||
// The "onClose" method of the popover doesn't pass any events so we can't filter them.
|
||||
setShowPopover((prevState) => (prevState ? undefined : prevState))
|
||||
}
|
||||
style={{ textDecoration: 'none', borderRadius: 99999 }}
|
||||
>
|
||||
<Chip>{stepErrors.length} issues</Chip>
|
||||
</Button>
|
||||
{showPopover && (
|
||||
<Popover
|
||||
offset={10}
|
||||
placement="bottom-end"
|
||||
onClose={() =>
|
||||
setShowPopover((prevState) =>
|
||||
prevState === undefined ? undefined : false,
|
||||
)
|
||||
}
|
||||
>
|
||||
<ErrorsCompositeContext.Provider value={compositeState}>
|
||||
<Composite
|
||||
state={compositeState}
|
||||
role="list"
|
||||
aria-label={__('Workflow errors', 'mailpoet')}
|
||||
className="mailpoet-automation-errors"
|
||||
>
|
||||
<div className="mailpoet-automation-errors-header">
|
||||
{__('The following steps are not fully set:', 'mailpoet')}
|
||||
</div>
|
||||
{stepErrors.map((error) => (
|
||||
<StepError key={error.step_id} stepId={error.step_id} />
|
||||
))}
|
||||
</Composite>
|
||||
</ErrorsCompositeContext.Provider>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -3,9 +3,9 @@ import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { PinnedItems } from '@wordpress/interface';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { DocumentActions } from './document_actions';
|
||||
import { Errors } from './errors';
|
||||
import { InserterToggle } from './inserter_toggle';
|
||||
import { MoreMenu } from './more_menu';
|
||||
import { Chip } from '../chip';
|
||||
import { storeName } from '../../store';
|
||||
import { WorkflowStatus } from '../../../listing/workflow';
|
||||
|
||||
@ -14,10 +14,22 @@ import { WorkflowStatus } from '../../../listing/workflow';
|
||||
// https://github.com/WordPress/gutenberg/blob/0ee78b1bbe9c6f3e6df99f3b967132fa12bef77d/packages/edit-site/src/components/header/index.js
|
||||
|
||||
function ActivateButton(): JSX.Element {
|
||||
const { errors } = useSelect(
|
||||
(select) => ({
|
||||
errors: select(storeName).getErrors(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { activate } = useDispatch(storeName);
|
||||
|
||||
return (
|
||||
<Button isPrimary className="editor-post-publish-button" onClick={activate}>
|
||||
<Button
|
||||
isPrimary
|
||||
className="editor-post-publish-button"
|
||||
onClick={activate}
|
||||
disabled={!!errors}
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
);
|
||||
@ -49,11 +61,10 @@ type Props = {
|
||||
|
||||
export function Header({ showInserterToggle }: Props): JSX.Element {
|
||||
const { setWorkflowName } = useDispatch(storeName);
|
||||
const { workflowName, workflowStatus, errors } = useSelect(
|
||||
const { workflowName, workflowStatus } = useSelect(
|
||||
(select) => ({
|
||||
workflowName: select(storeName).getWorkflowData().name,
|
||||
workflowStatus: select(storeName).getWorkflowData().status,
|
||||
errors: select(storeName).getErrors(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@ -91,9 +102,7 @@ export function Header({ showInserterToggle }: Props): JSX.Element {
|
||||
|
||||
<div className="edit-site-header_end">
|
||||
<div className="edit-site-header__actions">
|
||||
{errors && Object.values(errors.steps).length > 0 && (
|
||||
<Chip>{Object.values(errors.steps).length} issues</Chip>
|
||||
)}
|
||||
<Errors />
|
||||
<SaveDraftButton />
|
||||
{workflowStatus !== WorkflowStatus.ACTIVE && <ActivateButton />}
|
||||
{workflowStatus === WorkflowStatus.ACTIVE && <UpdateButton />}
|
||||
|
Reference in New Issue
Block a user