http block support multipart (#4259)
This commit is contained in:
@@ -58,6 +58,8 @@ const headersTooltip =
|
||||
"HTTP headers to include with the request as JSON object.";
|
||||
const bodyTooltip =
|
||||
"Request body as JSON object. Only used for POST, PUT, PATCH methods.";
|
||||
const filesTooltip =
|
||||
'Files to upload as multipart/form-data. Dictionary mapping field names to file paths/URLs. Supports HTTP/HTTPS URLs, S3 URIs (s3://), or limited local file access. Example: {"file": "https://example.com/file.pdf"} or {"document": "s3://bucket/path/file.pdf"}';
|
||||
const timeoutTooltip = "Request timeout in seconds.";
|
||||
const followRedirectsTooltip =
|
||||
"Whether to automatically follow HTTP redirects.";
|
||||
@@ -287,12 +289,34 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Files Section */}
|
||||
{showBodyEditor && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">Files</Label>
|
||||
<HelpTooltip content={filesTooltip} />
|
||||
</div>
|
||||
<CodeEditor
|
||||
className="w-full"
|
||||
language="json"
|
||||
value={data.files}
|
||||
onChange={(value) => {
|
||||
update({ files: value || "{}" });
|
||||
}}
|
||||
readOnly={!editable}
|
||||
minHeight="80px"
|
||||
maxHeight="160px"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Request Preview */}
|
||||
<RequestPreview
|
||||
method={data.method}
|
||||
url={data.url}
|
||||
headers={data.headers}
|
||||
body={data.body}
|
||||
files={data.files}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -182,11 +182,13 @@ export function RequestPreview({
|
||||
url,
|
||||
headers,
|
||||
body,
|
||||
files,
|
||||
}: {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: string;
|
||||
body: string;
|
||||
files?: string;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
@@ -194,6 +196,8 @@ export function RequestPreview({
|
||||
|
||||
if (!hasContent) return null;
|
||||
|
||||
const hasFiles = files && files.trim() && files !== "{}";
|
||||
|
||||
return (
|
||||
<div className="rounded-md border bg-slate-50 p-3 dark:bg-slate-900/50">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -202,6 +206,11 @@ export function RequestPreview({
|
||||
<span className="font-mono text-sm text-slate-600 dark:text-slate-400">
|
||||
{url || "No URL specified"}
|
||||
</span>
|
||||
{hasFiles && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Files
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -232,6 +241,17 @@ export function RequestPreview({
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Files (only for POST, PUT, PATCH) */}
|
||||
{["POST", "PUT", "PATCH"].includes(method.toUpperCase()) &&
|
||||
hasFiles && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-medium">Files:</div>
|
||||
<pre className="overflow-x-auto rounded bg-slate-100 p-2 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-400">
|
||||
{files || "{}"}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ export type HttpRequestNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
headers: string; // JSON string representation of headers
|
||||
body: string; // JSON string representation of body
|
||||
files: string; // JSON string representation of files (dict mapping field names to file paths/URLs)
|
||||
timeout: number;
|
||||
followRedirects: boolean;
|
||||
parameterKeys: Array<string>;
|
||||
@@ -22,6 +23,7 @@ export const httpRequestNodeDefaultData: HttpRequestNodeData = {
|
||||
url: "",
|
||||
headers: "{}",
|
||||
body: "{}",
|
||||
files: "{}",
|
||||
timeout: 30,
|
||||
followRedirects: true,
|
||||
parameterKeys: [],
|
||||
|
||||
@@ -760,6 +760,7 @@ function convertToNode(
|
||||
url: block.url ?? "",
|
||||
headers: JSON.stringify(block.headers || {}, null, 2),
|
||||
body: JSON.stringify(block.body || {}, null, 2),
|
||||
files: JSON.stringify(block.files || {}, null, 2),
|
||||
timeout: block.timeout,
|
||||
followRedirects: block.follow_redirects,
|
||||
parameterKeys: block.parameters.map((p) => p.key),
|
||||
@@ -2193,6 +2194,17 @@ function getWorkflowBlock(
|
||||
string
|
||||
> | null,
|
||||
body: JSONParseSafe(node.data.body) as Record<string, unknown> | null,
|
||||
files: (() => {
|
||||
const parsed = JSONParseSafe(node.data.files) as Record<
|
||||
string,
|
||||
string
|
||||
> | null;
|
||||
// Convert empty object to null to match backend's "if not self.files" check
|
||||
if (parsed && Object.keys(parsed).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
})(),
|
||||
timeout: node.data.timeout,
|
||||
follow_redirects: node.data.followRedirects,
|
||||
parameter_keys: node.data.parameterKeys,
|
||||
|
||||
@@ -546,6 +546,7 @@ export type HttpRequestBlock = WorkflowBlockBase & {
|
||||
url: string | null;
|
||||
headers: Record<string, string> | null;
|
||||
body: Record<string, unknown> | null;
|
||||
files: Record<string, string> | null; // Dictionary mapping field names to file paths/URLs
|
||||
timeout: number;
|
||||
follow_redirects: boolean;
|
||||
parameters: Array<WorkflowParameter>;
|
||||
|
||||
@@ -396,6 +396,7 @@ export type HttpRequestBlockYAML = BlockYAMLBase & {
|
||||
url: string | null;
|
||||
headers: Record<string, string> | null;
|
||||
body: Record<string, unknown> | null;
|
||||
files?: Record<string, string> | null; // Dictionary mapping field names to file paths/URLs
|
||||
timeout: number;
|
||||
follow_redirects: boolean;
|
||||
parameter_keys?: Array<string> | null;
|
||||
|
||||
Reference in New Issue
Block a user