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,
blockType,
script,
title,
onClick,
onExit,
}: {
blockLabel: string;
blockType: WorkflowBlockType;
blockType?: WorkflowBlockType;
script: string | undefined;
title?: string;
onClick?: (e: React.MouseEvent) => void;
/**
* Return `false` to cancel the exit.
*/
onExit?: () => boolean;
}) {
const [searchParams] = useSearchParams();
const blockTitle = workflowBlockTitle[blockType];
const blockTitle = blockType ? workflowBlockTitle[blockType] : title;
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
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">
<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 w-full flex-col gap-1">
{blockLabel}
<div className="flex w-full items-center justify-center gap-1">
<span className="text-xs text-slate-400">{blockTitle}</span>
<div className="ml-auto scale-[60%] opacity-50">
<KeyIcon />
{blockType ? (
<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 w-full flex-col gap-1">
{blockLabel}
<div className="flex w-full items-center justify-center gap-1">
<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>
<span className="text-xs text-slate-400">
{cacheKeyValue === "" || !cacheKeyValue
? "(none)"
: cacheKeyValue}
</span>
</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">
<ExitIcon
onClick={() => {
toggleScriptForNodeCallback({
label: blockLabel,
show: false,
});
if (onExit) {
const result = onExit();
if (result !== false) {
toggleScriptForNodeCallback({
label: blockLabel,
show: false,
});
}
}
}}
className="size-5 cursor-pointer"
/>

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 {
Accordion,
@@ -25,12 +25,25 @@ import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "../Taskv2Node/types";
import { KeyValueInput } from "@/components/KeyValueInput";
import { OrgWalled } from "@/components/Orgwalled";
import { placeholders } from "@/routes/workflows/editor/helpContent";
import { NodeActionMenu } from "@/routes/workflows/editor/nodes/NodeActionMenu";
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>) {
const workflowSettingsStore = useWorkflowSettingsStore();
const credentialGetter = useCredentialGetter();
const { updateNodeData } = useReactFlow();
// const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
const reactFlowInstance = useReactFlow();
const { data: availableModels } = useQuery<ModelsResponse>({
queryKey: ["models"],
@@ -66,6 +79,15 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
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(() => {
workflowSettingsStore.setWorkflowSettings(inputs);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -79,148 +101,217 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
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) {
return (
<div>
<Handle
type="source"
position={Position.Bottom}
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">
<header>Start</header>
<Separator />
<Accordion type="single" collapsible>
<AccordionItem value="settings" className="border-b-0">
<AccordionTrigger className="py-2">
Workflow Settings
</AccordionTrigger>
<AccordionContent className="pl-6 pr-1 pt-1">
<div 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 className="space-y-2">
<div className="flex gap-2">
<Label>Webhook Callback URL</Label>
<HelpTooltip content="The URL of a webhook endpoint to send the workflow results" />
<Flippable facing={facing} preserveFrontsideHeight={true}>
<div>
<Handle
type="source"
position={Position.Bottom}
id="a"
className="opacity-0"
/>
<div className="w-[30rem] rounded-lg bg-slate-elevation3 px-6 py-4 text-center">
<div className="relative">
<div className="absolute right-0 top-0">
<div>
<div className="rounded p-1 hover:bg-muted">
<NodeActionMenu
isDeleteable={false}
isScriptable={true}
showScriptText="Show All Scripts"
onShowScript={showAllScripts}
/>
</div>
</div>
</div>
<header className="mb-4">Start</header>
<Separator />
<Accordion
type="single"
collapsible
onValueChange={() => rerender.bump()}
>
<AccordionItem value="settings" className="mt-4 border-b-0">
<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>
<Input
value={inputs.webhookCallbackUrl}
placeholder="https://"
onChange={(event) => {
handleChange(
"webhookCallbackUrl",
event.target.value,
);
}}
/>
</div>
<div className="space-y-2">
<div className="flex gap-2">
<Label>Proxy Location</Label>
<HelpTooltip content="Route Skyvern through one of our available proxies." />
<div className="space-y-2">
<div className="flex gap-2">
<Label>Webhook Callback URL</Label>
<HelpTooltip content="The URL of a webhook endpoint to send the workflow results" />
</div>
<Input
value={inputs.webhookCallbackUrl}
placeholder="https://"
onChange={(event) => {
handleChange(
"webhookCallbackUrl",
event.target.value,
);
}}
/>
</div>
<ProxySelector
value={inputs.proxyLocation}
onChange={(value) => {
handleChange("proxyLocation", value);
}}
/>
</div>
<OrgWalled className="flex flex-col gap-4">
<div className="space-y-2">
<div className="flex gap-2">
<Label>Proxy Location</Label>
<HelpTooltip content="Route Skyvern through one of our available proxies." />
</div>
<ProxySelector
value={inputs.proxyLocation}
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="flex items-center gap-2">
<Label>Generate Script</Label>
<HelpTooltip content="Generate & use cached scripts for faster execution." />
<Label>Save &amp; Reuse Session</Label>
<HelpTooltip content="Persist session information across workflow runs" />
<Switch
className="ml-auto"
checked={inputs.useScriptCache}
checked={inputs.persistBrowserSession}
onCheckedChange={(value) => {
handleChange("useScriptCache", value);
handleChange("persistBrowserSession", 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 className="space-y-2">
<div className="flex items-center gap-2">
<Label>Extra HTTP Headers</Label>
<HelpTooltip content="Specify some self-defined HTTP requests headers" />
</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>
)}
</OrgWalled>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label>Save &amp; Reuse Session</Label>
<HelpTooltip content="Persist session information across workflow runs" />
<Switch
className="ml-auto"
checked={inputs.persistBrowserSession}
onCheckedChange={(value) => {
handleChange("persistBrowserSession", value);
<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>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label>Extra HTTP Headers</Label>
<HelpTooltip content="Specify some self-defined HTTP requests headers" />
</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>
</AccordionContent>
</AccordionItem>
</Accordion>
</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;
useScriptCache: boolean;
scriptCacheKey: string | null;
label: "__start_block__";
showCode: boolean;
};
export type OtherStartNodeData = {
withWorkflowSettings: false;
editable: boolean;
label: "__start_block__";
showCode: boolean;
};
export type StartNodeData = WorkflowStartNodeData | OtherStartNodeData;

View File

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