Jon/sky 5820 make browser task block flippable with code (#3165)
This commit is contained in:
@@ -0,0 +1,100 @@
|
|||||||
|
import { ExitIcon } from "@radix-ui/react-icons";
|
||||||
|
import { Handle } from "@xyflow/react";
|
||||||
|
import { Position } from "@xyflow/react";
|
||||||
|
|
||||||
|
import { WorkflowBlockType } from "@/routes/workflows/types/workflowTypes";
|
||||||
|
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
|
import { CodeEditor } from "./CodeEditor";
|
||||||
|
import { workflowBlockTitle } from "../editor/nodes/types";
|
||||||
|
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
|
||||||
|
|
||||||
|
function BlockCodeEditor({
|
||||||
|
blockLabel,
|
||||||
|
blockType,
|
||||||
|
script,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
blockLabel: string;
|
||||||
|
blockType: WorkflowBlockType;
|
||||||
|
script: string | undefined;
|
||||||
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
|
}) {
|
||||||
|
const blockTitle = workflowBlockTitle[blockType];
|
||||||
|
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="a"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="b"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"transform-origin-center flex h-full w-[30rem] flex-col space-y-4 rounded-lg border border-slate-600 bg-slate-elevation3 px-6 py-4 transition-all",
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
onClick?.(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header className="!mt-0 flex h-[2.75rem] justify-between gap-2">
|
||||||
|
<div className="flex w-full gap-2">
|
||||||
|
<div className="relative flex h-[2.75rem] w-[2.75rem] items-center justify-center overflow-hidden rounded border border-slate-600">
|
||||||
|
<WorkflowBlockIcon
|
||||||
|
workflowBlockType={blockType}
|
||||||
|
className="size-6"
|
||||||
|
/>
|
||||||
|
<div className="absolute -left-3 top-8 flex h-4 w-16 origin-top-left -rotate-45 transform items-center justify-center bg-yellow-400">
|
||||||
|
<span className="text-xs font-bold text-black">code</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{blockLabel}
|
||||||
|
<span className="text-xs text-slate-400">{blockTitle}</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex w-[2.75rem] items-center justify-center rounded hover:bg-slate-800">
|
||||||
|
<ExitIcon
|
||||||
|
onClick={() => {
|
||||||
|
toggleScriptForNodeCallback({
|
||||||
|
label: blockLabel,
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="size-5 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{script ? (
|
||||||
|
<div className="h-full flex-1 overflow-y-hidden">
|
||||||
|
<CodeEditor
|
||||||
|
key="static"
|
||||||
|
className="nopan nowheel h-full overflow-y-scroll"
|
||||||
|
language="python"
|
||||||
|
value={script}
|
||||||
|
lineWrap={false}
|
||||||
|
readOnly
|
||||||
|
fontSize={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center bg-slate-950">
|
||||||
|
No script defined
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BlockCodeEditor };
|
||||||
@@ -20,6 +20,7 @@ type Props = {
|
|||||||
value: string;
|
value: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
language?: "python" | "json" | "html";
|
language?: "python" | "json" | "html";
|
||||||
|
lineWrap?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
minHeight?: string;
|
minHeight?: string;
|
||||||
maxHeight?: string;
|
maxHeight?: string;
|
||||||
@@ -33,13 +34,14 @@ function CodeEditor({
|
|||||||
minHeight,
|
minHeight,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
language,
|
language,
|
||||||
|
lineWrap = true,
|
||||||
className,
|
className,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
fontSize = 12,
|
fontSize = 12,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const extensions = language
|
const extensions = language
|
||||||
? [getLanguageExtension(language), EditorView.lineWrapping]
|
? [getLanguageExtension(language), lineWrap ? EditorView.lineWrapping : []]
|
||||||
: [EditorView.lineWrapping];
|
: [lineWrap ? EditorView.lineWrapping : []];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { useOnChange } from "@/hooks/useOnChange";
|
import { useOnChange } from "@/hooks/useOnChange";
|
||||||
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
||||||
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
import { BlockActionContext } from "@/store/BlockActionContext";
|
||||||
import { useDebugStore } from "@/store/useDebugStore";
|
import { useDebugStore } from "@/store/useDebugStore";
|
||||||
import {
|
import {
|
||||||
useWorkflowHasChangesStore,
|
useWorkflowHasChangesStore,
|
||||||
@@ -543,6 +543,37 @@ function FlowRenderer({
|
|||||||
doLayout(newNodesWithUpdatedParameters, newEdges);
|
doLayout(newNodesWithUpdatedParameters, newEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleScript({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
show,
|
||||||
|
}: {
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
show: boolean;
|
||||||
|
}) {
|
||||||
|
if (id) {
|
||||||
|
const node = nodes.find((node) => node.id === id);
|
||||||
|
if (!node || !isWorkflowBlockNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.data.showCode = show;
|
||||||
|
} else if (label) {
|
||||||
|
const node = nodes.find(
|
||||||
|
(node) => "label" in node.data && node.data.label === label,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!node || !isWorkflowBlockNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.data.showCode = show;
|
||||||
|
}
|
||||||
|
|
||||||
|
doLayout(nodes, edges);
|
||||||
|
}
|
||||||
|
|
||||||
const editorElementRef = useRef<HTMLDivElement>(null);
|
const editorElementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useAutoPan(editorElementRef, nodes);
|
useAutoPan(editorElementRef, nodes);
|
||||||
@@ -642,7 +673,12 @@ function FlowRenderer({
|
|||||||
<WorkflowParametersStateContext.Provider
|
<WorkflowParametersStateContext.Provider
|
||||||
value={[parameters, setParameters]}
|
value={[parameters, setParameters]}
|
||||||
>
|
>
|
||||||
<DeleteNodeCallbackContext.Provider value={deleteNode}>
|
<BlockActionContext.Provider
|
||||||
|
value={{
|
||||||
|
deleteNodeCallback: deleteNode,
|
||||||
|
toggleScriptForNodeCallback: toggleScript,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
ref={editorElementRef}
|
ref={editorElementRef}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -772,7 +808,7 @@ function FlowRenderer({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</DeleteNodeCallbackContext.Provider>
|
</BlockActionContext.Provider>
|
||||||
</WorkflowParametersStateContext.Provider>
|
</WorkflowParametersStateContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { toast } from "@/components/ui/use-toast";
|
|||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||||
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
||||||
|
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
||||||
|
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||||
import { useSidebarStore } from "@/store/SidebarStore";
|
import { useSidebarStore } from "@/store/SidebarStore";
|
||||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||||
@@ -47,12 +49,18 @@ function WorkflowDebugger() {
|
|||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
|
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
|
||||||
|
const blockScriptStore = useBlockScriptStore();
|
||||||
|
|
||||||
const { data: workflowRun } = useWorkflowRunQuery();
|
const { data: workflowRun } = useWorkflowRunQuery();
|
||||||
|
|
||||||
const { data: workflow } = useWorkflowQuery({
|
const { data: workflow } = useWorkflowQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: blockScripts } = useBlockScriptsQuery({
|
||||||
|
workflowPermanentId,
|
||||||
|
});
|
||||||
|
|
||||||
const { data: debugSession } = useDebugSessionQuery({
|
const { data: debugSession } = useDebugSessionQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
enabled: shouldFetchDebugSession && !!workflowPermanentId,
|
enabled: shouldFetchDebugSession && !!workflowPermanentId,
|
||||||
@@ -80,6 +88,11 @@ function WorkflowDebugger() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
blockScriptStore.setScripts(blockScripts ?? {});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [blockScripts]);
|
||||||
|
|
||||||
const afterCycleBrowser = () => {
|
const afterCycleBrowser = () => {
|
||||||
setOpenDialogue(false);
|
setOpenDialogue(false);
|
||||||
setShowPowerButton(false);
|
setShowPowerButton(false);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||||
|
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
||||||
|
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||||
import { useSidebarStore } from "@/store/SidebarStore";
|
import { useSidebarStore } from "@/store/SidebarStore";
|
||||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||||
import { ReactFlowProvider } from "@xyflow/react";
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
@@ -17,6 +20,7 @@ function WorkflowEditor() {
|
|||||||
return state.setCollapsed;
|
return state.setCollapsed;
|
||||||
});
|
});
|
||||||
const workflowChangesStore = useWorkflowHasChangesStore();
|
const workflowChangesStore = useWorkflowHasChangesStore();
|
||||||
|
const blockScriptStore = useBlockScriptStore();
|
||||||
|
|
||||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
const { data: workflow, isLoading } = useWorkflowQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
@@ -25,11 +29,20 @@ function WorkflowEditor() {
|
|||||||
const { data: globalWorkflows, isLoading: isGlobalWorkflowsLoading } =
|
const { data: globalWorkflows, isLoading: isGlobalWorkflowsLoading } =
|
||||||
useGlobalWorkflowsQuery();
|
useGlobalWorkflowsQuery();
|
||||||
|
|
||||||
|
const { data: blockScripts } = useBlockScriptsQuery({
|
||||||
|
workflowPermanentId,
|
||||||
|
});
|
||||||
|
|
||||||
useMountEffect(() => {
|
useMountEffect(() => {
|
||||||
setCollapsed(true);
|
setCollapsed(true);
|
||||||
workflowChangesStore.setHasChanges(false);
|
workflowChangesStore.setHasChanges(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
blockScriptStore.setScripts(blockScripts ?? {});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [blockScripts]);
|
||||||
|
|
||||||
if (isLoading || isGlobalWorkflowsLoading) {
|
if (isLoading || isGlobalWorkflowsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full items-center justify-center">
|
<div className="flex h-screen w-full items-center justify-center">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Flippable } from "@/components/Flippable";
|
||||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -12,7 +14,9 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||||
|
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||||
import {
|
import {
|
||||||
Handle,
|
Handle,
|
||||||
NodeProps,
|
NodeProps,
|
||||||
@@ -40,7 +44,10 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
const { blockLabel: urlBlockLabel } = useParams();
|
const { blockLabel: urlBlockLabel } = useParams();
|
||||||
const debugStore = useDebugStore();
|
const debugStore = useDebugStore();
|
||||||
const { updateNodeData } = useReactFlow();
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||||
|
const blockScriptStore = useBlockScriptStore();
|
||||||
const { editable, debuggable, label } = data;
|
const { editable, debuggable, label } = data;
|
||||||
|
const script = blockScriptStore.scripts[label];
|
||||||
const thisBlockIsPlaying =
|
const thisBlockIsPlaying =
|
||||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||||
@@ -76,359 +83,382 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
updateNodeData(id, { [key]: value });
|
updateNodeData(id, { [key]: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div>
|
setFacing(data.showCode ? "back" : "front");
|
||||||
<Handle
|
}, [data.showCode]);
|
||||||
type="source"
|
|
||||||
position={Position.Bottom}
|
|
||||||
id="a"
|
|
||||||
className="opacity-0"
|
|
||||||
/>
|
|
||||||
<Handle
|
|
||||||
type="target"
|
|
||||||
position={Position.Top}
|
|
||||||
id="b"
|
|
||||||
className="opacity-0"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
|
||||||
{
|
|
||||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
|
||||||
thisBlockIsPlaying,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<NodeHeader
|
|
||||||
blockLabel={label}
|
|
||||||
editable={editable}
|
|
||||||
disabled={elideFromDebugging}
|
|
||||||
nodeId={id}
|
|
||||||
totpIdentifier={inputs.totpIdentifier}
|
|
||||||
totpUrl={inputs.totpVerificationUrl}
|
|
||||||
type={type}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={cn("space-y-4", {
|
|
||||||
"opacity-50": thisBlockIsPlaying,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Label className="text-xs text-slate-300">URL</Label>
|
|
||||||
<HelpTooltip content={helpTooltips["navigation"]["url"]} />
|
|
||||||
</div>
|
|
||||||
{isFirstWorkflowBlock ? (
|
|
||||||
<div className="flex justify-end text-xs text-slate-400">
|
|
||||||
Tip: Use the {"+"} button to add parameters!
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<WorkflowBlockInputTextarea
|
return (
|
||||||
canWriteTitle={true}
|
<Flippable facing={facing} preserveFrontsideHeight={true}>
|
||||||
nodeId={id}
|
<div>
|
||||||
onChange={(value) => {
|
<Handle
|
||||||
handleChange("url", value);
|
type="source"
|
||||||
}}
|
position={Position.Bottom}
|
||||||
value={inputs.url}
|
id="a"
|
||||||
placeholder={placeholders["navigation"]["url"]}
|
className="opacity-0"
|
||||||
className="nopan text-xs"
|
/>
|
||||||
/>
|
<Handle
|
||||||
</div>
|
type="target"
|
||||||
<div className="space-y-2">
|
position={Position.Top}
|
||||||
<div className="flex gap-2">
|
id="b"
|
||||||
<Label className="text-xs text-slate-300">Prompt</Label>
|
className="opacity-0"
|
||||||
<HelpTooltip
|
/>
|
||||||
content={helpTooltips["navigation"]["navigationGoal"]}
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||||
|
{
|
||||||
|
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||||
|
thisBlockIsPlaying,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NodeHeader
|
||||||
|
blockLabel={label}
|
||||||
|
editable={editable}
|
||||||
|
disabled={elideFromDebugging}
|
||||||
|
nodeId={id}
|
||||||
|
totpIdentifier={inputs.totpIdentifier}
|
||||||
|
totpUrl={inputs.totpVerificationUrl}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn("space-y-4", {
|
||||||
|
"opacity-50": thisBlockIsPlaying,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">URL</Label>
|
||||||
|
<HelpTooltip content={helpTooltips["navigation"]["url"]} />
|
||||||
|
</div>
|
||||||
|
{isFirstWorkflowBlock ? (
|
||||||
|
<div className="flex justify-end text-xs text-slate-400">
|
||||||
|
Tip: Use the {"+"} button to add parameters!
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WorkflowBlockInputTextarea
|
||||||
|
canWriteTitle={true}
|
||||||
|
nodeId={id}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleChange("url", value);
|
||||||
|
}}
|
||||||
|
value={inputs.url}
|
||||||
|
placeholder={placeholders["navigation"]["url"]}
|
||||||
|
className="nopan text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<WorkflowBlockInputTextarea
|
<div className="space-y-2">
|
||||||
nodeId={id}
|
<div className="flex gap-2">
|
||||||
onChange={(value) => {
|
<Label className="text-xs text-slate-300">
|
||||||
handleChange("navigationGoal", value);
|
Navigation Goal
|
||||||
}}
|
</Label>
|
||||||
value={inputs.navigationGoal}
|
<HelpTooltip
|
||||||
placeholder={placeholders["navigation"]["navigationGoal"]}
|
content={helpTooltips["navigation"]["navigationGoal"]}
|
||||||
className="nopan text-xs"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<WorkflowBlockInputTextarea
|
||||||
<div className="rounded-md bg-slate-800 p-2">
|
nodeId={id}
|
||||||
<div className="space-y-1 text-xs text-slate-400">
|
onChange={(value) => {
|
||||||
Tip: Try to phrase your prompt as a goal with an explicit
|
handleChange("navigationGoal", value);
|
||||||
completion criteria. While executing, Skyvern will take as many
|
}}
|
||||||
actions as necessary to accomplish the goal. Use words like
|
value={inputs.navigationGoal}
|
||||||
"Complete" or "Terminate" to help Skyvern identify when it's
|
placeholder={placeholders["navigation"]["navigationGoal"]}
|
||||||
finished or when it should give up.
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md bg-slate-800 p-2">
|
||||||
|
<div className="space-y-1 text-xs text-slate-400">
|
||||||
|
Tip: Try to phrase your prompt as a goal with an explicit
|
||||||
|
completion criteria. While executing, Skyvern will take as many
|
||||||
|
actions as necessary to accomplish the goal. Use words like
|
||||||
|
"Complete" or "Terminate" to help Skyvern identify when it's
|
||||||
|
finished or when it should give up.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Separator />
|
||||||
<Separator />
|
<Accordion
|
||||||
<Accordion
|
className={cn({
|
||||||
className={cn({
|
"pointer-events-none opacity-50": thisBlockIsPlaying,
|
||||||
"pointer-events-none opacity-50": thisBlockIsPlaying,
|
})}
|
||||||
})}
|
type="single"
|
||||||
type="single"
|
collapsible
|
||||||
collapsible
|
>
|
||||||
>
|
<AccordionItem value="advanced" className="border-b-0">
|
||||||
<AccordionItem value="advanced" className="border-b-0">
|
<AccordionTrigger className="py-0">
|
||||||
<AccordionTrigger className="py-0">
|
Advanced Settings
|
||||||
Advanced Settings
|
</AccordionTrigger>
|
||||||
</AccordionTrigger>
|
<AccordionContent className="pl-6 pr-1 pt-1">
|
||||||
<AccordionContent className="pl-6 pr-1 pt-1">
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<ParametersMultiSelect
|
||||||
<ParametersMultiSelect
|
availableOutputParameters={outputParameterKeys}
|
||||||
availableOutputParameters={outputParameterKeys}
|
parameters={data.parameterKeys}
|
||||||
parameters={data.parameterKeys}
|
onParametersChange={(parameterKeys) => {
|
||||||
onParametersChange={(parameterKeys) => {
|
updateNodeData(id, { parameterKeys });
|
||||||
updateNodeData(id, { parameterKeys });
|
}}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs text-slate-300">
|
|
||||||
Complete if...
|
|
||||||
</Label>
|
|
||||||
<WorkflowBlockInputTextarea
|
|
||||||
nodeId={id}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleChange("completeCriterion", value);
|
|
||||||
}}
|
|
||||||
value={inputs.completeCriterion}
|
|
||||||
className="nopan text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<ModelSelector
|
|
||||||
className="nopan w-52 text-xs"
|
|
||||||
value={inputs.model}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleChange("model", value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
|
||||||
Engine
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<RunEngineSelector
|
|
||||||
value={inputs.engine}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleChange("engine", value);
|
|
||||||
}}
|
|
||||||
className="nopan w-52 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
|
||||||
Max Steps Override
|
|
||||||
</Label>
|
|
||||||
<HelpTooltip
|
|
||||||
content={helpTooltips["navigation"]["maxStepsOverride"]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<div className="space-y-2">
|
||||||
type="number"
|
<Label className="text-xs text-slate-300">
|
||||||
placeholder={placeholders["navigation"]["maxStepsOverride"]}
|
Complete if...
|
||||||
|
</Label>
|
||||||
|
<WorkflowBlockInputTextarea
|
||||||
|
nodeId={id}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleChange("completeCriterion", value);
|
||||||
|
}}
|
||||||
|
value={inputs.completeCriterion}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<ModelSelector
|
||||||
className="nopan w-52 text-xs"
|
className="nopan w-52 text-xs"
|
||||||
min="0"
|
value={inputs.model}
|
||||||
value={inputs.maxStepsOverride ?? ""}
|
onChange={(value) => {
|
||||||
onChange={(event) => {
|
handleChange("model", value);
|
||||||
const value =
|
|
||||||
event.target.value === ""
|
|
||||||
? null
|
|
||||||
: Number(event.target.value);
|
|
||||||
handleChange("maxStepsOverride", value);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
Error Messages
|
Engine
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<RunEngineSelector
|
||||||
|
value={inputs.engine}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleChange("engine", value);
|
||||||
|
}}
|
||||||
|
className="nopan w-52 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Max Steps Override
|
||||||
</Label>
|
</Label>
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
content={helpTooltips["navigation"]["errorCodeMapping"]}
|
content={helpTooltips["navigation"]["maxStepsOverride"]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Input
|
||||||
checked={inputs.errorCodeMapping !== "null"}
|
type="number"
|
||||||
disabled={!editable}
|
placeholder={
|
||||||
onCheckedChange={(checked) => {
|
placeholders["navigation"]["maxStepsOverride"]
|
||||||
handleChange(
|
|
||||||
"errorCodeMapping",
|
|
||||||
checked
|
|
||||||
? JSON.stringify(errorMappingExampleValue, null, 2)
|
|
||||||
: "null",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{inputs.errorCodeMapping !== "null" && (
|
|
||||||
<div>
|
|
||||||
<CodeEditor
|
|
||||||
language="json"
|
|
||||||
value={inputs.errorCodeMapping}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleChange("errorCodeMapping", value);
|
|
||||||
}}
|
|
||||||
className="nowheel nopan"
|
|
||||||
fontSize={8}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
|
||||||
Include Action History
|
|
||||||
</Label>
|
|
||||||
<HelpTooltip
|
|
||||||
content={
|
|
||||||
helpTooltips["navigation"][
|
|
||||||
"includeActionHistoryInVerification"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
/>
|
className="nopan w-52 text-xs"
|
||||||
</div>
|
min="0"
|
||||||
<div className="w-52">
|
value={inputs.maxStepsOverride ?? ""}
|
||||||
<Switch
|
onChange={(event) => {
|
||||||
checked={inputs.includeActionHistoryInVerification}
|
const value =
|
||||||
onCheckedChange={(checked) => {
|
event.target.value === ""
|
||||||
handleChange(
|
? null
|
||||||
"includeActionHistoryInVerification",
|
: Number(event.target.value);
|
||||||
checked,
|
handleChange("maxStepsOverride", value);
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex gap-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
Continue on Failure
|
Error Messages
|
||||||
</Label>
|
</Label>
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
content={helpTooltips["navigation"]["continueOnFailure"]}
|
content={
|
||||||
/>
|
helpTooltips["navigation"]["errorCodeMapping"]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={inputs.errorCodeMapping !== "null"}
|
||||||
|
disabled={!editable}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleChange(
|
||||||
|
"errorCodeMapping",
|
||||||
|
checked
|
||||||
|
? JSON.stringify(
|
||||||
|
errorMappingExampleValue,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
: "null",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{inputs.errorCodeMapping !== "null" && (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={inputs.errorCodeMapping}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleChange("errorCodeMapping", value);
|
||||||
|
}}
|
||||||
|
className="nowheel nopan"
|
||||||
|
fontSize={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-52">
|
<Separator />
|
||||||
<Switch
|
<div className="flex items-center justify-between">
|
||||||
checked={inputs.continueOnFailure}
|
<div className="flex gap-2">
|
||||||
onCheckedChange={(checked) => {
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
handleChange("continueOnFailure", checked);
|
Include Action History
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={
|
||||||
|
helpTooltips["navigation"][
|
||||||
|
"includeActionHistoryInVerification"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-52">
|
||||||
|
<Switch
|
||||||
|
checked={inputs.includeActionHistoryInVerification}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleChange(
|
||||||
|
"includeActionHistoryInVerification",
|
||||||
|
checked,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Continue on Failure
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={
|
||||||
|
helpTooltips["navigation"]["continueOnFailure"]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-52">
|
||||||
|
<Switch
|
||||||
|
checked={inputs.continueOnFailure}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleChange("continueOnFailure", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Cache Actions
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={helpTooltips["navigation"]["cacheActions"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-52">
|
||||||
|
<Switch
|
||||||
|
checked={inputs.cacheActions}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleChange("cacheActions", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
Complete on Download
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={
|
||||||
|
helpTooltips["navigation"]["completeOnDownload"]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-52">
|
||||||
|
<Switch
|
||||||
|
checked={inputs.allowDownloads}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleChange("allowDownloads", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
|
File Suffix
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={helpTooltips["navigation"]["fileSuffix"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<WorkflowBlockInput
|
||||||
|
nodeId={id}
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholders["navigation"]["downloadSuffix"]}
|
||||||
|
className="nopan w-52 text-xs"
|
||||||
|
value={inputs.downloadSuffix ?? ""}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleChange("downloadSuffix", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Separator />
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
<Label className="text-xs text-slate-300">
|
||||||
Cache Actions
|
2FA Identifier
|
||||||
</Label>
|
</Label>
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
content={helpTooltips["navigation"]["cacheActions"]}
|
content={helpTooltips["navigation"]["totpIdentifier"]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-52">
|
<WorkflowBlockInputTextarea
|
||||||
<Switch
|
nodeId={id}
|
||||||
checked={inputs.cacheActions}
|
onChange={(value) => {
|
||||||
onCheckedChange={(checked) => {
|
handleChange("totpIdentifier", value);
|
||||||
handleChange("cacheActions", checked);
|
|
||||||
}}
|
}}
|
||||||
|
value={inputs.totpIdentifier ?? ""}
|
||||||
|
placeholder={placeholders["navigation"]["totpIdentifier"]}
|
||||||
|
className="nopan text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
<Separator />
|
<div className="flex gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<Label className="text-xs text-slate-300">
|
||||||
<div className="flex gap-2">
|
2FA Verification URL
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
</Label>
|
||||||
Complete on Download
|
<HelpTooltip
|
||||||
</Label>
|
content={helpTooltips["task"]["totpVerificationUrl"]}
|
||||||
<HelpTooltip
|
/>
|
||||||
content={helpTooltips["navigation"]["completeOnDownload"]}
|
</div>
|
||||||
/>
|
<WorkflowBlockInputTextarea
|
||||||
</div>
|
nodeId={id}
|
||||||
<div className="w-52">
|
onChange={(value) => {
|
||||||
<Switch
|
handleChange("totpVerificationUrl", value);
|
||||||
checked={inputs.allowDownloads}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
handleChange("allowDownloads", checked);
|
|
||||||
}}
|
}}
|
||||||
|
value={inputs.totpVerificationUrl ?? ""}
|
||||||
|
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||||
|
className="nopan text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
</AccordionContent>
|
||||||
<div className="flex gap-2">
|
</AccordionItem>
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
</Accordion>
|
||||||
File Suffix
|
</div>
|
||||||
</Label>
|
|
||||||
<HelpTooltip
|
|
||||||
content={helpTooltips["navigation"]["fileSuffix"]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<WorkflowBlockInput
|
|
||||||
nodeId={id}
|
|
||||||
type="text"
|
|
||||||
placeholder={placeholders["navigation"]["downloadSuffix"]}
|
|
||||||
className="nopan w-52 text-xs"
|
|
||||||
value={inputs.downloadSuffix ?? ""}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleChange("downloadSuffix", value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Label className="text-xs text-slate-300">
|
|
||||||
2FA Identifier
|
|
||||||
</Label>
|
|
||||||
<HelpTooltip
|
|
||||||
content={helpTooltips["navigation"]["totpIdentifier"]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<WorkflowBlockInputTextarea
|
|
||||||
nodeId={id}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleChange("totpIdentifier", value);
|
|
||||||
}}
|
|
||||||
value={inputs.totpIdentifier ?? ""}
|
|
||||||
placeholder={placeholders["navigation"]["totpIdentifier"]}
|
|
||||||
className="nopan text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Label className="text-xs text-slate-300">
|
|
||||||
2FA Verification URL
|
|
||||||
</Label>
|
|
||||||
<HelpTooltip
|
|
||||||
content={helpTooltips["task"]["totpVerificationUrl"]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<WorkflowBlockInputTextarea
|
|
||||||
nodeId={id}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleChange("totpVerificationUrl", value);
|
|
||||||
}}
|
|
||||||
value={inputs.totpVerificationUrl ?? ""}
|
|
||||||
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
|
||||||
className="nopan text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<BlockCodeEditor blockLabel={label} blockType={type} script={script} />
|
||||||
|
</Flippable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
|
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
|
||||||
|
import { OrgWalled } from "@/components/Orgwalled";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
onShowScript?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function NodeActionMenu({ onDelete }: Props) {
|
function NodeActionMenu({ onDelete, onShowScript }: Props) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -28,6 +30,17 @@ function NodeActionMenu({ onDelete }: Props) {
|
|||||||
>
|
>
|
||||||
Delete Block
|
Delete Block
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<OrgWalled className="p-0">
|
||||||
|
{onShowScript && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
onShowScript();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show Script
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</OrgWalled>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|||||||
|
|
||||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||||
|
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
|
||||||
import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQuery";
|
import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQuery";
|
||||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||||
import {
|
import {
|
||||||
@@ -145,6 +146,7 @@ function NodeHeader({
|
|||||||
});
|
});
|
||||||
const blockTitle = workflowBlockTitle[type];
|
const blockTitle = workflowBlockTitle[type];
|
||||||
const deleteNodeCallback = useDeleteNodeCallback();
|
const deleteNodeCallback = useDeleteNodeCallback();
|
||||||
|
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -411,6 +413,9 @@ function NodeHeader({
|
|||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
deleteNodeCallback(nodeId);
|
deleteNodeCallback(nodeId);
|
||||||
}}
|
}}
|
||||||
|
onShowScript={() =>
|
||||||
|
toggleScriptForNodeCallback({ id: nodeId, show: true })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type NodeBaseData = {
|
|||||||
continueOnFailure: boolean;
|
continueOnFailure: boolean;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
model: WorkflowModel | null;
|
model: WorkflowModel | null;
|
||||||
|
showCode?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const errorMappingExampleValue = {
|
export const errorMappingExampleValue = {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { getClient } from "@/api/AxiosClient";
|
||||||
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { ScriptBlocksResponse } from "../types/scriptTypes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
cacheKey?: string;
|
||||||
|
cacheKeyValue?: string;
|
||||||
|
workflowPermanentId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useBlockScriptsQuery({
|
||||||
|
cacheKey,
|
||||||
|
cacheKeyValue,
|
||||||
|
workflowPermanentId,
|
||||||
|
}: Props) {
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
|
|
||||||
|
return useQuery<{ [blockName: string]: string }>({
|
||||||
|
queryKey: ["block-scripts", workflowPermanentId, cacheKey, cacheKeyValue],
|
||||||
|
queryFn: async () => {
|
||||||
|
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||||
|
|
||||||
|
const result = await client
|
||||||
|
.post<ScriptBlocksResponse>(`/scripts/${workflowPermanentId}/blocks`, {
|
||||||
|
cache_key: cacheKey ?? "",
|
||||||
|
cache_key_value: cacheKeyValue ?? "",
|
||||||
|
})
|
||||||
|
.then((response) => response.data);
|
||||||
|
|
||||||
|
return result.blocks;
|
||||||
|
},
|
||||||
|
enabled: !!workflowPermanentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useBlockScriptsQuery };
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
import { BlockActionContext } from "@/store/BlockActionContext";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
function useDeleteNodeCallback() {
|
function useDeleteNodeCallback() {
|
||||||
const deleteNodeCallback = useContext(DeleteNodeCallbackContext);
|
const deleteNodeCallback = useContext(BlockActionContext)?.deleteNodeCallback;
|
||||||
|
|
||||||
if (!deleteNodeCallback) {
|
if (!deleteNodeCallback) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"useDeleteNodeCallback must be used within a DeleteNodeCallbackProvider",
|
"useDeleteNodeCallback must be used within a BlockActionContextProvider",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { BlockActionContext } from "@/store/BlockActionContext";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
function useToggleScriptForNodeCallback() {
|
||||||
|
const toggleScriptForNodeCallback =
|
||||||
|
useContext(BlockActionContext)?.toggleScriptForNodeCallback;
|
||||||
|
|
||||||
|
if (!toggleScriptForNodeCallback) {
|
||||||
|
throw new Error(
|
||||||
|
"useToggleScriptForNodeCallback must be used within a BlockActionContextProvider",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toggleScriptForNodeCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToggleScriptForNodeCallback };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type ScriptBlocksResponse = {
|
||||||
|
blocks: { [blockName: string]: string };
|
||||||
|
};
|
||||||
18
skyvern-frontend/src/store/BlockActionContext.ts
Normal file
18
skyvern-frontend/src/store/BlockActionContext.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
type DeleteNodeCallback = (id: string) => void;
|
||||||
|
type ToggleScriptForNodeCallback = (opts: {
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
show: boolean;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
const BlockActionContext = createContext<
|
||||||
|
| {
|
||||||
|
deleteNodeCallback: DeleteNodeCallback;
|
||||||
|
toggleScriptForNodeCallback?: ToggleScriptForNodeCallback;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export { BlockActionContext };
|
||||||
45
skyvern-frontend/src/store/BlockScriptStore.ts
Normal file
45
skyvern-frontend/src/store/BlockScriptStore.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* A store to hold the scripts for individual blocks in a workflow. As each
|
||||||
|
* workflow has uniquely (and differently) labelled blocks, and those labels
|
||||||
|
* are block identity, we'll eschew strong typing for this, and use a loose
|
||||||
|
* object literal instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface BlockScriptStore {
|
||||||
|
scriptId?: string;
|
||||||
|
scripts: { [k: string]: string };
|
||||||
|
// --
|
||||||
|
setScript: (blockId: string, script: string) => void;
|
||||||
|
setScripts: (scripts: { [k: string]: string }) => void;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBlockScriptStore = create<BlockScriptStore>((set) => {
|
||||||
|
return {
|
||||||
|
scriptId: undefined,
|
||||||
|
scripts: {},
|
||||||
|
// --
|
||||||
|
setScript: (blockId: string, script: string) => {
|
||||||
|
set((state) => ({
|
||||||
|
scripts: {
|
||||||
|
...state.scripts,
|
||||||
|
[blockId]: script,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
setScripts: (scripts: { [k: string]: string }) => {
|
||||||
|
set(() => ({
|
||||||
|
scripts,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
reset: () => {
|
||||||
|
set({
|
||||||
|
scripts: {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export { useBlockScriptStore };
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
type DeleteNodeCallback = (id: string) => void;
|
|
||||||
|
|
||||||
const DeleteNodeCallbackContext = createContext<DeleteNodeCallback | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
export { DeleteNodeCallbackContext };
|
|
||||||
Reference in New Issue
Block a user