splines!
This commit is contained in:
@@ -432,7 +432,9 @@ export default function TaskInspector({
|
||||
className={`border rounded-lg bg-gray-50 overflow-hidden transition-all duration-300 ${
|
||||
isFlashing
|
||||
? "border-blue-400 ring-2 ring-blue-300 shadow-md shadow-blue-100 animate-[flash-highlight_1.5s_ease-out]"
|
||||
: "border-gray-200"
|
||||
: highlightTransitionIndex === ti
|
||||
? "border-blue-400 ring-1 ring-blue-200 bg-blue-50/40"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
{/* Transition header */}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo, useCallback, useRef, useState } from "react";
|
||||
import { Trash2, GripVertical, Play, Octagon } from "lucide-react";
|
||||
import { Trash2, GripVertical, Play, Octagon, Info } from "lucide-react";
|
||||
import type { WorkflowTask, TransitionPreset } from "@/types/workflow";
|
||||
import {
|
||||
PRESET_LABELS,
|
||||
@@ -75,7 +75,7 @@ function hasActiveTransition(
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a short summary of outgoing transitions for the node body.
|
||||
* Compute a short summary of outgoing transitions for the tooltip.
|
||||
*/
|
||||
function transitionSummary(task: WorkflowTask): string | null {
|
||||
if (!task.next || task.next.length === 0) return null;
|
||||
@@ -93,6 +93,68 @@ function transitionSummary(task: WorkflowTask): string | null {
|
||||
return `${totalTargets} target${totalTargets !== 1 ? "s" : ""} via ${task.next.length} transition${task.next.length !== 1 ? "s" : ""}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is "populated" (non-null, non-undefined, non-empty-string).
|
||||
*/
|
||||
function hasValue(value: unknown): boolean {
|
||||
if (value === null || value === undefined) return false;
|
||||
if (typeof value === "string" && value.trim() === "") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries from task.input that actually have values.
|
||||
*/
|
||||
function getPopulatedInputs(
|
||||
input: Record<string, unknown>,
|
||||
): [string, unknown][] {
|
||||
return Object.entries(input).filter(([, v]) => hasValue(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a value for inline display on the card — keep it short.
|
||||
*/
|
||||
function formatValueShort(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
if (value.length > 28) return value.slice(0, 25) + "…";
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.length} items]`;
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return `{${Object.keys(value).length} keys}`;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a value for the tooltip — can be slightly longer.
|
||||
*/
|
||||
function formatValueTooltip(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
if (value.length > 40) return value.slice(0, 37) + "…";
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.length} items]`;
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const keys = Object.keys(value);
|
||||
if (keys.length <= 2) {
|
||||
return `{${keys.join(", ")}}`;
|
||||
}
|
||||
return `{${keys.length} keys}`;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function TaskNodeInner({
|
||||
task,
|
||||
isSelected,
|
||||
@@ -109,6 +171,8 @@ function TaskNodeInner({
|
||||
const [hoveredHandle, setHoveredHandle] = useState<TransitionPreset | null>(
|
||||
null,
|
||||
);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const tooltipTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
@@ -119,6 +183,7 @@ function TaskNodeInner({
|
||||
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
setShowTooltip(false);
|
||||
dragOffset.current = {
|
||||
x: e.clientX - task.position.x,
|
||||
y: e.clientY - task.position.y,
|
||||
@@ -174,6 +239,19 @@ function TaskNodeInner({
|
||||
[task.id, onStartConnection],
|
||||
);
|
||||
|
||||
const handleBodyMouseEnter = useCallback(() => {
|
||||
if (isDragging) return;
|
||||
tooltipTimeout.current = setTimeout(() => setShowTooltip(true), 400);
|
||||
}, [isDragging]);
|
||||
|
||||
const handleBodyMouseLeave = useCallback(() => {
|
||||
if (tooltipTimeout.current) {
|
||||
clearTimeout(tooltipTimeout.current);
|
||||
tooltipTimeout.current = null;
|
||||
}
|
||||
setShowTooltip(false);
|
||||
}, []);
|
||||
|
||||
const isConnectionTarget = connectingFrom !== null;
|
||||
|
||||
const borderColor = isSelected
|
||||
@@ -197,6 +275,49 @@ function TaskNodeInner({
|
||||
return ct === "custom";
|
||||
}).length;
|
||||
|
||||
// Inputs that actually have values
|
||||
const populatedInputs = getPopulatedInputs(task.input);
|
||||
const populatedCount = populatedInputs.length;
|
||||
|
||||
// Show inline if 1–2 populated inputs
|
||||
const showInlineInputs = populatedCount > 0 && populatedCount <= 2;
|
||||
|
||||
// Build tooltip lines
|
||||
const tooltipLines: string[] = [];
|
||||
if (populatedCount > 0) {
|
||||
tooltipLines.push(
|
||||
`${populatedCount} input${populatedCount !== 1 ? "s" : ""} configured`,
|
||||
);
|
||||
// Show all input key-values in tooltip when > 2
|
||||
if (populatedCount > 2) {
|
||||
for (const [key, val] of populatedInputs) {
|
||||
tooltipLines.push(` ${key}: ${formatValueTooltip(val)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (summary) {
|
||||
tooltipLines.push(summary);
|
||||
}
|
||||
if (customTransitionCount > 0) {
|
||||
tooltipLines.push(
|
||||
`${customTransitionCount} custom transition${customTransitionCount !== 1 ? "s" : ""}`,
|
||||
);
|
||||
}
|
||||
if (task.delay) {
|
||||
tooltipLines.push(`Delay: ${task.delay}s`);
|
||||
}
|
||||
if (task.with_items) {
|
||||
tooltipLines.push("with_items iteration");
|
||||
}
|
||||
if (task.retry) {
|
||||
tooltipLines.push(`Retry: ${task.retry.count}×`);
|
||||
}
|
||||
if (task.timeout) {
|
||||
tooltipLines.push(`Timeout: ${task.timeout}s`);
|
||||
}
|
||||
|
||||
const hasTooltipContent = tooltipLines.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={nodeRef}
|
||||
@@ -243,7 +364,11 @@ function TaskNodeInner({
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-2.5 py-2">
|
||||
<div
|
||||
className="px-2.5 py-2 relative"
|
||||
onMouseEnter={handleBodyMouseEnter}
|
||||
onMouseLeave={handleBodyMouseLeave}
|
||||
>
|
||||
{hasAction ? (
|
||||
<div className="font-mono text-[11px] text-gray-600 truncate">
|
||||
{task.action}
|
||||
@@ -254,19 +379,25 @@ function TaskNodeInner({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input summary */}
|
||||
{Object.keys(task.input).length > 0 && (
|
||||
<div className="mt-1.5 text-[10px] text-gray-400">
|
||||
{Object.keys(task.input).length} input
|
||||
{Object.keys(task.input).length !== 1 ? "s" : ""}
|
||||
{/* Inline inputs (1–2 populated) */}
|
||||
{showInlineInputs && (
|
||||
<div className="mt-1.5 space-y-0.5">
|
||||
{populatedInputs.map(([key, val]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-baseline gap-1 text-[10px] leading-tight"
|
||||
>
|
||||
<span className="text-gray-400 font-medium shrink-0">
|
||||
{key}:
|
||||
</span>
|
||||
<span className="text-gray-600 truncate font-mono">
|
||||
{formatValueShort(val)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Transition summary */}
|
||||
{summary && (
|
||||
<div className="mt-1 text-[10px] text-gray-400">{summary}</div>
|
||||
)}
|
||||
|
||||
{/* Delay badge */}
|
||||
{task.delay && (
|
||||
<div className="mt-1 inline-block px-1.5 py-0.5 bg-yellow-50 border border-yellow-200 rounded text-[10px] text-yellow-700 truncate max-w-full">
|
||||
@@ -288,11 +419,36 @@ function TaskNodeInner({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom transitions badge */}
|
||||
{customTransitionCount > 0 && (
|
||||
<div className="mt-1 inline-block px-1.5 py-0.5 bg-violet-50 border border-violet-200 rounded text-[10px] text-violet-700 ml-1">
|
||||
{customTransitionCount} custom transition
|
||||
{customTransitionCount !== 1 ? "s" : ""}
|
||||
{/* Info icon hint — shown when there's tooltip content */}
|
||||
{hasTooltipContent && (
|
||||
<div className="absolute top-1.5 right-1.5">
|
||||
<Info className="w-3 h-3 text-gray-300" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tooltip */}
|
||||
{showTooltip && hasTooltipContent && (
|
||||
<div
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-[100] pointer-events-none"
|
||||
style={{ minWidth: 180, maxWidth: 260 }}
|
||||
>
|
||||
<div className="bg-gray-900 text-white text-[10px] leading-relaxed rounded-md shadow-xl px-2.5 py-2 whitespace-pre-wrap">
|
||||
{tooltipLines.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={
|
||||
line.startsWith(" ")
|
||||
? "pl-2 text-gray-300 font-mono"
|
||||
: i > 0
|
||||
? "mt-1 border-t border-gray-700 pt-1"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 -bottom-1 w-2 h-2 bg-gray-900 rotate-45" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,12 @@ import { useState, useCallback, useRef, useMemo } from "react";
|
||||
import TaskNode from "./TaskNode";
|
||||
import type { TransitionPreset } from "./TaskNode";
|
||||
import WorkflowEdges from "./WorkflowEdges";
|
||||
import type { EdgeHoverInfo } from "./WorkflowEdges";
|
||||
import type { WorkflowTask, WorkflowEdge } from "@/types/workflow";
|
||||
import type { EdgeHoverInfo, SelectedEdgeInfo } from "./WorkflowEdges";
|
||||
import type {
|
||||
WorkflowTask,
|
||||
WorkflowEdge,
|
||||
NodePosition,
|
||||
} from "@/types/workflow";
|
||||
import {
|
||||
deriveEdges,
|
||||
generateUniqueTaskName,
|
||||
@@ -55,6 +59,10 @@ export default function WorkflowCanvas({
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
const [selectedEdge, setSelectedEdge] = useState<SelectedEdgeInfo | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const allTaskNames = useMemo(() => tasks.map((t) => t.name), [tasks]);
|
||||
|
||||
const edges: WorkflowEdge[] = useMemo(() => deriveEdges(tasks), [tasks]);
|
||||
@@ -73,10 +81,12 @@ export default function WorkflowCanvas({
|
||||
setMousePosition(null);
|
||||
} else {
|
||||
onSelectTask(null);
|
||||
setSelectedEdge(null);
|
||||
onEdgeClick?.(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onSelectTask, connectingFrom],
|
||||
[onSelectTask, onEdgeClick, connectingFrom],
|
||||
);
|
||||
|
||||
const handleCanvasMouseMove = useCallback(
|
||||
@@ -113,8 +123,102 @@ export default function WorkflowCanvas({
|
||||
const handleStartConnection = useCallback(
|
||||
(taskId: string, preset: TransitionPreset) => {
|
||||
setConnectingFrom({ taskId, preset });
|
||||
setSelectedEdge(null);
|
||||
onEdgeClick?.(null);
|
||||
},
|
||||
[],
|
||||
[onEdgeClick],
|
||||
);
|
||||
|
||||
/** Handle edge click: select the edge and propagate to parent */
|
||||
const handleEdgeClick = useCallback(
|
||||
(info: EdgeHoverInfo | null) => {
|
||||
if (info) {
|
||||
setSelectedEdge({
|
||||
from: info.taskId,
|
||||
to: info.targetTaskId,
|
||||
transitionIndex: info.transitionIndex,
|
||||
});
|
||||
} else {
|
||||
setSelectedEdge(null);
|
||||
}
|
||||
onEdgeClick?.(info);
|
||||
},
|
||||
[onEdgeClick],
|
||||
);
|
||||
|
||||
/** Handle selecting a task (also clears edge selection) */
|
||||
const handleSelectTask = useCallback(
|
||||
(taskId: string | null) => {
|
||||
onSelectTask(taskId);
|
||||
if (taskId !== null) {
|
||||
// Keep selected edge if the task being selected is part of it
|
||||
// (i.e. user clicked the source task of the edge via edge click)
|
||||
// Otherwise clear it
|
||||
if (selectedEdge && selectedEdge.from !== taskId) {
|
||||
setSelectedEdge(null);
|
||||
onEdgeClick?.(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onSelectTask, onEdgeClick, selectedEdge],
|
||||
);
|
||||
|
||||
/** Update waypoints for a specific edge */
|
||||
const handleWaypointUpdate = useCallback(
|
||||
(
|
||||
fromTaskId: string,
|
||||
transitionIndex: number,
|
||||
targetTaskName: string,
|
||||
waypoints: NodePosition[],
|
||||
) => {
|
||||
const task = tasks.find((t) => t.id === fromTaskId);
|
||||
if (!task || !task.next || transitionIndex >= task.next.length) return;
|
||||
|
||||
const updatedNext = [...task.next];
|
||||
const transition = { ...updatedNext[transitionIndex] };
|
||||
const edgeWaypoints = { ...(transition.edge_waypoints || {}) };
|
||||
|
||||
if (waypoints.length > 0) {
|
||||
edgeWaypoints[targetTaskName] = waypoints;
|
||||
} else {
|
||||
delete edgeWaypoints[targetTaskName];
|
||||
}
|
||||
|
||||
transition.edge_waypoints =
|
||||
Object.keys(edgeWaypoints).length > 0 ? edgeWaypoints : undefined;
|
||||
updatedNext[transitionIndex] = transition;
|
||||
onUpdateTask(fromTaskId, { next: updatedNext });
|
||||
},
|
||||
[tasks, onUpdateTask],
|
||||
);
|
||||
|
||||
/** Update label position for a specific edge */
|
||||
const handleLabelPositionUpdate = useCallback(
|
||||
(
|
||||
fromTaskId: string,
|
||||
transitionIndex: number,
|
||||
targetTaskName: string,
|
||||
position: number | undefined,
|
||||
) => {
|
||||
const task = tasks.find((t) => t.id === fromTaskId);
|
||||
if (!task || !task.next || transitionIndex >= task.next.length) return;
|
||||
|
||||
const updatedNext = [...task.next];
|
||||
const transition = { ...updatedNext[transitionIndex] };
|
||||
const labelPositions = { ...(transition.label_positions || {}) };
|
||||
|
||||
if (position) {
|
||||
labelPositions[targetTaskName] = position;
|
||||
} else {
|
||||
delete labelPositions[targetTaskName];
|
||||
}
|
||||
|
||||
transition.label_positions =
|
||||
Object.keys(labelPositions).length > 0 ? labelPositions : undefined;
|
||||
updatedNext[transitionIndex] = transition;
|
||||
onUpdateTask(fromTaskId, { next: updatedNext });
|
||||
},
|
||||
[tasks, onUpdateTask],
|
||||
);
|
||||
|
||||
const handleCompleteConnection = useCallback(
|
||||
@@ -211,7 +315,10 @@ export default function WorkflowCanvas({
|
||||
tasks={tasks}
|
||||
connectingFrom={connectingFrom}
|
||||
mousePosition={mousePosition}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
selectedEdge={selectedEdge}
|
||||
onWaypointUpdate={handleWaypointUpdate}
|
||||
onLabelPositionUpdate={handleLabelPositionUpdate}
|
||||
/>
|
||||
|
||||
{/* Task nodes */}
|
||||
@@ -222,7 +329,7 @@ export default function WorkflowCanvas({
|
||||
isSelected={task.id === selectedTaskId}
|
||||
isStartNode={startingTaskIds.has(task.id)}
|
||||
allTaskNames={allTaskNames}
|
||||
onSelect={onSelectTask}
|
||||
onSelect={handleSelectTask}
|
||||
onDelete={onDeleteTask}
|
||||
onPositionChange={handlePositionChange}
|
||||
onStartConnection={handleStartConnection}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,10 @@ export interface TaskTransition {
|
||||
label?: string;
|
||||
/** Custom color for the transition edge (CSS color string, e.g., "#ff6600") */
|
||||
color?: string;
|
||||
/** Intermediate waypoints per target task (keyed by target task name) for edge routing */
|
||||
edge_waypoints?: Record<string, NodePosition[]>;
|
||||
/** Label position per target task as t-parameter (0–1) along the edge path */
|
||||
label_positions?: Record<string, number>;
|
||||
}
|
||||
|
||||
/** A task node in the workflow builder */
|
||||
@@ -144,6 +148,8 @@ export interface WorkflowEdge {
|
||||
from: string;
|
||||
/** Target task ID */
|
||||
to: string;
|
||||
/** Target task name (stable key for waypoints) */
|
||||
toName: string;
|
||||
/** Visual type of transition (derived from `when`) */
|
||||
type: EdgeType;
|
||||
/** Label to display on the edge */
|
||||
@@ -152,6 +158,10 @@ export interface WorkflowEdge {
|
||||
transitionIndex: number;
|
||||
/** Custom color override for the edge (CSS color string) */
|
||||
color?: string;
|
||||
/** Intermediate waypoints for this specific edge */
|
||||
waypoints?: NodePosition[];
|
||||
/** Label position as t-parameter (0–1) along the edge path; default 0.5 */
|
||||
labelPosition?: number;
|
||||
}
|
||||
|
||||
/** Complete workflow builder state */
|
||||
@@ -210,6 +220,10 @@ export interface TransitionChartMeta {
|
||||
label?: string;
|
||||
/** Custom color for the transition edge (CSS color string) */
|
||||
color?: string;
|
||||
/** Intermediate waypoints per target task (keyed by target task name) */
|
||||
edge_waypoints?: Record<string, NodePosition[]>;
|
||||
/** Label position per target task as t-parameter (0–1) along the edge path */
|
||||
label_positions?: Record<string, number>;
|
||||
}
|
||||
|
||||
/** Transition as represented in YAML format */
|
||||
@@ -383,11 +397,19 @@ export function builderStateToDefinition(
|
||||
if (t.when) yt.when = t.when;
|
||||
if (t.publish && t.publish.length > 0) yt.publish = t.publish;
|
||||
if (t.do && t.do.length > 0) yt.do = t.do;
|
||||
// Store label/color in __chart_meta__ to avoid polluting the transition namespace
|
||||
if (t.label || t.color) {
|
||||
// Store label/color/waypoints in __chart_meta__ to avoid polluting the transition namespace
|
||||
const hasChartMeta =
|
||||
t.label || t.color || t.edge_waypoints || t.label_positions;
|
||||
if (hasChartMeta) {
|
||||
yt.__chart_meta__ = {};
|
||||
if (t.label) yt.__chart_meta__.label = t.label;
|
||||
if (t.color) yt.__chart_meta__.color = t.color;
|
||||
if (t.edge_waypoints && Object.keys(t.edge_waypoints).length > 0) {
|
||||
yt.__chart_meta__.edge_waypoints = t.edge_waypoints;
|
||||
}
|
||||
if (t.label_positions && Object.keys(t.label_positions).length > 0) {
|
||||
yt.__chart_meta__.label_positions = t.label_positions;
|
||||
}
|
||||
}
|
||||
return yt;
|
||||
});
|
||||
@@ -529,6 +551,8 @@ export function definitionToBuilderState(
|
||||
do: t.do,
|
||||
label: t.__chart_meta__?.label,
|
||||
color: t.__chart_meta__?.color,
|
||||
edge_waypoints: t.__chart_meta__?.edge_waypoints,
|
||||
label_positions: t.__chart_meta__?.label_positions,
|
||||
}));
|
||||
} else {
|
||||
const converted = legacyTransitionsToNext(task);
|
||||
@@ -607,10 +631,13 @@ export function deriveEdges(tasks: WorkflowTask[]): WorkflowEdge[] {
|
||||
edges.push({
|
||||
from: task.id,
|
||||
to: targetId,
|
||||
toName: targetName,
|
||||
type: edgeType,
|
||||
label,
|
||||
transitionIndex: ti,
|
||||
color: transition.color,
|
||||
waypoints: transition.edge_waypoints?.[targetName],
|
||||
labelPosition: transition.label_positions?.[targetName],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -698,7 +725,29 @@ export function removeTaskFromTransitions(
|
||||
.map((t) => {
|
||||
if (!t.do || !t.do.includes(taskName)) return t;
|
||||
const newDo = t.do.filter((name) => name !== taskName);
|
||||
return { ...t, do: newDo.length > 0 ? newDo : undefined };
|
||||
// Also clean up waypoint/label entries for the removed target
|
||||
const updatedWaypoints = t.edge_waypoints
|
||||
? Object.fromEntries(
|
||||
Object.entries(t.edge_waypoints).filter(([k]) => k !== taskName),
|
||||
)
|
||||
: undefined;
|
||||
const updatedLabelPos = t.label_positions
|
||||
? Object.fromEntries(
|
||||
Object.entries(t.label_positions).filter(([k]) => k !== taskName),
|
||||
)
|
||||
: undefined;
|
||||
return {
|
||||
...t,
|
||||
do: newDo.length > 0 ? newDo : undefined,
|
||||
edge_waypoints:
|
||||
updatedWaypoints && Object.keys(updatedWaypoints).length > 0
|
||||
? updatedWaypoints
|
||||
: undefined,
|
||||
label_positions:
|
||||
updatedLabelPos && Object.keys(updatedLabelPos).length > 0
|
||||
? updatedLabelPos
|
||||
: undefined,
|
||||
};
|
||||
})
|
||||
// Keep transitions that still have `do` targets or `publish` directives
|
||||
.filter(
|
||||
@@ -723,12 +772,36 @@ export function renameTaskInTransitions(
|
||||
|
||||
let changed = false;
|
||||
const updated = next.map((t) => {
|
||||
if (!t.do || !t.do.includes(oldName)) return t;
|
||||
const hasDo = t.do && t.do.includes(oldName);
|
||||
const hasWaypoint = t.edge_waypoints && oldName in t.edge_waypoints;
|
||||
const hasLabelPos = t.label_positions && oldName in t.label_positions;
|
||||
|
||||
if (!hasDo && !hasWaypoint && !hasLabelPos) return t;
|
||||
changed = true;
|
||||
return {
|
||||
...t,
|
||||
do: t.do.map((name) => (name === oldName ? newName : name)),
|
||||
};
|
||||
|
||||
const result = { ...t };
|
||||
|
||||
if (hasDo) {
|
||||
result.do = t.do!.map((name) => (name === oldName ? newName : name));
|
||||
}
|
||||
|
||||
if (hasWaypoint && t.edge_waypoints) {
|
||||
const entries = Object.entries(t.edge_waypoints).map(([k, v]) => [
|
||||
k === oldName ? newName : k,
|
||||
v,
|
||||
]);
|
||||
result.edge_waypoints = Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
if (hasLabelPos && t.label_positions) {
|
||||
const entries = Object.entries(t.label_positions).map(([k, v]) => [
|
||||
k === oldName ? newName : k,
|
||||
v,
|
||||
]);
|
||||
result.label_positions = Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return changed ? updated : next;
|
||||
|
||||
Reference in New Issue
Block a user