Add "Print PDF" Block (#4452)

This commit is contained in:
Marc Kelechava
2026-01-14 15:46:49 -08:00
committed by GitHub
parent 7dcfa00508
commit 4c2c7df42c
16 changed files with 539 additions and 7 deletions

View File

@@ -14,6 +14,7 @@ export const ArtifactType = {
HTMLScrape: "html_scrape",
SkyvernLog: "skyvern_log",
SkyvernLogRaw: "skyvern_log_raw",
PDF: "pdf",
} as const;
export type ArtifactType = (typeof ArtifactType)[keyof typeof ArtifactType];

View File

@@ -0,0 +1,162 @@
import { HelpTooltip } from "@/components/HelpTooltip";
import { Label } from "@/components/ui/label";
import { Handle, NodeProps, Position } from "@xyflow/react";
import type { PrintPageNode } from "./types";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
function PrintPageNode({ id, data }: NodeProps<PrintPageNode>) {
const { editable, label } = data;
const { blockLabel: urlBlockLabel } = useParams();
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const update = useUpdate<PrintPageNode["data"]>({ id, editable });
return (
<div>
<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(
"w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
{
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>
<NodeHeader
blockLabel={label}
editable={editable}
nodeId={id}
totpIdentifier={null}
totpUrl={null}
type="print_page"
/>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-xs text-slate-300">Page Format</Label>
<Select
value={data.format}
onValueChange={(value) => update({ format: value })}
disabled={!editable}
>
<SelectTrigger className="nopan w-36 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="A4">A4</SelectItem>
<SelectItem value="Letter">Letter</SelectItem>
<SelectItem value="Legal">Legal</SelectItem>
<SelectItem value="Tabloid">Tabloid</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs text-slate-300">Print Background</Label>
<HelpTooltip content="Include CSS background colors and images in the PDF" />
</div>
<Switch
checked={data.printBackground}
onCheckedChange={(checked) =>
update({ printBackground: checked })
}
disabled={!editable}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Label className="text-xs font-normal text-slate-300">
Headers & Footers
</Label>
<HelpTooltip content="Adds date, title, URL, and page numbers to the PDF" />
</div>
<Switch
checked={data.includeTimestamp}
onCheckedChange={(checked) =>
update({ includeTimestamp: checked })
}
disabled={!editable}
/>
</div>
</div>
<Separator />
<Accordion type="single" collapsible>
<AccordionItem value="advanced" className="border-b-0">
<AccordionTrigger className="py-0">
Advanced Settings
</AccordionTrigger>
<AccordionContent className="pl-6 pr-1 pt-1">
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs text-slate-300">
Custom Filename
</Label>
<Input
value={data.customFilename}
onChange={(e) => update({ customFilename: e.target.value })}
placeholder="my_report"
disabled={!editable}
className="nopan text-xs"
/>
</div>
<div className="flex items-center justify-between">
<Label className="text-xs font-normal text-slate-300">
Landscape
</Label>
<Switch
checked={data.landscape}
onCheckedChange={(checked) =>
update({ landscape: checked })
}
disabled={!editable}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
);
}
export { PrintPageNode };

View File

@@ -0,0 +1,30 @@
import type { Node } from "@xyflow/react";
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { NodeBaseData } from "../types";
export type PrintPageNodeData = NodeBaseData & {
includeTimestamp: boolean;
customFilename: string;
format: string;
landscape: boolean;
printBackground: boolean;
};
export type PrintPageNode = Node<PrintPageNodeData, "printPage">;
export const printPageNodeDefaultData: PrintPageNodeData = {
debuggable: debuggableWorkflowBlockTypes.has("print_page"),
label: "",
continueOnFailure: false,
editable: true,
model: null,
includeTimestamp: true,
customFilename: "",
format: "A4",
landscape: false,
printBackground: true,
};
export function isPrintPageNode(node: Node): node is PrintPageNode {
return node.type === "printPage";
}

View File

@@ -91,6 +91,9 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) {
case "http_request": {
return <GlobeIcon className={className} />;
}
case "print_page": {
return <FileTextIcon className={className} />;
}
}
}

View File

@@ -47,6 +47,8 @@ import { HttpRequestNode } from "./HttpRequestNode/types";
import { HttpRequestNode as HttpRequestNodeComponent } from "./HttpRequestNode/HttpRequestNode";
import { HumanInteractionNode } from "./HumanInteractionNode/types";
import { HumanInteractionNode as HumanInteractionNodeComponent } from "./HumanInteractionNode/HumanInteractionNode";
import { PrintPageNode } from "./PrintPageNode/types";
import { PrintPageNode as PrintPageNodeComponent } from "./PrintPageNode/PrintPageNode";
export type UtilityNode = StartNode | NodeAdderNode;
@@ -72,7 +74,8 @@ export type WorkflowBlockNode =
| PDFParserNode
| Taskv2Node
| URLNode
| HttpRequestNode;
| HttpRequestNode
| PrintPageNode;
export function isUtilityNode(node: AppNode): node is UtilityNode {
return node.type === "nodeAdder" || node.type === "start";
@@ -109,4 +112,5 @@ export const nodeTypes = {
taskv2: memo(Taskv2NodeComponent),
url: memo(URLNodeComponent),
http_request: memo(HttpRequestNodeComponent),
printPage: memo(PrintPageNodeComponent),
} as const;

View File

@@ -66,4 +66,5 @@ export const workflowBlockTitle: {
task_v2: "Browser Task v2",
goto_url: "Go to URL",
http_request: "HTTP Request",
print_page: "Print Page",
};

View File

@@ -266,6 +266,17 @@ const nodeLibraryItems: Array<{
title: "HTTP Request Block",
description: "Make HTTP API calls",
},
{
nodeType: "printPage",
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.PrintPage}
className="size-6"
/>
),
title: "Print Page Block",
description: "Print current page to PDF",
},
];
type Props = {

View File

@@ -46,6 +46,7 @@ import {
URLBlockYAML,
FileUploadBlockYAML,
HttpRequestBlockYAML,
PrintPageBlockYAML,
} from "../types/workflowYamlTypes";
import {
EMAIL_BLOCK_SENDER,
@@ -122,6 +123,7 @@ import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types";
import { urlNodeDefaultData } from "./nodes/URLNode/types";
import { fileUploadNodeDefaultData } from "./nodes/FileUploadNode/types";
import { httpRequestNodeDefaultData } from "./nodes/HttpRequestNode/types";
import { printPageNodeDefaultData } from "./nodes/PrintPageNode/types";
export const NEW_NODE_LABEL_PREFIX = "block_";
@@ -839,6 +841,21 @@ function convertToNode(
},
};
}
case "print_page": {
return {
...identifiers,
...common,
type: "printPage",
data: {
...commonData,
includeTimestamp: block.include_timestamp ?? false,
customFilename: block.custom_filename ?? "",
format: block.format ?? "A4",
landscape: block.landscape ?? false,
printBackground: block.print_background ?? true,
},
};
}
}
}
@@ -1877,6 +1894,17 @@ function createNode(
},
};
}
case "printPage": {
return {
...identifiers,
...common,
type: "printPage",
data: {
...printPageNodeDefaultData,
label,
},
};
}
case "conditional": {
const branches = createDefaultBranchConditions();
return {
@@ -2332,6 +2360,17 @@ function getWorkflowBlock(
save_response_as_file: node.data.saveResponseAsFile,
};
}
case "printPage": {
return {
...base,
block_type: "print_page",
include_timestamp: node.data.includeTimestamp,
custom_filename: node.data.customFilename || null,
format: node.data.format,
landscape: node.data.landscape,
print_background: node.data.printBackground,
};
}
case "conditional": {
return serializeConditionalBlock(node as ConditionalNode, nodes, edges);
}
@@ -3338,6 +3377,18 @@ function convertBlocksToBlockYAML(
};
return blockYaml;
}
case "print_page": {
const blockYaml: PrintPageBlockYAML = {
...base,
block_type: "print_page",
include_timestamp: block.include_timestamp,
custom_filename: block.custom_filename,
format: block.format,
landscape: block.landscape,
print_background: block.print_background,
};
return blockYaml;
}
}
});
}

View File

@@ -211,7 +211,8 @@ export type WorkflowBlock =
| PDFParserBlock
| Taskv2Block
| URLBlock
| HttpRequestBlock;
| HttpRequestBlock
| PrintPageBlock;
export const WorkflowBlockTypes = {
Task: "task",
@@ -236,6 +237,7 @@ export const WorkflowBlockTypes = {
Taskv2: "task_v2",
URL: "goto_url",
HttpRequest: "http_request",
PrintPage: "print_page",
} as const;
// all of them
@@ -554,6 +556,15 @@ export type HttpRequestBlock = WorkflowBlockBase & {
save_response_as_file: boolean;
};
export type PrintPageBlock = WorkflowBlockBase & {
block_type: "print_page";
include_timestamp: boolean;
custom_filename: string | null;
format: string;
landscape: boolean;
print_background: boolean;
};
export type WorkflowDefinition = {
version?: number | null;
parameters: Array<Parameter>;

View File

@@ -141,7 +141,8 @@ export type BlockYAML =
| PDFParserBlockYAML
| Taskv2BlockYAML
| URLBlockYAML
| HttpRequestBlockYAML;
| HttpRequestBlockYAML
| PrintPageBlockYAML;
export type BlockYAMLBase = {
block_type: WorkflowBlockType;
@@ -404,3 +405,12 @@ export type HttpRequestBlockYAML = BlockYAMLBase & {
download_filename?: string | null;
save_response_as_file?: boolean;
};
export type PrintPageBlockYAML = BlockYAMLBase & {
block_type: "print_page";
include_timestamp: boolean;
custom_filename: string | null;
format: string;
landscape: boolean;
print_background: boolean;
};