Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add the ability to configure query params encoding for requests #4412

Merged
merged 12 commits into from
Oct 23, 2024
12 changes: 11 additions & 1 deletion packages/hoppscotch-common/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"turn_on": "Turn on",
"undo": "Undo",
"verify": "Verify",
"yes": "Yes"
"yes": "Yes",
"enable": "Enable",
"disable": "Disable"
},
"add": {
"new": "Add new",
Expand Down Expand Up @@ -714,12 +716,16 @@
"account_email_description": "Your primary email address.",
"account_name_description": "This is your display name.",
"additional": "Additional Settings",
"auto_encode_mode": "Auto",
"auto_encode_mode_tooltip": "Encode the parameters in the request only if some special characters are present",
"background": "Background",
"black_mode": "Black",
"choose_language": "Choose language",
"dark_mode": "Dark",
"delete_account": "Delete account",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"disable_encode_mode_tooltip": "Never encode the parameters in the request",
"enable_encode_mode_tooltip": "Always encode the parameters in the request",
"expand_navigation": "Expand navigation",
"experiments": "Experiments",
"experiments_notice": "This is a collection of experiments we're working on that might turn out to be useful, fun, both, or neither. They're not final and may not be stable, so if something overly weird happens, don't panic. Just turn the dang thing off. Jokes aside, ",
Expand All @@ -728,11 +734,15 @@
"extensions": "Browser extension",
"extensions_use_toggle": "Use the browser extension to send requests (if present)",
"follow": "Follow us",
"general": "General",
"general_description": " General settings used in the application",
"interceptor": "Interceptor",
"interceptor_description": "Middleware between application and APIs.",
"language": "Language",
"light_mode": "Light",
"official_proxy_hosting": "Official Proxy is hosted by Hoppscotch.",
"query_parameters_encoding": "Query Parameters Encoding",
"query_parameters_encoding_description": "Configure encoding for query parameters in requests",
"profile": "Profile",
"profile_description": "Update your profile details",
"profile_email": "Email address",
Expand Down
1 change: 1 addition & 0 deletions packages/hoppscotch-common/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ declare module 'vue' {
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
SmartEncodingPicker: typeof import('./components/smart/EncodingPicker.vue')['default']
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
Expand Down
60 changes: 60 additions & 0 deletions packages/hoppscotch-common/src/components/smart/EncodingPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<div class="flex flex-col">
<div
v-for="(mode, index) of modes"
:key="`mode-${index}`"
class="flex w-fit"
>
<HoppSmartRadio
v-tippy="{ theme: 'tooltip', maxWidth: 500 }"
:value="mode"
:label="t(getEncodingModeName(mode))"
:title="t(getEncodingModeTooltip(mode))"
:selected="mode === activeMode"
:class="'!px-0 hover:bg-transparent'"
@change="changeMode(mode)"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { EncodeModes, EncodeMode, applySetting } from "~/newstore/settings"
import { useSetting } from "@composables/settings"
import { useI18n } from "~/composables/i18n"

const t = useI18n()

const modes = EncodeModes
const activeMode = useSetting("ENCODE_MODE")

const changeMode = (mode: EncodeMode) => {
applySetting("ENCODE_MODE", mode)
}

const getEncodingModeName = (mode: string) => {
switch (mode) {
case "enable":
return "action.enable"
case "disable":
return "action.disable"
case "auto":
return "settings.auto_encode_mode"
default:
return "settings.encode_mode"
}
}

const getEncodingModeTooltip = (mode: string) => {
switch (mode) {
case "enable":
return "settings.enable_encode_mode_tooltip"
case "disable":
return "settings.disable_encode_mode_tooltip"
case "auto":
return "settings.auto_encode_mode_tooltip"
default:
return "settings.enable_encode_mode_tooltip"
}
}
</script>
6 changes: 6 additions & 0 deletions packages/hoppscotch-common/src/newstore/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const HoppAccentColors = [
"pink",
] as const

export const EncodeModes = ["enable", "disable", "auto"] as const

export type EncodeMode = (typeof EncodeModes)[number]

export type HoppAccentColor = (typeof HoppAccentColors)[number]

export type SettingsDef = {
Expand Down Expand Up @@ -59,6 +63,7 @@ export type SettingsDef = {
}
THEME_COLOR: HoppAccentColor
BG_COLOR: HoppBgColor
ENCODE_MODE: EncodeMode
TELEMETRY_ENABLED: boolean
EXPAND_NAVIGATION: boolean
SIDEBAR: boolean
Expand Down Expand Up @@ -107,6 +112,7 @@ export const getDefaultSettings = (): SettingsDef => ({
},
THEME_COLOR: "indigo",
BG_COLOR: "system",
ENCODE_MODE: "enable",
TELEMETRY_ENABLED: true,
EXPAND_NAVIGATION: false,
SIDEBAR: true,
Expand Down
68 changes: 47 additions & 21 deletions packages/hoppscotch-common/src/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,34 @@
<div class="md:grid md:grid-cols-3 md:gap-4">
<div class="p-8 md:col-span-1">
<h3 class="heading">
{{ t("settings.theme") }}
{{ t("settings.general") }}
</h3>
<p class="my-1 text-secondaryLight">
{{ t("settings.theme_description") }}
{{ t("settings.general_description") }}
</p>
</div>
<div class="space-y-8 p-8 md:col-span-2">
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.background") }}
{{ t("settings.language") }}
</h4>
<div class="my-1 text-secondaryLight">
{{ t(getColorModeName(colorMode.preference)) }}
<span v-if="colorMode.preference === 'system'">
({{ t(getColorModeName(colorMode.value)) }})
</span>
</div>
<div class="mt-4">
<SmartColorModePicker />
<SmartChangeLanguage />
</div>
</section>

<section>
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.accent_color") }}
{{ t("settings.query_parameters_encoding") }}
</h4>
<div class="my-1 text-secondaryLight">
{{ ACCENT_COLOR.charAt(0).toUpperCase() + ACCENT_COLOR.slice(1) }}
</div>
<div class="mt-4">
<SmartAccentModePicker />
{{ t("settings.query_parameters_encoding_description") }}
</div>
</section>
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.language") }}
</h4>
<div class="mt-4">
<SmartChangeLanguage />
<SmartEncodingPicker />
</div>
</section>

<section>
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.experiments") }}
Expand Down Expand Up @@ -96,6 +84,44 @@
</div>
</div>

<div class="md:grid md:grid-cols-3 md:gap-4">
<div class="p-8 md:col-span-1">
<h3 class="heading">
{{ t("settings.theme") }}
</h3>
<p class="my-1 text-secondaryLight">
{{ t("settings.theme_description") }}
</p>
</div>
<div class="space-y-8 p-8 md:col-span-2">
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.background") }}
</h4>
<div class="my-1 text-secondaryLight">
{{ t(getColorModeName(colorMode.preference)) }}
<span v-if="colorMode.preference === 'system'">
({{ t(getColorModeName(colorMode.value)) }})
</span>
</div>
<div class="mt-4">
<SmartColorModePicker />
</div>
</section>
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.accent_color") }}
</h4>
<div class="my-1 text-secondaryLight">
{{ ACCENT_COLOR.charAt(0).toUpperCase() + ACCENT_COLOR.slice(1) }}
</div>
<div class="mt-4">
<SmartAccentModePicker />
</div>
</section>
</div>
</div>

<div class="md:grid md:grid-cols-3 md:gap-4">
<div class="p-8 md:col-span-1">
<h3 class="heading">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
RequestRunResult,
} from "~/services/interceptor.service"
import { Service } from "dioc"
import { cloneDeep } from "lodash-es"
import * as E from "fp-ts/Either"
import { ref, watch } from "vue"
import { z } from "zod"
Expand All @@ -24,6 +23,7 @@ import { UIExtensionService } from "~/services/ui-extension.service"
import { x25519 } from "@noble/curves/ed25519"
import { base16 } from "@scure/base"
import { invokeAction } from "~/helpers/actions"
import { preProcessRequest } from "../helpers"

type KeyValuePair = {
key: string
Expand Down Expand Up @@ -98,33 +98,6 @@ type RunRequestResponse = {
// and the axios present in this package
type AxiosRequestConfig = Parameters<Interceptor["runRequest"]>[0]

export const preProcessRequest = (
req: AxiosRequestConfig
): AxiosRequestConfig => {
const reqClone = cloneDeep(req)

// If the parameters are URLSearchParams, inject them to URL instead
// This prevents issues of marshalling the URLSearchParams to the proxy
if (reqClone.params instanceof URLSearchParams) {
try {
const url = new URL(reqClone.url ?? "")

for (const [key, value] of reqClone.params.entries()) {
url.searchParams.append(key, value)
}

reqClone.url = url.toString()
} catch (e) {
// making this a non-empty block, so we can make the linter happy.
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
}

reqClone.params = {}
}

return reqClone
}

async function processBody(
axiosReq: AxiosRequestConfig
): Promise<BodyDef | null> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,17 @@ import {
RequestRunResult,
} from "../../../services/interceptor.service"
import axios, { AxiosRequestConfig, CancelToken } from "axios"
import { cloneDeep } from "lodash-es"

export const preProcessRequest = (
req: AxiosRequestConfig
): AxiosRequestConfig => {
const reqClone = cloneDeep(req)

// If the parameters are URLSearchParams, inject them to URL instead
// This prevents issues of marshalling the URLSearchParams to the proxy
if (reqClone.params instanceof URLSearchParams) {
try {
const url = new URL(reqClone.url ?? "")

for (const [key, value] of reqClone.params.entries()) {
url.searchParams.append(key, value)
}

reqClone.url = url.toString()
} catch (e) {
// making this a non-empty block, so we can make the linter happy.
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
}

reqClone.params = {}
}

return reqClone
}
import { preProcessRequest } from "./helpers"

async function runRequest(
req: AxiosRequestConfig,
cancelToken: CancelToken
): RequestRunResult["response"] {
const timeStart = Date.now()

const processedReq = preProcessRequest(req)

try {
const res = await axios({
...processedReq,
...req,
cancelToken,
responseType: "arraybuffer",
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
InterceptorError,
RequestRunResult,
} from "~/services/interceptor.service"
import { cloneDeep } from "lodash-es"
import { computed, readonly, ref } from "vue"
import { browserIsChrome, browserIsFirefox } from "~/helpers/utils/userAgent"
import SettingsExtension from "~/components/settings/Extension.vue"
import InterceptorsExtensionSubtitle from "~/components/interceptors/ExtensionSubtitle.vue"
import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue"
import { until } from "@vueuse/core"
import { preProcessRequest } from "./helpers"

export const defineSubscribableObject = <T extends object>(obj: T) => {
const proxyObject = {
Expand Down Expand Up @@ -55,31 +55,6 @@ export const cancelRunningExtensionRequest = () => {
window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRequest()
}

const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => {
const reqClone = cloneDeep(req)

// If the parameters are URLSearchParams, inject them to URL instead
// This prevents marshalling issues with structured cloning of URLSearchParams
if (reqClone.params instanceof URLSearchParams) {
try {
const url = new URL(reqClone.url ?? "")

for (const [key, value] of reqClone.params.entries()) {
url.searchParams.append(key, value)
}

reqClone.url = url.toString()
} catch (e) {
// making this a non-empty block, so we can make the linter happy.
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
}

reqClone.params = {}
}

return reqClone
}

export type ExtensionStatus = "available" | "unknown-origin" | "waiting"

/**
Expand Down
Loading