{
+ 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 && (
,
+ 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(
+ 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 | 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
const MAX_TRANSFORM_DEPTH = 50;
@@ -2841,6 +2966,74 @@ function removeJinjaReferenceFromNodes(
});
}
+/**
+ * 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(
+ 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).withWorkflowSettings &&
+ (node.data as Record).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;
+ 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)
+ .parameterKeys as Array | null | undefined;
+ if (parameterKeys !== undefined) {
+ return {
+ ...node,
+ data: {
+ ...node.data,
+ parameterKeys: parameterKeys?.filter((key) => key !== keyToRemove),
+ },
+ } as T;
+ }
+
+ return node;
+ });
+}
+
function getUpdatedNodesAfterLabelUpdateForParameterKeys(
id: string,
newLabel: string,
@@ -3845,6 +4038,7 @@ export {
createNode,
generateNodeData,
generateNodeLabel,
+ getAffectedBlocks,
getNestingLevel,
getAdditionalParametersForEmailBlock,
getAvailableOutputParameterKeys,
@@ -3866,5 +4060,8 @@ export {
isOutputParameterKey,
layout,
removeJinjaReferenceFromNodes,
+ removeKeyFromNodesParameterKeys,
replaceJinjaReferenceInNodes,
};
+
+export type { AffectedBlock };
diff --git a/skyvern-frontend/src/routes/workflows/hooks/useDeleteNodeCallback.ts b/skyvern-frontend/src/routes/workflows/hooks/useDeleteNodeCallback.ts
deleted file mode 100644
index 6351aa48..00000000
--- a/skyvern-frontend/src/routes/workflows/hooks/useDeleteNodeCallback.ts
+++ /dev/null
@@ -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 };
diff --git a/skyvern-frontend/src/routes/workflows/hooks/useRequestDeleteNodeCallback.ts b/skyvern-frontend/src/routes/workflows/hooks/useRequestDeleteNodeCallback.ts
new file mode 100644
index 00000000..b5fb34b1
--- /dev/null
+++ b/skyvern-frontend/src/routes/workflows/hooks/useRequestDeleteNodeCallback.ts
@@ -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 };
diff --git a/skyvern-frontend/src/store/BlockActionContext.ts b/skyvern-frontend/src/store/BlockActionContext.ts
index f1877466..f5b07733 100644
--- a/skyvern-frontend/src/store/BlockActionContext.ts
+++ b/skyvern-frontend/src/store/BlockActionContext.ts
@@ -1,6 +1,6 @@
import { createContext } from "react";
-type DeleteNodeCallback = (id: string) => void;
+type RequestDeleteNodeCallback = (id: string, label: string) => void;
type TransmuteNodeCallback = (id: string, nodeName: string) => void;
type ToggleScriptForNodeCallback = (opts: {
id?: string;
@@ -10,7 +10,7 @@ type ToggleScriptForNodeCallback = (opts: {
const BlockActionContext = createContext<
| {
- deleteNodeCallback: DeleteNodeCallback;
+ requestDeleteNodeCallback: RequestDeleteNodeCallback;
transmuteNodeCallback: TransmuteNodeCallback;
toggleScriptForNodeCallback?: ToggleScriptForNodeCallback;
}