show affected blocks in confirmation dialog when deleting blocks or parameters (#4519)
This commit is contained in:
93
skyvern-frontend/src/components/DeleteConfirmationDialog.tsx
Normal file
93
skyvern-frontend/src/components/DeleteConfirmationDialog.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
type AffectedBlock = {
|
||||||
|
nodeId: string;
|
||||||
|
label: string;
|
||||||
|
hasParameterKeyReference: boolean;
|
||||||
|
hasJinjaReference: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
affectedBlocks: AffectedBlock[];
|
||||||
|
onConfirm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DeleteConfirmationDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
affectedBlocks,
|
||||||
|
onConfirm,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription asChild>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p>{description}</p>
|
||||||
|
{affectedBlocks.length > 0 && (
|
||||||
|
<div className="rounded-md border border-amber-500/50 bg-amber-500/10 p-3">
|
||||||
|
<p className="mb-2 font-medium text-amber-500">
|
||||||
|
The following blocks reference this item and will be
|
||||||
|
updated:
|
||||||
|
</p>
|
||||||
|
<ul className="list-inside list-disc space-y-1 text-sm text-slate-300">
|
||||||
|
{affectedBlocks.map((block) => (
|
||||||
|
<li key={block.nodeId}>
|
||||||
|
<span className="font-medium">{block.label}</span>
|
||||||
|
<span className="text-slate-400">
|
||||||
|
{" "}
|
||||||
|
(
|
||||||
|
{[
|
||||||
|
block.hasParameterKeyReference &&
|
||||||
|
"parameter selector",
|
||||||
|
block.hasJinjaReference && "text field",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ")}
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DeleteConfirmationDialog };
|
||||||
|
export type { AffectedBlock };
|
||||||
@@ -89,6 +89,7 @@ import {
|
|||||||
getWorkflowSettings,
|
getWorkflowSettings,
|
||||||
layout,
|
layout,
|
||||||
removeJinjaReferenceFromNodes,
|
removeJinjaReferenceFromNodes,
|
||||||
|
removeKeyFromNodesParameterKeys,
|
||||||
upgradeWorkflowDefinitionToVersionTwo,
|
upgradeWorkflowDefinitionToVersionTwo,
|
||||||
} from "./workflowEditorUtils";
|
} from "./workflowEditorUtils";
|
||||||
import { getWorkflowErrors } from "./workflowEditorUtils";
|
import { getWorkflowErrors } from "./workflowEditorUtils";
|
||||||
@@ -282,6 +283,11 @@ type Props = {
|
|||||||
onMouseDownCapture?: () => void;
|
onMouseDownCapture?: () => void;
|
||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
onContainerResize?: number;
|
onContainerResize?: number;
|
||||||
|
onRequestDeleteNode?: (
|
||||||
|
nodeId: string,
|
||||||
|
nodeLabel: string,
|
||||||
|
confirmCallback: () => void,
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FlowRenderer({
|
function FlowRenderer({
|
||||||
@@ -300,6 +306,7 @@ function FlowRenderer({
|
|||||||
onMouseDownCapture,
|
onMouseDownCapture,
|
||||||
zIndex,
|
zIndex,
|
||||||
onContainerResize,
|
onContainerResize,
|
||||||
|
onRequestDeleteNode,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const reactFlowInstance = useReactFlow();
|
const reactFlowInstance = useReactFlow();
|
||||||
const debugStore = useDebugStore();
|
const debugStore = useDebugStore();
|
||||||
@@ -347,11 +354,14 @@ function FlowRenderer({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
const doLayout = useCallback(
|
||||||
const layoutedElements = layout(nodes, edges);
|
(nodes: Array<AppNode>, edges: Array<Edge>) => {
|
||||||
setNodes(layoutedElements.nodes);
|
const layoutedElements = layout(nodes, edges);
|
||||||
setEdges(layoutedElements.edges);
|
setNodes(layoutedElements.nodes);
|
||||||
}
|
setEdges(layoutedElements.edges);
|
||||||
|
},
|
||||||
|
[setNodes, setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nodesInitialized) {
|
if (nodesInitialized) {
|
||||||
@@ -446,223 +456,92 @@ function FlowRenderer({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteNode(id: string) {
|
const deleteNode = useCallback(
|
||||||
const node = nodes.find((node) => node.id === id);
|
(id: string) => {
|
||||||
if (!node || !isWorkflowBlockNode(node)) {
|
const node = nodes.find((node) => node.id === id);
|
||||||
return;
|
if (!node || !isWorkflowBlockNode(node)) {
|
||||||
}
|
return;
|
||||||
const nodesToDelete = descendants(nodes, id);
|
|
||||||
const deletedNodeLabel = node.data.label;
|
|
||||||
const newNodes = nodes.filter(
|
|
||||||
(node) => !nodesToDelete.includes(node) && node.id !== id,
|
|
||||||
);
|
|
||||||
const newEdges = edges.flatMap((edge) => {
|
|
||||||
if (edge.source === id) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
if (
|
const nodesToDelete = descendants(nodes, id);
|
||||||
nodesToDelete.some(
|
const deletedNodeLabel = node.data.label;
|
||||||
(node) => node.id === edge.source || node.id === edge.target,
|
const newNodes = nodes.filter(
|
||||||
)
|
(node) => !nodesToDelete.includes(node) && node.id !== id,
|
||||||
) {
|
);
|
||||||
return [];
|
const newEdges = edges.flatMap((edge) => {
|
||||||
}
|
if (edge.source === id) {
|
||||||
if (edge.target === id) {
|
return [];
|
||||||
const nextEdge = edges.find((edge) => edge.source === id);
|
}
|
||||||
if (nextEdge) {
|
if (
|
||||||
// connect the old incoming edge to the next node if both of them exist
|
nodesToDelete.some(
|
||||||
// also take the type of the old edge for plus button edge vs default
|
(node) => node.id === edge.source || node.id === edge.target,
|
||||||
return [
|
)
|
||||||
{
|
) {
|
||||||
...edge,
|
return [];
|
||||||
type: nextEdge.type,
|
}
|
||||||
target: nextEdge.target,
|
if (edge.target === id) {
|
||||||
},
|
const nextEdge = edges.find((edge) => edge.source === id);
|
||||||
];
|
if (nextEdge) {
|
||||||
|
// connect the old incoming edge to the next node if both of them exist
|
||||||
|
// also take the type of the old edge for plus button edge vs default
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...edge,
|
||||||
|
type: nextEdge.type,
|
||||||
|
target: nextEdge.target,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [edge];
|
||||||
}
|
}
|
||||||
return [edge];
|
return [edge];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newNodes.every((node) => node.type === "nodeAdder")) {
|
||||||
|
// No user created nodes left, so return to the empty state.
|
||||||
|
doLayout([], []);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return [edge];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newNodes.every((node) => node.type === "nodeAdder")) {
|
// Step 1: Remove inline {{ deleted_block_output }} references from all nodes
|
||||||
// No user created nodes left, so return to the empty state.
|
const deletedOutputKey = getOutputParameterKey(deletedNodeLabel);
|
||||||
doLayout([], []);
|
const nodesWithRemovedInlineRefs = removeJinjaReferenceFromNodes(
|
||||||
return;
|
newNodes,
|
||||||
}
|
deletedOutputKey,
|
||||||
|
);
|
||||||
|
|
||||||
// Step 1: Remove inline {{ deleted_block_output }} references from all nodes
|
// Step 2: Remove from parameterKeys arrays and handle special cases
|
||||||
const deletedOutputKey = getOutputParameterKey(deletedNodeLabel);
|
const newNodesWithUpdatedParameters = removeKeyFromNodesParameterKeys(
|
||||||
const nodesWithRemovedInlineRefs = removeJinjaReferenceFromNodes(
|
nodesWithRemovedInlineRefs,
|
||||||
newNodes,
|
deletedOutputKey,
|
||||||
deletedOutputKey,
|
deletedNodeLabel,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 2: Remove from parameterKeys arrays and handle special cases
|
workflowChangesStore.setHasChanges(true);
|
||||||
const newNodesWithUpdatedParameters = nodesWithRemovedInlineRefs.map(
|
|
||||||
(node) => {
|
|
||||||
// Clear finallyBlockLabel if the deleted block was the finally block
|
|
||||||
if (
|
|
||||||
node.type === "start" &&
|
|
||||||
node.data.withWorkflowSettings &&
|
|
||||||
node.data.finallyBlockLabel === deletedNodeLabel
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
finallyBlockLabel: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle parameterKeys - filter out the deleted output key
|
doLayout(newNodesWithUpdatedParameters, newEdges);
|
||||||
// Each node type needs a separate branch due to TypeScript union type limitations
|
},
|
||||||
if (node.type === "task") {
|
[nodes, edges, doLayout, workflowChangesStore],
|
||||||
return {
|
);
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "textPrompt") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "login") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "navigation") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "extraction") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "fileDownload") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "action") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "http_request") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "validation") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "codeBlock") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys:
|
|
||||||
node.data.parameterKeys?.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
) ?? null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (node.type === "printPage") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys: node.data.parameterKeys.filter(
|
|
||||||
(parameter) => parameter !== deletedOutputKey,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Handle loop node's loopVariableReference (the active field displayed in UI).
|
|
||||||
// Note: loopValue is a legacy field populated during conversion for backward compatibility.
|
|
||||||
// It's not displayed in UI or sent to backend, so we only clean up loopVariableReference.
|
|
||||||
if (node.type === "loop") {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
loopVariableReference:
|
|
||||||
node.data.loopVariableReference === deletedOutputKey
|
|
||||||
? ""
|
|
||||||
: node.data.loopVariableReference,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
// Use a ref to always have access to the latest deleteNode without causing re-renders
|
||||||
},
|
const deleteNodeRef = useRef(deleteNode);
|
||||||
);
|
useEffect(() => {
|
||||||
workflowChangesStore.setHasChanges(true);
|
deleteNodeRef.current = deleteNode;
|
||||||
|
}, [deleteNode]);
|
||||||
|
|
||||||
doLayout(newNodesWithUpdatedParameters, newEdges);
|
// Callback for requesting node deletion (opens confirmation dialog in parent)
|
||||||
}
|
// Uses ref to avoid recreating on every nodes/edges change while still using latest deleteNode
|
||||||
|
const requestDeleteNode = useCallback(
|
||||||
|
(id: string, label: string) => {
|
||||||
|
if (onRequestDeleteNode) {
|
||||||
|
onRequestDeleteNode(id, label, () => deleteNodeRef.current(id));
|
||||||
|
} else {
|
||||||
|
// Fallback: delete directly if no confirmation handler provided
|
||||||
|
deleteNodeRef.current(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onRequestDeleteNode],
|
||||||
|
);
|
||||||
|
|
||||||
function transmuteNode(id: string, nodeType: string) {
|
function transmuteNode(id: string, nodeType: string) {
|
||||||
const nodeToTransmute = nodes.find((node) => node.id === id);
|
const nodeToTransmute = nodes.find((node) => node.id === id);
|
||||||
@@ -947,13 +826,7 @@ function FlowRenderer({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
<BlockActionContext.Provider
|
<BlockActionContext.Provider
|
||||||
value={{
|
value={{
|
||||||
/**
|
requestDeleteNodeCallback: requestDeleteNode,
|
||||||
* NOTE: defer deletion to next tick to allow React Flow's internal
|
|
||||||
* event handlers to complete; removes a console warning from the
|
|
||||||
* React Flow library
|
|
||||||
*/
|
|
||||||
deleteNodeCallback: (id: string) =>
|
|
||||||
setTimeout(() => deleteNode(id), 0),
|
|
||||||
transmuteNodeCallback: (id: string, nodeName: string) =>
|
transmuteNodeCallback: (id: string, nodeName: string) =>
|
||||||
setTimeout(() => transmuteNode(id, nodeName), 0),
|
setTimeout(() => transmuteNode(id, nodeName), 0),
|
||||||
toggleScriptForNodeCallback: toggleScript,
|
toggleScriptForNodeCallback: toggleScript,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AxiosError } from "axios";
|
|||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
@@ -56,6 +57,7 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
import { DeleteConfirmationDialog } from "@/components/DeleteConfirmationDialog";
|
||||||
import { BrowserStream } from "@/components/BrowserStream";
|
import { BrowserStream } from "@/components/BrowserStream";
|
||||||
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
@@ -83,7 +85,12 @@ import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
|||||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||||
import { WorkflowCacheKeyValuesPanel } from "./panels/WorkflowCacheKeyValuesPanel";
|
import { WorkflowCacheKeyValuesPanel } from "./panels/WorkflowCacheKeyValuesPanel";
|
||||||
import { WorkflowComparisonPanel } from "./panels/WorkflowComparisonPanel";
|
import { WorkflowComparisonPanel } from "./panels/WorkflowComparisonPanel";
|
||||||
import { getWorkflowErrors, getElements } from "./workflowEditorUtils";
|
import {
|
||||||
|
getWorkflowErrors,
|
||||||
|
getElements,
|
||||||
|
getAffectedBlocks,
|
||||||
|
getOutputParameterKey,
|
||||||
|
} from "./workflowEditorUtils";
|
||||||
import { WorkflowHeader } from "./WorkflowHeader";
|
import { WorkflowHeader } from "./WorkflowHeader";
|
||||||
import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel";
|
import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel";
|
||||||
import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery";
|
import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery";
|
||||||
@@ -259,6 +266,39 @@ function Workspace({
|
|||||||
const recordingStore = useRecordingStore();
|
const recordingStore = useRecordingStore();
|
||||||
const cacheKey = workflow?.cache_key ?? "";
|
const cacheKey = workflow?.cache_key ?? "";
|
||||||
|
|
||||||
|
// Block delete confirmation dialog state
|
||||||
|
const [deleteBlockDialogState, setDeleteBlockDialogState] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
nodeId: string | null;
|
||||||
|
nodeLabel: string | null;
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
nodeId: null,
|
||||||
|
nodeLabel: null,
|
||||||
|
});
|
||||||
|
// Use a ref for the callback to avoid storing functions in state
|
||||||
|
const deleteConfirmCallbackRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const affectedBlocksForDelete = useMemo(() => {
|
||||||
|
if (!deleteBlockDialogState.nodeLabel) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const outputKey = getOutputParameterKey(deleteBlockDialogState.nodeLabel);
|
||||||
|
return getAffectedBlocks(nodes, outputKey);
|
||||||
|
}, [nodes, deleteBlockDialogState.nodeLabel]);
|
||||||
|
|
||||||
|
const handleRequestDeleteNode = useCallback(
|
||||||
|
(nodeId: string, nodeLabel: string, confirmCallback: () => void) => {
|
||||||
|
deleteConfirmCallbackRef.current = confirmCallback;
|
||||||
|
setDeleteBlockDialogState({
|
||||||
|
open: true,
|
||||||
|
nodeId,
|
||||||
|
nodeLabel,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const [cacheKeyValue, setCacheKeyValue] = useState(
|
const [cacheKeyValue, setCacheKeyValue] = useState(
|
||||||
cacheKey === ""
|
cacheKey === ""
|
||||||
? ""
|
? ""
|
||||||
@@ -1281,6 +1321,7 @@ function Workspace({
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
initialTitle={initialTitle}
|
initialTitle={initialTitle}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
|
onRequestDeleteNode={handleRequestDeleteNode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* sub panels */}
|
{/* sub panels */}
|
||||||
@@ -1446,6 +1487,7 @@ function Workspace({
|
|||||||
initialTitle={initialTitle}
|
initialTitle={initialTitle}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
onContainerResize={containerResizeTrigger}
|
onContainerResize={containerResizeTrigger}
|
||||||
|
onRequestDeleteNode={handleRequestDeleteNode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1705,6 +1747,33 @@ function Workspace({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<DeleteConfirmationDialog
|
||||||
|
open={deleteBlockDialogState.open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
deleteConfirmCallbackRef.current = null;
|
||||||
|
setDeleteBlockDialogState({
|
||||||
|
open: false,
|
||||||
|
nodeId: null,
|
||||||
|
nodeLabel: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Delete Block"
|
||||||
|
description={`Are you sure you want to delete "${deleteBlockDialogState.nodeLabel}"?`}
|
||||||
|
affectedBlocks={affectedBlocksForDelete}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (deleteConfirmCallbackRef.current) {
|
||||||
|
deleteConfirmCallbackRef.current();
|
||||||
|
}
|
||||||
|
deleteConfirmCallbackRef.current = null;
|
||||||
|
setDeleteBlockDialogState({
|
||||||
|
open: false,
|
||||||
|
nodeId: null,
|
||||||
|
nodeLabel: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function NodeActionMenu({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<DotsHorizontalIcon className="h-6 w-6 cursor-pointer" />
|
<DotsHorizontalIcon className="h-6 w-6 cursor-pointer" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useOnChange } from "@/hooks/useOnChange";
|
|||||||
import { useAutoplayStore } from "@/store/useAutoplayStore";
|
import { useAutoplayStore } from "@/store/useAutoplayStore";
|
||||||
|
|
||||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
import { useRequestDeleteNodeCallback } from "@/routes/workflows/hooks/useRequestDeleteNodeCallback";
|
||||||
import { useTransmuteNodeCallback } from "@/routes/workflows/hooks/useTransmuteNodeCallback";
|
import { useTransmuteNodeCallback } from "@/routes/workflows/hooks/useTransmuteNodeCallback";
|
||||||
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
|
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
|
||||||
import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQuery";
|
import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQuery";
|
||||||
@@ -180,7 +180,7 @@ function NodeHeader({
|
|||||||
initialValue: blockLabel,
|
initialValue: blockLabel,
|
||||||
});
|
});
|
||||||
const blockTitle = workflowBlockTitle[type];
|
const blockTitle = workflowBlockTitle[type];
|
||||||
const deleteNodeCallback = useDeleteNodeCallback();
|
const requestDeleteNodeCallback = useRequestDeleteNodeCallback();
|
||||||
const transmuteNodeCallback = useTransmuteNodeCallback();
|
const transmuteNodeCallback = useTransmuteNodeCallback();
|
||||||
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
|
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
@@ -625,7 +625,7 @@ function NodeHeader({
|
|||||||
<NodeActionMenu
|
<NodeActionMenu
|
||||||
isScriptable={isScriptable}
|
isScriptable={isScriptable}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
deleteNodeCallback(nodeId);
|
requestDeleteNodeCallback(nodeId, blockLabel);
|
||||||
}}
|
}}
|
||||||
onShowScript={() =>
|
onShowScript={() =>
|
||||||
toggleScriptForNodeCallback({ id: nodeId, show: true })
|
toggleScriptForNodeCallback({ id: nodeId, show: true })
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { ParametersState } from "../types";
|
import { ParametersState } from "../types";
|
||||||
import { WorkflowParameterEditPanel } from "./WorkflowParameterEditPanel";
|
import { WorkflowParameterEditPanel } from "./WorkflowParameterEditPanel";
|
||||||
import { MixerVerticalIcon, PlusIcon } from "@radix-ui/react-icons";
|
import { MixerVerticalIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { GarbageIcon } from "@/components/icons/GarbageIcon";
|
import { GarbageIcon } from "@/components/icons/GarbageIcon";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { DialogClose } from "@radix-ui/react-dialog";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -22,7 +12,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useReactFlow } from "@xyflow/react";
|
import { useNodes, useReactFlow } from "@xyflow/react";
|
||||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||||
import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore";
|
import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore";
|
||||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||||
@@ -31,10 +21,14 @@ import {
|
|||||||
WorkflowEditorParameterTypes,
|
WorkflowEditorParameterTypes,
|
||||||
} from "../../types/workflowTypes";
|
} from "../../types/workflowTypes";
|
||||||
import {
|
import {
|
||||||
|
getAffectedBlocks,
|
||||||
getLabelForWorkflowParameterType,
|
getLabelForWorkflowParameterType,
|
||||||
removeJinjaReferenceFromNodes,
|
removeJinjaReferenceFromNodes,
|
||||||
|
removeKeyFromNodesParameterKeys,
|
||||||
replaceJinjaReferenceInNodes,
|
replaceJinjaReferenceInNodes,
|
||||||
} from "../workflowEditorUtils";
|
} from "../workflowEditorUtils";
|
||||||
|
import { DeleteConfirmationDialog } from "@/components/DeleteConfirmationDialog";
|
||||||
|
import { AppNode } from "../nodes";
|
||||||
|
|
||||||
const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16;
|
const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16;
|
||||||
const WORKFLOW_EDIT_PANEL_GAP = 1 * 16;
|
const WORKFLOW_EDIT_PANEL_GAP = 1 * 16;
|
||||||
@@ -62,8 +56,43 @@ function WorkflowParametersPanel({ onMouseDownCapture }: Props) {
|
|||||||
parameter: null,
|
parameter: null,
|
||||||
type: "workflow",
|
type: "workflow",
|
||||||
});
|
});
|
||||||
|
const [deleteDialogState, setDeleteDialogState] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
parameterKey: string | null;
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
parameterKey: null,
|
||||||
|
});
|
||||||
|
const nodes = useNodes<AppNode>();
|
||||||
const { setNodes } = useReactFlow();
|
const { setNodes } = useReactFlow();
|
||||||
|
|
||||||
|
const affectedBlocksForDelete = useMemo(() => {
|
||||||
|
if (!deleteDialogState.parameterKey) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getAffectedBlocks(nodes, deleteDialogState.parameterKey);
|
||||||
|
}, [nodes, deleteDialogState.parameterKey]);
|
||||||
|
|
||||||
|
const handleDeleteParameter = (parameterKey: string) => {
|
||||||
|
setWorkflowParameters(
|
||||||
|
workflowParameters.filter((p) => p.key !== parameterKey),
|
||||||
|
);
|
||||||
|
setHasChanges(true);
|
||||||
|
setNodes((nodes) => {
|
||||||
|
// Step 1: Remove inline {{ parameter.key }} references
|
||||||
|
const nodesWithRemovedRefs = removeJinjaReferenceFromNodes(
|
||||||
|
nodes,
|
||||||
|
parameterKey,
|
||||||
|
);
|
||||||
|
// Step 2: Remove from parameterKeys arrays
|
||||||
|
return removeKeyFromNodesParameterKeys(
|
||||||
|
nodesWithRemovedRefs,
|
||||||
|
parameterKey,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setDeleteDialogState({ open: false, parameterKey: null });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative z-10 w-[25rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl"
|
className="relative z-10 w-[25rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl"
|
||||||
@@ -156,77 +185,17 @@ function WorkflowParametersPanel({ onMouseDownCapture }: Props) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Dialog>
|
<button
|
||||||
<DialogTrigger>
|
type="button"
|
||||||
<GarbageIcon className="size-4 cursor-pointer text-destructive-foreground text-red-600" />
|
onClick={() => {
|
||||||
</DialogTrigger>
|
setDeleteDialogState({
|
||||||
<DialogContent>
|
open: true,
|
||||||
<DialogHeader>
|
parameterKey: parameter.key,
|
||||||
<DialogTitle>Are you sure?</DialogTitle>
|
});
|
||||||
<DialogDescription>
|
}}
|
||||||
This parameter will be deleted.
|
>
|
||||||
</DialogDescription>
|
<GarbageIcon className="size-4 cursor-pointer text-destructive-foreground text-red-600" />
|
||||||
</DialogHeader>
|
</button>
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="secondary">Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => {
|
|
||||||
setWorkflowParameters(
|
|
||||||
workflowParameters.filter(
|
|
||||||
(p) => p.key !== parameter.key,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setHasChanges(true);
|
|
||||||
setNodes((nodes) => {
|
|
||||||
// Step 1: Remove inline {{ parameter.key }} references
|
|
||||||
const nodesWithRemovedRefs =
|
|
||||||
removeJinjaReferenceFromNodes(
|
|
||||||
nodes,
|
|
||||||
parameter.key,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 2: Remove from parameterKeys arrays
|
|
||||||
return nodesWithRemovedRefs.map((node) => {
|
|
||||||
// All node types that have parameterKeys
|
|
||||||
if (
|
|
||||||
node.type === "task" ||
|
|
||||||
node.type === "textPrompt" ||
|
|
||||||
node.type === "login" ||
|
|
||||||
node.type === "navigation" ||
|
|
||||||
node.type === "extraction" ||
|
|
||||||
node.type === "fileDownload" ||
|
|
||||||
node.type === "action" ||
|
|
||||||
node.type === "http_request" ||
|
|
||||||
node.type === "validation" ||
|
|
||||||
node.type === "codeBlock" ||
|
|
||||||
node.type === "printPage"
|
|
||||||
) {
|
|
||||||
const parameterKeys = node.data
|
|
||||||
.parameterKeys as Array<string> | null;
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
parameterKeys:
|
|
||||||
parameterKeys?.filter(
|
|
||||||
(key) => key !== parameter.key,
|
|
||||||
) ?? null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -235,6 +204,22 @@ function WorkflowParametersPanel({ onMouseDownCapture }: Props) {
|
|||||||
</ScrollAreaViewport>
|
</ScrollAreaViewport>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
<DeleteConfirmationDialog
|
||||||
|
open={deleteDialogState.open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setDeleteDialogState({ open: false, parameterKey: null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Delete Parameter"
|
||||||
|
description={`Are you sure you want to delete "${deleteDialogState.parameterKey}"?`}
|
||||||
|
affectedBlocks={affectedBlocksForDelete}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (deleteDialogState.parameterKey) {
|
||||||
|
handleDeleteParameter(deleteDialogState.parameterKey);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{operationPanelState.active && (
|
{operationPanelState.active && (
|
||||||
<div
|
<div
|
||||||
className="absolute"
|
className="absolute"
|
||||||
|
|||||||
@@ -2720,6 +2720,131 @@ function containsJinjaReference(text: string, key: string): boolean {
|
|||||||
return regex.test(text);
|
return regex.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively checks if any string field in an object contains a jinja reference to a key.
|
||||||
|
* @param obj - The object to check
|
||||||
|
* @param key - The key to look for
|
||||||
|
* @param skipKeys - Set of keys to skip
|
||||||
|
* @param depth - Current recursion depth
|
||||||
|
* @returns True if any string field contains a jinja reference to the key
|
||||||
|
*/
|
||||||
|
function objectContainsJinjaReference(
|
||||||
|
obj: unknown,
|
||||||
|
key: string,
|
||||||
|
skipKeys: Set<string>,
|
||||||
|
depth: number = 0,
|
||||||
|
): boolean {
|
||||||
|
const MAX_DEPTH = 50;
|
||||||
|
if (depth > MAX_DEPTH || obj === null || obj === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === "string") {
|
||||||
|
return containsJinjaReference(obj, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.some((item) =>
|
||||||
|
objectContainsJinjaReference(item, key, skipKeys, depth + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === "object") {
|
||||||
|
for (const [objKey, value] of Object.entries(obj)) {
|
||||||
|
if (skipKeys.has(objKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (objectContainsJinjaReference(value, key, skipKeys, depth + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys to skip when checking for jinja references (same as transform)
|
||||||
|
const SKIP_KEYS_FOR_JINJA_CHECK = new Set([
|
||||||
|
"label",
|
||||||
|
"key",
|
||||||
|
"type",
|
||||||
|
"id",
|
||||||
|
"nodeId",
|
||||||
|
"parameterKeys",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a block that references a parameter or block output.
|
||||||
|
*/
|
||||||
|
type AffectedBlock = {
|
||||||
|
nodeId: string;
|
||||||
|
label: string;
|
||||||
|
hasParameterKeyReference: boolean;
|
||||||
|
hasJinjaReference: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all blocks that reference a given key (parameter or block output).
|
||||||
|
* Checks both parameterKeys arrays and jinja references in text fields.
|
||||||
|
* @param nodes - Array of workflow nodes
|
||||||
|
* @param key - The key to search for (e.g., "my_param" or "block_1_output")
|
||||||
|
* @returns Array of affected block information
|
||||||
|
*/
|
||||||
|
function getAffectedBlocks<T extends Node>(
|
||||||
|
nodes: T[],
|
||||||
|
key: string,
|
||||||
|
): AffectedBlock[] {
|
||||||
|
const affectedBlocks: AffectedBlock[] = [];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
// Skip non-block nodes (start, nodeAdder, etc.)
|
||||||
|
if (
|
||||||
|
!node.data ||
|
||||||
|
!("label" in node.data) ||
|
||||||
|
node.type === "start" ||
|
||||||
|
node.type === "nodeAdder"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = node.data.label as string;
|
||||||
|
let hasParameterKeyReference = false;
|
||||||
|
let hasJinjaReference = false;
|
||||||
|
|
||||||
|
// Check parameterKeys array
|
||||||
|
const parameterKeys = node.data.parameterKeys as Array<string> | undefined;
|
||||||
|
if (parameterKeys?.includes(key)) {
|
||||||
|
hasParameterKeyReference = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for loop node's loopVariableReference
|
||||||
|
if (node.type === "loop") {
|
||||||
|
const loopVarRef = node.data.loopVariableReference as string | undefined;
|
||||||
|
if (loopVarRef === key || containsJinjaReference(loopVarRef ?? "", key)) {
|
||||||
|
hasJinjaReference = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check jinja references in text fields
|
||||||
|
if (
|
||||||
|
objectContainsJinjaReference(node.data, key, SKIP_KEYS_FOR_JINJA_CHECK)
|
||||||
|
) {
|
||||||
|
hasJinjaReference = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasParameterKeyReference || hasJinjaReference) {
|
||||||
|
affectedBlocks.push({
|
||||||
|
nodeId: node.id,
|
||||||
|
label,
|
||||||
|
hasParameterKeyReference,
|
||||||
|
hasJinjaReference,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return affectedBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
// Maximum recursion depth to prevent stack overflow from malicious deeply nested objects
|
// Maximum recursion depth to prevent stack overflow from malicious deeply nested objects
|
||||||
const MAX_TRANSFORM_DEPTH = 50;
|
const MAX_TRANSFORM_DEPTH = 50;
|
||||||
|
|
||||||
@@ -2841,6 +2966,74 @@ function removeJinjaReferenceFromNodes<T extends Node>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a key from all nodes' parameterKeys arrays and handles special cases.
|
||||||
|
* Used when deleting a block output or parameter.
|
||||||
|
*
|
||||||
|
* @param nodes - Array of nodes to process
|
||||||
|
* @param keyToRemove - The key to remove from parameterKeys arrays
|
||||||
|
* @param deletedBlockLabel - Optional label of deleted block (for finallyBlockLabel cleanup)
|
||||||
|
* @returns New array of nodes with the key removed
|
||||||
|
*/
|
||||||
|
function removeKeyFromNodesParameterKeys<T extends Node>(
|
||||||
|
nodes: T[],
|
||||||
|
keyToRemove: string,
|
||||||
|
deletedBlockLabel?: string,
|
||||||
|
): T[] {
|
||||||
|
return nodes.map((node) => {
|
||||||
|
if (!node.data) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle start node's finallyBlockLabel
|
||||||
|
if (
|
||||||
|
node.type === "start" &&
|
||||||
|
deletedBlockLabel &&
|
||||||
|
(node.data as Record<string, unknown>).withWorkflowSettings &&
|
||||||
|
(node.data as Record<string, unknown>).finallyBlockLabel ===
|
||||||
|
deletedBlockLabel
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
finallyBlockLabel: null,
|
||||||
|
},
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle loop node's loopVariableReference
|
||||||
|
if (node.type === "loop") {
|
||||||
|
const loopData = node.data as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
loopVariableReference:
|
||||||
|
loopData.loopVariableReference === keyToRemove
|
||||||
|
? ""
|
||||||
|
: loopData.loopVariableReference,
|
||||||
|
},
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle parameterKeys for all other node types
|
||||||
|
const parameterKeys = (node.data as Record<string, unknown>)
|
||||||
|
.parameterKeys as Array<string> | null | undefined;
|
||||||
|
if (parameterKeys !== undefined) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
parameterKeys: parameterKeys?.filter((key) => key !== keyToRemove),
|
||||||
|
},
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getUpdatedNodesAfterLabelUpdateForParameterKeys(
|
function getUpdatedNodesAfterLabelUpdateForParameterKeys(
|
||||||
id: string,
|
id: string,
|
||||||
newLabel: string,
|
newLabel: string,
|
||||||
@@ -3845,6 +4038,7 @@ export {
|
|||||||
createNode,
|
createNode,
|
||||||
generateNodeData,
|
generateNodeData,
|
||||||
generateNodeLabel,
|
generateNodeLabel,
|
||||||
|
getAffectedBlocks,
|
||||||
getNestingLevel,
|
getNestingLevel,
|
||||||
getAdditionalParametersForEmailBlock,
|
getAdditionalParametersForEmailBlock,
|
||||||
getAvailableOutputParameterKeys,
|
getAvailableOutputParameterKeys,
|
||||||
@@ -3866,5 +4060,8 @@ export {
|
|||||||
isOutputParameterKey,
|
isOutputParameterKey,
|
||||||
layout,
|
layout,
|
||||||
removeJinjaReferenceFromNodes,
|
removeJinjaReferenceFromNodes,
|
||||||
|
removeKeyFromNodesParameterKeys,
|
||||||
replaceJinjaReferenceInNodes,
|
replaceJinjaReferenceInNodes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { AffectedBlock };
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { BlockActionContext } from "@/store/BlockActionContext";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
function useDeleteNodeCallback() {
|
|
||||||
const deleteNodeCallback = useContext(BlockActionContext)?.deleteNodeCallback;
|
|
||||||
|
|
||||||
if (!deleteNodeCallback) {
|
|
||||||
throw new Error(
|
|
||||||
"useDeleteNodeCallback must be used within a BlockActionContextProvider",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deleteNodeCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useDeleteNodeCallback };
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { BlockActionContext } from "@/store/BlockActionContext";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
function useRequestDeleteNodeCallback() {
|
||||||
|
const requestDeleteNodeCallback =
|
||||||
|
useContext(BlockActionContext)?.requestDeleteNodeCallback;
|
||||||
|
|
||||||
|
if (!requestDeleteNodeCallback) {
|
||||||
|
throw new Error(
|
||||||
|
"useRequestDeleteNodeCallback must be used within a BlockActionContextProvider",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestDeleteNodeCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useRequestDeleteNodeCallback };
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
type DeleteNodeCallback = (id: string) => void;
|
type RequestDeleteNodeCallback = (id: string, label: string) => void;
|
||||||
type TransmuteNodeCallback = (id: string, nodeName: string) => void;
|
type TransmuteNodeCallback = (id: string, nodeName: string) => void;
|
||||||
type ToggleScriptForNodeCallback = (opts: {
|
type ToggleScriptForNodeCallback = (opts: {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -10,7 +10,7 @@ type ToggleScriptForNodeCallback = (opts: {
|
|||||||
|
|
||||||
const BlockActionContext = createContext<
|
const BlockActionContext = createContext<
|
||||||
| {
|
| {
|
||||||
deleteNodeCallback: DeleteNodeCallback;
|
requestDeleteNodeCallback: RequestDeleteNodeCallback;
|
||||||
transmuteNodeCallback: TransmuteNodeCallback;
|
transmuteNodeCallback: TransmuteNodeCallback;
|
||||||
toggleScriptForNodeCallback?: ToggleScriptForNodeCallback;
|
toggleScriptForNodeCallback?: ToggleScriptForNodeCallback;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user