diff --git a/packages/hoppscotch-app/components/http/Body.vue b/packages/hoppscotch-app/components/http/Body.vue index f2a635210..368766c44 100644 --- a/packages/hoppscotch-app/components/http/Body.vue +++ b/packages/hoppscotch-app/components/http/Body.vue @@ -50,6 +50,9 @@ +
+
+
+ +
+ + + + +
+
+
+
+
+ + + + + + + + +
+
+ + + {{ t("empty.headers") }} + + +
+
+
+ + + diff --git a/packages/hoppscotch-app/helpers/functional/array.ts b/packages/hoppscotch-app/helpers/functional/array.ts new file mode 100644 index 000000000..f2e387d17 --- /dev/null +++ b/packages/hoppscotch-app/helpers/functional/array.ts @@ -0,0 +1,2 @@ +export const stringArrayJoin = (separator: string) => (arr: string[]) => + arr.join(separator) diff --git a/packages/hoppscotch-app/helpers/rawKeyValue.ts b/packages/hoppscotch-app/helpers/rawKeyValue.ts new file mode 100644 index 000000000..b89f2d3ac --- /dev/null +++ b/packages/hoppscotch-app/helpers/rawKeyValue.ts @@ -0,0 +1,39 @@ +import * as A from "fp-ts/Array" +import * as RA from "fp-ts/ReadonlyArray" +import * as S from "fp-ts/string" +import { pipe, flow } from "fp-ts/function" +import { stringArrayJoin } from "./functional/array" + +export type RawKeyValueEntry = { + key: string + value: string + active: boolean +} + +const parseRawKeyValueEntry = (str: string): RawKeyValueEntry => { + const trimmed = str.trim() + const inactive = trimmed.startsWith("#") + + const [key, value] = trimmed.split(":").map(S.trim) + + return { + key: inactive ? key.replaceAll(/^#+\s*/g, "") : key, // Remove comment hash and early space + value, + active: !inactive, + } +} + +export const parseRawKeyValueEntries = flow( + S.split("\n"), + RA.map(parseRawKeyValueEntry), + RA.toArray +) + +export const rawKeyValueEntriesToString = (entries: RawKeyValueEntry[]) => + pipe( + entries, + A.map(({ key, value, active }) => + active ? `${key}: ${value}` : `# ${key}: ${value}` + ), + stringArrayJoin("\n") + ) diff --git a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts index 73dbd0928..d204dd750 100644 --- a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts +++ b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts @@ -1,7 +1,6 @@ -import * as RA from "fp-ts/ReadonlyArray" -import * as S from "fp-ts/string" +import * as A from "fp-ts/Array" import qs from "qs" -import { pipe, flow } from "fp-ts/function" +import { pipe } from "fp-ts/function" import { combineLatest, Observable } from "rxjs" import { map } from "rxjs/operators" import { @@ -11,6 +10,7 @@ import { } from "@hoppscotch/data" import { parseTemplateString, parseBodyEnvVariables } from "../templating" import { tupleToRecord } from "../functional/record" +import { parseRawKeyValueEntries } from "../rawKeyValue" import { Environment, getGlobalVariables } from "~/newstore/environments" export interface EffectiveHoppRESTRequest extends HoppRESTRequest { @@ -66,18 +66,15 @@ function getFinalBodyFromRequest( if (request.body.contentType === "application/x-www-form-urlencoded") { return pipe( request.body.body, - S.split("\n"), - RA.map( - flow( - // Define how each lines are parsed + parseRawKeyValueEntries, - S.split(":"), // Split by ":" - RA.map(S.trim), // Remove trailing spaces in key/value begins and ends - ([key, value]) => [key, value ?? ""] as [string, string] // Add a default empty by default - ) - ), - RA.toArray, - tupleToRecord, // Convert the tuple to a record + // Filter out active + A.filter((x) => x.active), + // Convert to tuple + A.map(({ key, value }) => [key, value] as [string, string]), + // Tuple to Record object + tupleToRecord, + // Stringify qs.stringify ) } diff --git a/packages/hoppscotch-app/newstore/RESTSession.ts b/packages/hoppscotch-app/newstore/RESTSession.ts index 324f1aaf9..d00d7314f 100644 --- a/packages/hoppscotch-app/newstore/RESTSession.ts +++ b/packages/hoppscotch-app/newstore/RESTSession.ts @@ -1,3 +1,5 @@ +import * as A from "fp-ts/Array" +import { pipe } from "fp-ts/function" import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators" import { Ref } from "@nuxtjs/composition-api" import { @@ -15,6 +17,11 @@ import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" import { useStream } from "~/helpers/utils/composables" import { HoppTestResult } from "~/helpers/types/HoppTestResult" import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext" +import { + parseRawKeyValueEntries, + rawKeyValueEntriesToString, + RawKeyValueEntry, +} from "~/helpers/rawKeyValue" type RESTSession = { request: HoppRESTRequest @@ -203,9 +210,30 @@ const dispatchers = defineDispatchers({ curr: RESTSession, { newContentType }: { newContentType: ValidContentTypes | null } ) { + // TODO: Cleaner implementation // TODO: persist body evenafter switching content typees if (curr.request.body.contentType !== "multipart/form-data") { if (newContentType === "multipart/form-data") { + // Preserve entries when comping from urlencoded to multipart + if ( + curr.request.body.contentType === "application/x-www-form-urlencoded" + ) { + return { + ...curr.request, + body: { + contentType: "multipart/form-data", + body: pipe( + curr.request.body.body, + parseRawKeyValueEntries, + A.map( + ({ key, value, active }) => + { key, value, active, isFile: false } + ) + ), + }, + } + } + // Going from non-formdata to form-data, discard contents and set empty array as body return { request: { @@ -232,6 +260,29 @@ const dispatchers = defineDispatchers({ } } } else if (newContentType !== "multipart/form-data") { + if (newContentType === "application/x-www-form-urlencoded") { + return { + request: { + ...curr.request, + body: { + contentType: newContentType, + body: pipe( + curr.request.body.body, + A.map( + ({ key, value, isFile, active }) => + { + key, + value: isFile ? "" : value, + active, + } + ), + rawKeyValueEntriesToString + ), + }, + }, + } + } + // Going from formdata to non-formdata, discard contents and set empty string return { request: {