show all of the scripts via a button (#3294)
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -417,6 +417,8 @@ function Workspace({
|
||||
{
|
||||
withWorkflowSettings: false,
|
||||
editable: true,
|
||||
label: "__start_block__",
|
||||
showCode: false,
|
||||
},
|
||||
id,
|
||||
),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user