Skip to content

Commit

Permalink
Add handling for different request body types.
Browse files Browse the repository at this point in the history
  • Loading branch information
c0rtexR committed Oct 17, 2024
1 parent 4506d42 commit 2546b99
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 75 deletions.
4 changes: 0 additions & 4 deletions packages/blocks/src/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,6 @@ export abstract class Block implements IBlock {
}
}

if (missingFields.length > 0 || invalidFields.length > 0) {
debugger;
}

return {
valid: missingFields.length === 0 && invalidFields.length === 0,
missingFields,
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/blocks/requestNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const requestNode: Omit<Node<RequestNodeData>, "position"> = {
value: "application/json",
},
],
body: "{}",
bodyType: "none",
},
controls: [
{
Expand Down
63 changes: 38 additions & 25 deletions packages/editor/src/components/MonacoEditorWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { Editor, EditorProps, OnMount } from "@monaco-editor/react";
import {
MemoExoticComponent,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { MemoExoticComponent, useCallback, useEffect, useState } from "react";
import { editor } from "monaco-editor";
import darkTheme from "../themes/dark.json";
import useTheme from "@data-river/shared/ui/hooks/useTheme";
import { Skeleton } from "@data-river/shared/ui";

const EditorSkeleton = () => {
return (
<div className="flex flex-col space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
);
};

const MonacoEditorWrapper = (props: EditorProps) => {
const [MonacoEditor, setMonacoEditor] = useState<
MemoExoticComponent<typeof Editor>
>(null as unknown as MemoExoticComponent<typeof Editor>);

const editor = useRef<editor.IStandaloneCodeEditor | null>(null);
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor | null>(
null,
);

const theme = useTheme();
useEffect(() => {
Expand All @@ -26,33 +34,38 @@ const MonacoEditorWrapper = (props: EditorProps) => {
});
}, []);

const handleEditorDidMount: OnMount = useCallback((_editor, monaco) => {
const customTheme: editor.IStandaloneThemeData = {
base: "vs-dark",
inherit: true,
colors: darkTheme.colors,
rules: [],
};
const handleEditorDidMount: OnMount = useCallback(
(_editor, monaco) => {
const customTheme: editor.IStandaloneThemeData = {
base: "vs-dark",
inherit: true,
colors: darkTheme.colors,
rules: [],
};

monaco.editor.defineTheme("customTheme", customTheme);
editor.current = _editor;
}, []);
monaco.editor.defineTheme("customTheme", customTheme);
theme === "dark"
? monaco.editor.setTheme("customTheme")
: monaco.editor.setTheme("vs-light");
setEditor(_editor);
},
[theme, setEditor],
);

useEffect(() => {
if (theme === "dark") {
editor.current?.updateOptions({ theme: "customTheme" });
} else {
editor.current?.updateOptions({ theme: "vs-light" });
}
}, [theme, editor.current]);
editor?.updateOptions({
theme: theme === "dark" ? "customTheme" : "vs-light",
});
}, [theme, editor]);

if (!MonacoEditor) return <div>Loading Editor...</div>;
if (!MonacoEditor) return <EditorSkeleton />;

return (
<MonacoEditor
{...props}
options={{ ...props.options }}
onMount={handleEditorDidMount}
loading={<EditorSkeleton />}
/>
);
};
Expand Down
213 changes: 185 additions & 28 deletions packages/editor/src/components/panelViews/Request/BodyTab.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,194 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { Label, RadioGroup, RadioGroupItem } from "@data-river/shared/ui";
import MonacoEditorWrapper from "../../MonacoEditorWrapper";
import { KeyValueTable, KeyValuePair } from "./QueryParamsTable";
import _ from "lodash";

export type RequestBodyType =
| "none"
| "json"
| "form-data"
| "x-www-form-urlencoded";

interface BodyTabProps {
body: string;
handleEditorChange: (value: string | undefined) => void;
jsonError: string | null;
bodyType: RequestBodyType;
handleBodyChange: (value: string | undefined) => void;
handleBodyTypeChange: (value: RequestBodyType) => void;
validationError: string | null;
}

export function BodyTab({ body, handleEditorChange, jsonError }: BodyTabProps) {
export function BodyTab({
body,
bodyType,
handleBodyChange,
handleBodyTypeChange,
validationError,
}: BodyTabProps) {
const [jsonBody, setJsonBody] = useState(
body && bodyType === "json" ? body : "{}",
);
const [formData, setFormData] = useState<KeyValuePair[]>([]);
const [urlEncodedData, setUrlEncodedData] = useState<KeyValuePair[]>([]);

useEffect(() => {
if (bodyType === "json" && body !== jsonBody) {
setJsonBody(body || "{}");
} else if (
bodyType === "form-data" ||
bodyType === "x-www-form-urlencoded"
) {
const parsedData = parseBodyToFormData(body);
if (bodyType === "form-data") {
setFormData(parsedData);
} else {
setUrlEncodedData(parsedData);
}
}
}, [body, bodyType]);

const updateBody = useCallback(
(newBodyType: RequestBodyType, newBody: string) => {
handleBodyTypeChange(newBodyType);
handleBodyChange(newBody);
},
[handleBodyTypeChange, handleBodyChange],
);

const parseBodyToFormData = (bodyString: string): KeyValuePair[] => {
if (!bodyString || bodyString === "{}" || bodyString === "[]") return [];
try {
if (
bodyString.trim().startsWith("{") ||
bodyString.trim().startsWith("[")
) {
// It's likely JSON, don't parse it as form data
return [];
}
const pairs = bodyString.split("&");
return pairs.map((pair) => {
const [key, value] = pair.split("=");
return {
id: _.uniqueId("form-data-"),
key: decodeURIComponent(key),
value: decodeURIComponent(value || ""),
};
});
} catch (error) {
console.error("Error parsing body to form data:", error);
return [];
}
};

const formDataToString = (data: KeyValuePair[]): string => {
return data
.filter((item) => item.key.trim() !== "") // Only include items with non-empty keys
.map(
(item) =>
`${encodeURIComponent(item.key)}=${encodeURIComponent(item.value)}`,
)
.join("&");
};

const handleFormDataChange = (
newFormData: KeyValuePair[],
type: "form-data" | "x-www-form-urlencoded",
) => {
if (type === "form-data") {
setFormData(newFormData);
} else {
setUrlEncodedData(newFormData);
}
const newBodyString = formDataToString(newFormData);
updateBody(type, newBodyString);
};

const handleJsonChange = (value: string | undefined) => {
setJsonBody(value || "{}");
updateBody("json", value || "{}");
};

const handleBodyTypeChangeInternal = (newBodyType: RequestBodyType) => {
let newBody = "";
switch (newBodyType) {
case "json":
newBody = jsonBody;
break;
case "form-data":
newBody = formDataToString(formData);
break;
case "x-www-form-urlencoded":
newBody = formDataToString(urlEncodedData);
break;
case "none":
newBody = "";
break;
}
updateBody(newBodyType, newBody);
};

const renderBodyContent = () => {
switch (bodyType) {
case "none":
return (
<div className="flex justify-center items-center h-32 border rounded-md">
<p className="text-muted-foreground">
This request does not have a body
</p>
</div>
);
case "json":
return (
<div className="border rounded-md">
<MonacoEditorWrapper
height="150px"
defaultLanguage="json"
value={jsonBody}
onChange={handleJsonChange}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
folding: false,
lineNumbers: "off",
wordWrap: "on",
wrappingIndent: "deepIndent",
automaticLayout: true,
suggest: {
showProperties: false,
},
formatOnPaste: true,
formatOnType: true,
}}
/>
</div>
);
case "form-data":
return (
<KeyValueTable
data={formData}
setData={(newData) => handleFormDataChange(newData, "form-data")}
title="Form Field"
/>
);
case "x-www-form-urlencoded":
return (
<KeyValueTable
data={urlEncodedData}
setData={(newData) =>
handleFormDataChange(newData, "x-www-form-urlencoded")
}
title="Form Field"
/>
);
default:
return null;
}
};

return (
<div className="flex flex-col gap-4 mt-6">
<Label>Body</Label>
<RadioGroup defaultValue="none">
<RadioGroup value={bodyType} onValueChange={handleBodyTypeChangeInternal}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="none" id="none" />
<Label htmlFor="none">None</Label>
Expand All @@ -35,29 +211,10 @@ export function BodyTab({ body, handleEditorChange, jsonError }: BodyTabProps) {
</RadioGroup>

<div>
<div className="border rounded-md">
<MonacoEditorWrapper
height="150px"
defaultLanguage="json"
defaultValue={body}
onChange={handleEditorChange}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
folding: false,
lineNumbers: "off",
wordWrap: "on",
wrappingIndent: "deepIndent",
automaticLayout: true,
suggest: {
showProperties: false,
},
formatOnPaste: true,
formatOnType: true,
}}
/>
</div>
{jsonError && <p className="text-red-500 text-sm mt-1">{jsonError}</p>}
{renderBodyContent()}
{bodyType === "json" && validationError && (
<p className="text-red-500 text-sm mt-1">{validationError}</p>
)}
</div>
</div>
);
Expand Down
Loading

0 comments on commit 2546b99

Please sign in to comment.