show all of the scripts via a button (#3294)

This commit is contained in:
Jonathan Dobson
2025-08-25 09:32:54 -04:00
committed by GitHub
parent 27b637cbd3
commit cb340893f4
7 changed files with 292 additions and 161 deletions

View File

@@ -16,15 +16,22 @@ function BlockCodeEditor({
blockLabel, blockLabel,
blockType, blockType,
script, script,
title,
onClick, onClick,
onExit,
}: { }: {
blockLabel: string; blockLabel: string;
blockType: WorkflowBlockType; blockType?: WorkflowBlockType;
script: string | undefined; script: string | undefined;
title?: string;
onClick?: (e: React.MouseEvent) => void; onClick?: (e: React.MouseEvent) => void;
/**
* Return `false` to cancel the exit.
*/
onExit?: () => boolean;
}) { }) {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const blockTitle = workflowBlockTitle[blockType]; const blockTitle = blockType ? workflowBlockTitle[blockType] : title;
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback(); const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
const cacheKeyValue = searchParams.get("cache-key-value"); const cacheKeyValue = searchParams.get("cache-key-value");
@@ -53,38 +60,51 @@ function BlockCodeEditor({
}} }}
> >
<header className="relative !mt-0 flex h-[2.75rem] justify-between gap-2"> <header className="relative !mt-0 flex h-[2.75rem] justify-between gap-2">
<div className="flex w-full gap-2"> {blockType ? (
<div className="relative flex h-[2.75rem] w-[2.75rem] items-center justify-center overflow-hidden rounded border border-slate-600"> <div className="flex w-full gap-2">
<WorkflowBlockIcon <div className="relative flex h-[2.75rem] w-[2.75rem] items-center justify-center overflow-hidden rounded border border-slate-600">
workflowBlockType={blockType} <WorkflowBlockIcon
className="size-6" 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 className="absolute -left-3 top-8 flex h-4 w-16 origin-top-left -rotate-45 transform items-center justify-center bg-yellow-400">
</div> <span className="text-xs font-bold text-black">code</span>
</div> </div>
<div className="flex w-full flex-col gap-1"> </div>
{blockLabel}
<div className="flex w-full items-center justify-center gap-1"> <div className="flex w-full flex-col gap-1">
<span className="text-xs text-slate-400">{blockTitle}</span> {blockLabel}
<div className="ml-auto scale-[60%] opacity-50"> <div className="flex w-full items-center justify-center gap-1">
<KeyIcon /> <span className="text-xs text-slate-400">{blockTitle}</span>
<div className="ml-auto scale-[60%] opacity-50">
<KeyIcon />
</div>
<span className="text-xs text-slate-400">
{cacheKeyValue === "" || !cacheKeyValue
? "(none)"
: cacheKeyValue}
</span>
</div> </div>
<span className="text-xs text-slate-400">
{cacheKeyValue === "" || !cacheKeyValue
? "(none)"
: cacheKeyValue}
</span>
</div> </div>
</div> </div>
</div> ) : (
<header className="mt-0 flex h-[2.75rem] w-full items-center justify-center">
{title ?? blockLabel}
</header>
)}
<div className="absolute right-[-0.5rem] top-0 flex h-[2rem] w-[2rem] items-center justify-center rounded hover:bg-slate-800"> <div className="absolute right-[-0.5rem] top-0 flex h-[2rem] w-[2rem] items-center justify-center rounded hover:bg-slate-800">
<ExitIcon <ExitIcon
onClick={() => { onClick={() => {
toggleScriptForNodeCallback({ if (onExit) {
label: blockLabel, const result = onExit();
show: false,
}); if (result !== false) {
toggleScriptForNodeCallback({
label: blockLabel,
show: false,
});
}
}
}} }}
className="size-5 cursor-pointer" className="size-5 cursor-pointer"
/> />

View File

@@ -459,7 +459,7 @@ function FlowRenderer({
}) { }) {
if (id) { if (id) {
const node = nodes.find((node) => node.id === id); const node = nodes.find((node) => node.id === id);
if (!node || !isWorkflowBlockNode(node)) { if (!node) {
return; return;
} }
@@ -469,7 +469,7 @@ function FlowRenderer({
(node) => "label" in node.data && node.data.label === label, (node) => "label" in node.data && node.data.label === label,
); );
if (!node || !isWorkflowBlockNode(node)) { if (!node) {
return; return;
} }

View File

@@ -417,6 +417,8 @@ function Workspace({
{ {
withWorkflowSettings: false, withWorkflowSettings: false,
editable: true, editable: true,
label: "__start_block__",
showCode: false,
}, },
id, id,
), ),

View File

@@ -10,16 +10,24 @@ import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { OrgWalled } from "@/components/Orgwalled"; import { OrgWalled } from "@/components/Orgwalled";
type Props = { type Props = {
isDeleteable?: boolean;
isScriptable?: boolean; isScriptable?: boolean;
onDelete: () => void; showScriptText?: string;
onDelete?: () => void;
onShowScript?: () => void; onShowScript?: () => void;
}; };
function NodeActionMenu({ function NodeActionMenu({
isDeleteable = true,
isScriptable = false, isScriptable = false,
showScriptText,
onDelete, onDelete,
onShowScript, onShowScript,
}: Props) { }: Props) {
if (!isDeleteable && !isScriptable) {
return null;
}
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -28,13 +36,15 @@ function NodeActionMenu({
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuLabel>Block Actions</DropdownMenuLabel> <DropdownMenuLabel>Block Actions</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem {isDeleteable && (
onSelect={() => { <DropdownMenuItem
onDelete(); onSelect={() => {
}} onDelete?.();
> }}
Delete Block >
</DropdownMenuItem> Delete Block
</DropdownMenuItem>
)}
{isScriptable && ( {isScriptable && (
<OrgWalled className="p-0"> <OrgWalled className="p-0">
{onShowScript && ( {onShowScript && (
@@ -43,7 +53,7 @@ function NodeActionMenu({
onShowScript(); onShowScript();
}} }}
> >
Show Script {showScriptText ?? "Show Script"}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgWalled> </OrgWalled>

View File

@@ -1,5 +1,5 @@
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import { Handle, Node, NodeProps, Position, useReactFlow } from "@xyflow/react";
import type { StartNode } from "./types"; import type { StartNode } from "./types";
import { import {
Accordion, Accordion,
@@ -25,12 +25,25 @@ import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "../Taskv2Node/types";
import { KeyValueInput } from "@/components/KeyValueInput"; import { KeyValueInput } from "@/components/KeyValueInput";
import { OrgWalled } from "@/components/Orgwalled"; import { OrgWalled } from "@/components/Orgwalled";
import { placeholders } from "@/routes/workflows/editor/helpContent"; import { placeholders } from "@/routes/workflows/editor/helpContent";
import { NodeActionMenu } from "@/routes/workflows/editor/nodes/NodeActionMenu";
import { useWorkflowSettingsStore } from "@/store/WorkflowSettingsStore"; import { useWorkflowSettingsStore } from "@/store/WorkflowSettingsStore";
import {
scriptableWorkflowBlockTypes,
type WorkflowBlockType,
} from "@/routes/workflows/types/workflowTypes";
// import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
import { Flippable } from "@/components/Flippable";
import { useRerender } from "@/hooks/useRerender";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
function StartNode({ id, data }: NodeProps<StartNode>) { function StartNode({ id, data }: NodeProps<StartNode>) {
const workflowSettingsStore = useWorkflowSettingsStore(); const workflowSettingsStore = useWorkflowSettingsStore();
const credentialGetter = useCredentialGetter(); const credentialGetter = useCredentialGetter();
const { updateNodeData } = useReactFlow(); const { updateNodeData } = useReactFlow();
// const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
const reactFlowInstance = useReactFlow();
const { data: availableModels } = useQuery<ModelsResponse>({ const { data: availableModels } = useQuery<ModelsResponse>({
queryKey: ["models"], queryKey: ["models"],
@@ -66,6 +79,15 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
scriptCacheKey: data.withWorkflowSettings ? data.scriptCacheKey : null, scriptCacheKey: data.withWorkflowSettings ? data.scriptCacheKey : null,
}); });
const [facing, setFacing] = useState<"front" | "back">("front");
const blockScriptStore = useBlockScriptStore();
const script = blockScriptStore.scripts.__start_block__;
const rerender = useRerender({ prefix: "accordion" });
useEffect(() => {
setFacing(data.showCode ? "back" : "front");
}, [data.showCode]);
useEffect(() => { useEffect(() => {
workflowSettingsStore.setWorkflowSettings(inputs); workflowSettingsStore.setWorkflowSettings(inputs);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -79,148 +101,217 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
updateNodeData(id, { [key]: value }); updateNodeData(id, { [key]: value });
} }
function nodeIsFlippable(node: Node) {
return (
scriptableWorkflowBlockTypes.has(node.type as WorkflowBlockType) ||
node.type === "start"
);
}
function showAllScripts() {
reactFlowInstance.setNodes((nodes) => {
return nodes.map((node) => {
if (nodeIsFlippable(node)) {
return {
...node,
data: {
...node.data,
showCode: true,
},
};
}
return node;
});
});
}
function hideAllScripts() {
reactFlowInstance.setNodes((nodes) => {
return nodes.map((node) => {
if (nodeIsFlippable(node)) {
return {
...node,
data: {
...node.data,
showCode: false,
},
};
}
return node;
});
});
}
if (data.withWorkflowSettings) { if (data.withWorkflowSettings) {
return ( return (
<div> <Flippable facing={facing} preserveFrontsideHeight={true}>
<Handle <div>
type="source" <Handle
position={Position.Bottom} type="source"
id="a" position={Position.Bottom}
className="opacity-0" id="a"
/> className="opacity-0"
<div className="w-[30rem] rounded-lg bg-slate-elevation3 px-6 py-4 text-center"> />
<div className="space-y-4"> <div className="w-[30rem] rounded-lg bg-slate-elevation3 px-6 py-4 text-center">
<header>Start</header> <div className="relative">
<Separator /> <div className="absolute right-0 top-0">
<Accordion type="single" collapsible> <div>
<AccordionItem value="settings" className="border-b-0"> <div className="rounded p-1 hover:bg-muted">
<AccordionTrigger className="py-2"> <NodeActionMenu
Workflow Settings isDeleteable={false}
</AccordionTrigger> isScriptable={true}
<AccordionContent className="pl-6 pr-1 pt-1"> showScriptText="Show All Scripts"
<div className="space-y-4"> onShowScript={showAllScripts}
<div className="space-y-2"> />
<ModelSelector </div>
className="nopan w-52 text-xs" </div>
value={inputs.model} </div>
onChange={(value) => { <header className="mb-4">Start</header>
handleChange("model", value); <Separator />
}} <Accordion
/> type="single"
</div> collapsible
<div className="space-y-2"> onValueChange={() => rerender.bump()}
<div className="flex gap-2"> >
<Label>Webhook Callback URL</Label> <AccordionItem value="settings" className="mt-4 border-b-0">
<HelpTooltip content="The URL of a webhook endpoint to send the workflow results" /> <AccordionTrigger className="py-2">
Workflow Settings
</AccordionTrigger>
<AccordionContent className="pl-6 pr-1 pt-1">
<div key={rerender.key} className="space-y-4">
<div className="space-y-2">
<ModelSelector
className="nopan w-52 text-xs"
value={inputs.model}
onChange={(value) => {
handleChange("model", value);
}}
/>
</div> </div>
<Input <div className="space-y-2">
value={inputs.webhookCallbackUrl} <div className="flex gap-2">
placeholder="https://" <Label>Webhook Callback URL</Label>
onChange={(event) => { <HelpTooltip content="The URL of a webhook endpoint to send the workflow results" />
handleChange( </div>
"webhookCallbackUrl", <Input
event.target.value, value={inputs.webhookCallbackUrl}
); placeholder="https://"
}} onChange={(event) => {
/> handleChange(
</div> "webhookCallbackUrl",
<div className="space-y-2"> event.target.value,
<div className="flex gap-2"> );
<Label>Proxy Location</Label> }}
<HelpTooltip content="Route Skyvern through one of our available proxies." /> />
</div> </div>
<ProxySelector <div className="space-y-2">
value={inputs.proxyLocation} <div className="flex gap-2">
onChange={(value) => { <Label>Proxy Location</Label>
handleChange("proxyLocation", value); <HelpTooltip content="Route Skyvern through one of our available proxies." />
}} </div>
/> <ProxySelector
</div> value={inputs.proxyLocation}
<OrgWalled className="flex flex-col gap-4"> onChange={(value) => {
handleChange("proxyLocation", value);
}}
/>
</div>
<OrgWalled className="flex flex-col gap-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label>Generate Script</Label>
<HelpTooltip content="Generate & use cached scripts for faster execution." />
<Switch
className="ml-auto"
checked={inputs.useScriptCache}
onCheckedChange={(value) => {
handleChange("useScriptCache", value);
}}
/>
</div>
</div>
{inputs.useScriptCache && (
<div className="space-y-2">
<div className="flex gap-2">
<Label>Script Key (optional)</Label>
</div>
<WorkflowBlockInputTextarea
nodeId={id}
onChange={(value) => {
const v = value.length ? value : null;
handleChange("scriptCacheKey", v);
}}
value={inputs.scriptCacheKey ?? ""}
placeholder={placeholders["scripts"]["scriptKey"]}
className="nopan text-xs"
/>
</div>
)}
</OrgWalled>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label>Generate Script</Label> <Label>Save &amp; Reuse Session</Label>
<HelpTooltip content="Generate & use cached scripts for faster execution." /> <HelpTooltip content="Persist session information across workflow runs" />
<Switch <Switch
className="ml-auto" className="ml-auto"
checked={inputs.useScriptCache} checked={inputs.persistBrowserSession}
onCheckedChange={(value) => { onCheckedChange={(value) => {
handleChange("useScriptCache", value); handleChange("persistBrowserSession", value);
}} }}
/> />
</div> </div>
</div> </div>
{inputs.useScriptCache && ( <div className="space-y-2">
<div className="space-y-2"> <div className="flex items-center gap-2">
<div className="flex gap-2"> <Label>Extra HTTP Headers</Label>
<Label>Script Key (optional)</Label> <HelpTooltip content="Specify some self-defined HTTP requests headers" />
</div> </div>
<WorkflowBlockInputTextarea <KeyValueInput
nodeId={id} value={inputs.extraHttpHeaders ?? null}
onChange={(value) => { onChange={(val) =>
const v = value.length ? value : null; handleChange("extraHttpHeaders", val)
handleChange("scriptCacheKey", v); }
}} addButtonText="Add Header"
value={inputs.scriptCacheKey ?? ""} />
placeholder={placeholders["scripts"]["scriptKey"]} </div>
className="nopan text-xs" <div className="space-y-2">
<div className="flex items-center gap-2">
<Label>Max Screenshot Scrolls</Label>
<HelpTooltip
content={`The maximum number of scrolls for the post action screenshot. Default is ${MAX_SCREENSHOT_SCROLLS_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`}
/> />
</div> </div>
)} <Input
</OrgWalled> value={inputs.maxScreenshotScrolls ?? ""}
<div className="space-y-2"> placeholder={`Default: ${MAX_SCREENSHOT_SCROLLS_DEFAULT}`}
<div className="flex items-center gap-2"> onChange={(event) => {
<Label>Save &amp; Reuse Session</Label> const value =
<HelpTooltip content="Persist session information across workflow runs" /> event.target.value === ""
<Switch ? null
className="ml-auto" : Number(event.target.value);
checked={inputs.persistBrowserSession}
onCheckedChange={(value) => { handleChange("maxScreenshotScrolls", value);
handleChange("persistBrowserSession", value);
}} }}
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> </AccordionContent>
<div className="flex items-center gap-2"> </AccordionItem>
<Label>Extra HTTP Headers</Label> </Accordion>
<HelpTooltip content="Specify some self-defined HTTP requests headers" /> </div>
</div>
<KeyValueInput
value={inputs.extraHttpHeaders ?? null}
onChange={(val) =>
handleChange("extraHttpHeaders", val)
}
addButtonText="Add Header"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label>Max Screenshot Scrolls</Label>
<HelpTooltip
content={`The maximum number of scrolls for the post action screenshot. Default is ${MAX_SCREENSHOT_SCROLLS_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`}
/>
</div>
<Input
value={inputs.maxScreenshotScrolls ?? ""}
placeholder={`Default: ${MAX_SCREENSHOT_SCROLLS_DEFAULT}`}
onChange={(event) => {
const value =
event.target.value === ""
? null
: Number(event.target.value);
handleChange("maxScreenshotScrolls", value);
}}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div> </div>
</div> </div>
</div>
<BlockCodeEditor
blockLabel="__start_block__"
title="Start"
script={script}
onExit={() => {
hideAllScripts();
return false;
}}
/>
</Flippable>
); );
} }

View File

@@ -14,11 +14,15 @@ export type WorkflowStartNodeData = {
editable: boolean; editable: boolean;
useScriptCache: boolean; useScriptCache: boolean;
scriptCacheKey: string | null; scriptCacheKey: string | null;
label: "__start_block__";
showCode: boolean;
}; };
export type OtherStartNodeData = { export type OtherStartNodeData = {
withWorkflowSettings: false; withWorkflowSettings: false;
editable: boolean; editable: boolean;
label: "__start_block__";
showCode: boolean;
}; };
export type StartNodeData = WorkflowStartNodeData | OtherStartNodeData; export type StartNodeData = WorkflowStartNodeData | OtherStartNodeData;

View File

@@ -703,6 +703,8 @@ function getElements(
editable, editable,
useScriptCache: settings.useScriptCache, useScriptCache: settings.useScriptCache,
scriptCacheKey: settings.scriptCacheKey, scriptCacheKey: settings.scriptCacheKey,
label: "__start_block__",
showCode: false,
}), }),
); );
@@ -733,6 +735,8 @@ function getElements(
{ {
withWorkflowSettings: false, withWorkflowSettings: false,
editable, editable,
label: "__start_block__",
showCode: false,
}, },
block.id, block.id,
), ),