Add "Print PDF" Block (#4452)
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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 };
|
||||
@@ -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";
|
||||
}
|
||||
@@ -91,6 +91,9 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) {
|
||||
case "http_request": {
|
||||
return <GlobeIcon className={className} />;
|
||||
}
|
||||
case "print_page": {
|
||||
return <FileTextIcon className={className} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user