libs/designer/src/lib/ui/connections/edge.tsx (206 lines of code) (raw):
import { useReadOnly } from '../../core/state/designerOptions/designerOptionsSelectors';
import { useActionMetadata, useNodeEdgeTargets, useNodeMetadata } from '../../core/state/workflow/workflowSelectors';
import { DropZone } from './dropzone';
import { ArrowCap } from './dynamicsvgs/arrowCap';
import { CollapsedRunAfterIndicator, RunAfterIndicator } from './runAfterIndicator';
import type { LogicAppsV2 } from '@microsoft/logic-apps-shared';
import { containsIdTag, removeIdTag, getEdgeCenter, RUN_AFTER_STATUS, useEdgeIndex } from '@microsoft/logic-apps-shared';
import type { ElkExtendedEdge } from 'elkjs/lib/elk-api';
import type React from 'react';
import { memo, useMemo } from 'react';
import { EdgeLabelRenderer, getSmoothStepPath, useReactFlow, type EdgeProps } from '@xyflow/react';
import { useIsNodeSelectedInOperationPanel } from '../../core/state/panel/panelSelectors';
import { css } from '@fluentui/utilities';
interface EdgeContentProps {
x: number;
y: number;
graphId: string;
parentId?: string;
childId?: string;
isLeaf?: boolean;
tabIndex?: number;
}
const EdgeContent = (props: EdgeContentProps) => (
<EdgeLabelRenderer>
<div
style={{
width: edgeContentWidth,
height: edgeContentHeight,
position: 'absolute',
left: props.x,
top: props.y,
pointerEvents: 'all',
zIndex: 100,
}}
>
<DropZone graphId={props.graphId} parentId={props.parentId} childId={props.childId} isLeaf={props.isLeaf} tabIndex={props.tabIndex} />
</div>
</EdgeLabelRenderer>
);
export interface LogicAppsEdgeProps {
id: string;
source: string;
target: string;
elkEdge?: ElkExtendedEdge;
style?: React.CSSProperties;
}
const edgeContentHeight = 24;
const edgeContentWidth = 200;
const runAfterWidth = 36;
const runAfterHeight = 12;
const ButtonEdge: React.FC<EdgeProps<LogicAppsEdgeProps>> = ({
id,
sourceX,
sourceY,
targetX,
targetY,
source,
target,
sourcePosition,
targetPosition,
style = {},
}) => {
const readOnly = useReadOnly();
const reactFlow = useReactFlow();
const operationData = useActionMetadata(target) as LogicAppsV2.ActionDefinition;
const edgeSources = Object.keys(operationData?.runAfter ?? {});
const edgeTargets = useNodeEdgeTargets(source);
const nodeMetadata = useNodeMetadata(source);
const sourceId = containsIdTag(source) ? removeIdTag(source) : source;
const targetId = containsIdTag(target) ? removeIdTag(target) : target;
const graphId = (containsIdTag(source) ? removeIdTag(source) : undefined) ?? nodeMetadata?.graphId ?? '';
const [centerX, centerY] = getEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
});
const filteredRunAfters: Record<string, string[]> = useMemo(
() =>
Object.entries(operationData?.runAfter ?? {}).reduce((pv: Record<string, string[]>, [id, cv]) => {
if ((cv ?? []).some((status) => status.toUpperCase() !== RUN_AFTER_STATUS.SUCCEEDED)) {
pv[id] = cv;
}
return pv;
}, {}),
[operationData?.runAfter]
);
const numRunAfters = Object.keys(filteredRunAfters).length;
const raIndex: number = useMemo(() => {
const sortedRunAfters = Object.keys(filteredRunAfters)
.slice(0)
.sort((id1, id2) => (reactFlow.getNode(id2)?.position?.x ?? 0) - (reactFlow.getNode(id1)?.position?.x ?? 0));
return sortedRunAfters?.findIndex((key) => key === source);
}, [filteredRunAfters, reactFlow, source]);
const runAfterStatuses = useMemo(() => filteredRunAfters?.[source] ?? [], [filteredRunAfters, source]);
const runAfterCount = Object.keys(filteredRunAfters).length;
const showRunAfter = runAfterStatuses.length && runAfterCount < 6;
const showCollapsedRunAfter = runAfterStatuses.length && runAfterCount > 5 && Object.keys(filteredRunAfters)[0] === source;
const showSourceButton = edgeTargets[edgeTargets.length - 1] === target;
const showTargetButton = edgeSources?.[edgeSources.length - 1] === source;
const multipleSources = edgeSources.length > 1;
const multipleTargets = edgeTargets.length > 1;
const onlyEdge = !multipleSources && !multipleTargets;
const isLeaf = edgeTargets.length === 0;
const runAfterX = targetX - runAfterWidth / 2 + (numRunAfters - 1 - raIndex * 2) * (runAfterWidth / 2 + 4);
const runAfterY = targetY - runAfterHeight;
const [d] = useMemo(() => {
return getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY: (numRunAfters !== 0 ? targetY - runAfterHeight : targetY) - 2, // move up to allow space for run after indicator
targetPosition,
borderRadius: 8,
centerY,
});
}, [numRunAfters, sourcePosition, sourceX, sourceY, targetPosition, targetX, targetY, centerY]);
const tabIndex = useEdgeIndex(id);
const isSourceSelected = useIsNodeSelectedInOperationPanel(sourceId);
const isTargetSelected = useIsNodeSelectedInOperationPanel(targetId);
const highlighted = useMemo(() => isSourceSelected || isTargetSelected, [isSourceSelected, isTargetSelected]);
return (
<>
<defs>
<marker
id={`arrow-end-${id}`}
className={css(highlighted ? 'highlighted' : '')}
viewBox="0 0 20 20"
refX="6"
refY="4"
markerWidth="10"
markerHeight="10"
>
<ArrowCap />
</marker>
</defs>
<path
id={id}
style={style}
className={css('react-flow__edge-path', highlighted ? 'highlighted' : '')}
d={d}
strokeDasharray={showRunAfter ? '4' : '0'}
markerEnd={`url(#arrow-end-${id})`}
/>
{/* ADD ACTION / BRANCH BUTTONS */}
{readOnly ? null : (
<>
{/* TOP BUTTON */}
{((multipleTargets && showSourceButton) || multipleSources) && (
<EdgeContent
x={sourceX - edgeContentWidth / 2}
y={sourceY + 28 - edgeContentHeight / 2}
graphId={graphId}
parentId={source}
childId={multipleTargets ? undefined : target}
tabIndex={tabIndex}
/>
)}
{/* MIDDLE BUTTON */}
{(onlyEdge || (multipleTargets && multipleSources)) && (
<EdgeContent
x={centerX - edgeContentWidth / 2}
y={centerY - edgeContentHeight / 2}
graphId={graphId}
parentId={source}
childId={target}
tabIndex={tabIndex}
/>
)}
{/* BOTTOM BUTTOM */}
{((multipleSources && showTargetButton) || multipleTargets) && (
<EdgeContent
x={targetX - edgeContentWidth / 2}
y={targetY - 32 - edgeContentHeight / 2 - (numRunAfters !== 0 ? 4 : 0)} // Make a little more room for run after
graphId={graphId}
parentId={multipleSources ? undefined : source}
childId={target}
isLeaf={isLeaf}
tabIndex={tabIndex}
/>
)}
</>
)}
{/* RUN AFTER INDICATOR */}
{showRunAfter ? (
<foreignObject id="msla-run-after-traffic-light" width={runAfterWidth} height={runAfterHeight} x={runAfterX} y={runAfterY}>
<RunAfterIndicator statuses={runAfterStatuses} sourceNodeId={source} />
</foreignObject>
) : null}
{/* RUN AFTER INDICATOR WHEN COLLAPSED */}
{showCollapsedRunAfter ? (
<foreignObject
id="msla-run-after-traffic-light"
width={runAfterWidth}
height={runAfterHeight}
x={targetX - runAfterWidth / 2}
y={targetY - runAfterHeight}
>
<CollapsedRunAfterIndicator filteredRunAfters={filteredRunAfters} runAfterCount={runAfterCount} />
</foreignObject>
) : null}
</>
);
};
export default memo(ButtonEdge);