From 9a02b63a6b0e816cd5a99b9399fcff8465bbb4d0 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 13 Nov 2023 11:28:37 -0800 Subject: [PATCH] Form urlencoded bodies! --- src-tauri/src/send.rs | 22 ++++++++- src-web/components/FormUrlencodedEditor.tsx | 37 +++++++++++++++ src-web/components/RequestPane.tsx | 46 ++++++++++++++++--- ...meterEditor.tsx => UrlParameterEditor.tsx} | 0 src-web/lib/models.ts | 4 +- 5 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 src-web/components/FormUrlencodedEditor.tsx rename src-web/components/{ParameterEditor.tsx => UrlParameterEditor.tsx} (100%) diff --git a/src-tauri/src/send.rs b/src-tauri/src/send.rs index c49d1a4d..dfd40704 100644 --- a/src-tauri/src/send.rs +++ b/src-tauri/src/send.rs @@ -121,14 +121,32 @@ pub async fn actually_send_request( } request_builder = request_builder.query(&query_params); + if let Some(t) = &request.body_type { - let empty_value = &serde_json::to_value("").unwrap(); + let empty_string = &serde_json::to_value("").unwrap(); + let empty_bool = &serde_json::to_value(false).unwrap(); let b = request.body.0; if b.contains_key("text") { - let raw_text = b.get("text").unwrap_or(empty_value).as_str().unwrap_or(""); + let raw_text = b.get("text").unwrap_or(empty_string).as_str().unwrap_or(""); let body = render::render(raw_text, &workspace, environment_ref); request_builder = request_builder.body(body); + } else if b.contains_key("form") { + let mut form_params = Vec::new(); + let form = b.get("form"); + if let Some(f) = form { + for p in f.as_array().unwrap_or(&Vec::new()) { + let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false); + let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default(); + let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default(); + if !enabled || name.is_empty() { continue; } + form_params.push(( + render::render(name, &workspace, environment_ref), + render::render(value, &workspace, environment_ref), + )); + } + } + request_builder = request_builder.form(&form_params); } else { warn!("Unsupported body type: {}", t); } diff --git a/src-web/components/FormUrlencodedEditor.tsx b/src-web/components/FormUrlencodedEditor.tsx new file mode 100644 index 00000000..754d7f22 --- /dev/null +++ b/src-web/components/FormUrlencodedEditor.tsx @@ -0,0 +1,37 @@ +import { useCallback, useMemo } from 'react'; +import type { HttpRequest } from '../lib/models'; +import type { PairEditorProps } from './core/PairEditor'; +import { PairEditor } from './core/PairEditor'; + +type Props = { + forceUpdateKey: string; + body: HttpRequest['body']; + onChange: (headers: HttpRequest['body']) => void; +}; + +export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props) { + const pairs = useMemo( + () => + (Array.isArray(body.form) ? body.form : []).map((p) => ({ + enabled: p.enabled, + name: p.name, + value: p.value, + })), + [body.form], + ); + + const handleChange = useCallback( + (pairs) => onChange({ form: pairs }), + [onChange], + ); + + return ( + + ); +} diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 81b691e5..424277f3 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -15,6 +15,8 @@ import { AUTH_TYPE_BASIC, AUTH_TYPE_BEARER, AUTH_TYPE_NONE, + BODY_TYPE_FORM_MULTIPART, + BODY_TYPE_FORM_URLENCODED, BODY_TYPE_GRAPHQL, BODY_TYPE_JSON, BODY_TYPE_NONE, @@ -27,10 +29,11 @@ import { Editor } from './core/Editor'; import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; import { EmptyStateText } from './EmptyStateText'; +import { FormUrlencodedEditor } from './FormUrlencodedEditor'; import { GraphQLEditor } from './GraphQLEditor'; import { HeadersEditor } from './HeadersEditor'; -import { UrlParametersEditor } from './ParameterEditor'; import { UrlBar } from './UrlBar'; +import { UrlParametersEditor } from './UrlParameterEditor'; interface Props { style?: CSSProperties; @@ -59,10 +62,14 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN options: { value: activeRequest.bodyType, items: [ + { type: 'separator', label: 'Form Data' }, + { label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED }, + // { label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, + { type: 'separator', label: 'Text Content' }, { label: 'JSON', value: BODY_TYPE_JSON }, { label: 'XML', value: BODY_TYPE_XML }, { label: 'GraphQL', value: BODY_TYPE_GRAPHQL }, - { type: 'separator' }, + { type: 'separator', label: 'Other' }, { label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE }, ], onChange: async (bodyType) => { @@ -71,7 +78,24 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN patch.headers = activeRequest?.headers.filter( (h) => h.name.toLowerCase() !== 'content-type', ); - } else if (bodyType == BODY_TYPE_GRAPHQL || bodyType === BODY_TYPE_JSON) { + } else if ( + bodyType === BODY_TYPE_FORM_URLENCODED || + bodyType === BODY_TYPE_FORM_MULTIPART || + bodyType === BODY_TYPE_JSON || + bodyType === BODY_TYPE_XML + ) { + patch.method = 'POST'; + patch.headers = [ + ...(activeRequest?.headers.filter( + (h) => h.name.toLowerCase() !== 'content-type', + ) ?? []), + { + name: 'Content-Type', + value: bodyType, + enabled: true, + }, + ]; + } else if (bodyType == BODY_TYPE_GRAPHQL) { patch.method = 'POST'; patch.headers = [ ...(activeRequest?.headers.filter( @@ -141,6 +165,10 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN [activeRequest, updateRequest], ); + const handleBodyChange = useCallback( + (body: HttpRequest['body']) => updateRequest.mutate({ body }), + [updateRequest], + ); const handleBodyTextChange = useCallback( (text: string) => updateRequest.mutate({ body: { text } }), [updateRequest], @@ -223,7 +251,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN placeholder="..." className="!bg-gray-50" heightMode={fullHeight ? 'full' : 'auto'} - defaultValue={`${activeRequest?.body?.text}` ?? ''} + defaultValue={`${activeRequest?.body?.text ?? ''}`} contentType="application/json" onChange={handleBodyTextChange} format={(v) => tryFormatJson(v)} @@ -236,7 +264,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN placeholder="..." className="!bg-gray-50" heightMode={fullHeight ? 'full' : 'auto'} - defaultValue={`${activeRequest?.body?.text}` ?? ''} + defaultValue={`${activeRequest?.body?.text ?? ''}`} contentType="text/xml" onChange={handleBodyTextChange} /> @@ -245,9 +273,15 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN forceUpdateKey={forceUpdateKey} baseRequest={activeRequest} className="!bg-gray-50" - defaultValue={`${activeRequest?.body?.text}` ?? ''} + defaultValue={`${activeRequest?.body?.text ?? ''}`} onChange={handleBodyTextChange} /> + ) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? ( + ) : ( No Body )} diff --git a/src-web/components/ParameterEditor.tsx b/src-web/components/UrlParameterEditor.tsx similarity index 100% rename from src-web/components/ParameterEditor.tsx rename to src-web/components/UrlParameterEditor.tsx diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index c3bb2ca0..f6b8c19e 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -1,6 +1,8 @@ export const BODY_TYPE_NONE = null; export const BODY_TYPE_GRAPHQL = 'graphql'; export const BODY_TYPE_JSON = 'application/json'; +export const BODY_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'; +export const BODY_TYPE_FORM_MULTIPART = 'multipart/form-data'; export const BODY_TYPE_XML = 'text/xml'; export const AUTH_TYPE_NONE = null; @@ -63,7 +65,7 @@ export interface HttpRequest extends BaseModel { name: string; url: string; urlParameters: HttpUrlParameter[]; - body: Record; + body: Record; bodyType: string | null; authentication: Record; authenticationType: string | null;