Compare commits

...

78 Commits

Author SHA1 Message Date
Andrew Bastin
6086ebd824 Merge pull request #2575 from codeday-labs/codeday/main 2022-10-29 18:02:46 -04:00
Jason Jock Nava Casareno
dd83f8ef24 Merge branch 'hoppscotch:main' into codeday/main 2022-08-22 12:54:54 -07:00
Jason Casareno
682200ce68 Rename file and undid unnecessary changes 2022-08-22 12:54:12 -07:00
Jason Casareno
18910e429c Added missing property to request 2022-08-15 16:59:16 -07:00
Jason Casareno
924d6a87d0 Made active parameter counter include existing 'my variables' 2022-08-12 15:49:42 -07:00
Jason Jock Nava Casareno
fc15a5a1e4 Merge branch 'hoppscotch:main' into codeday/main 2022-08-12 15:45:10 -07:00
Jason Casareno
477811c414 Removed console.log messages 2022-08-12 15:03:54 -07:00
Jason Casareno
631a16feb0 Added warning msg when variables detect infinite expansion (WIP) 2022-08-10 18:01:34 -07:00
Jason Casareno
d0f4080771 Bug Fix: Environment modal not displaying expand error warning message 2022-08-10 14:48:10 -07:00
Jason Casareno
0da75cb23d Sync with Main Repository 2022-08-10 10:29:05 -07:00
Jason Jock Nava Casareno
abba09ea80 Merge branch 'hoppscotch:main' into codeday/main 2022-08-08 17:28:25 -07:00
Jason Jock Nava Casareno
fb5967294b Merge branch 'hoppscotch:main' into codeday/main 2022-08-05 15:02:01 -07:00
Jason Casareno
21d8b8fb2e Added TODO Comments + File Deletion 2022-08-03 17:04:13 -07:00
isaiM6
7ce85fee81 Merge branch 'codeday/main' of https://github.com/codeday-labs/hoppscotch into codeday/main 2022-08-03 16:50:36 -07:00
isaiM6
10615ca1a1 merge commit 2022-08-03 16:49:31 -07:00
Adrian Tuschek
dd5c876e32 Merging my branch into codeday/main 2022-08-03 16:47:52 -07:00
isaiM6
c2002f0f27 fixed merge conflict 2022-08-03 16:37:33 -07:00
isaiM6
9cfba797f6 fixed return statement 2022-08-03 16:23:52 -07:00
isaiM6
d1e6ffda49 fixed return statement 2022-08-03 16:21:33 -07:00
Jason Casareno
4f71b163ea Minor changes 2022-08-03 16:18:15 -07:00
isaiM6
775bf9a9c3 Merged environment.ts and variables.ts 2022-08-03 16:01:19 -07:00
Jason Casareno
33ecea5d75 Separate query parameters and variables vue files 2022-08-03 14:54:10 -07:00
Jason Casareno
551dfd1e20 Re-added the draggable button component to the variables UI component 2022-08-01 18:02:32 -07:00
Jason Casareno
8663934075 Removed unecessary code 2022-08-01 17:18:56 -07:00
Jason Casareno
a73d64ddc1 Renamed 'parameter' into 'variable' for the variable UI component 2022-08-01 17:03:38 -07:00
isaiM6
99f119d262 simplified conditional statement 2022-08-01 16:24:46 -07:00
isaiM6
6a8a687616 merge 2022-08-01 16:21:38 -07:00
Adrian Tuschek
6a33083790 Fixed recursive variables bug 2022-08-01 16:12:35 -07:00
isaiM6
5c7c355d95 modified pane size 2022-08-01 16:09:35 -07:00
isaiM6
328fb1176d changed layout of parameters so the pane with the params is more visible 2022-08-01 15:57:43 -07:00
isaiM6
fdf0c95f9a merged main into my branch 2022-08-01 15:04:46 -07:00
Jason Casareno
9e90e703f7 Deleted unnecessary class 2022-08-01 15:03:12 -07:00
Jason Casareno
0a241663ac Deleted unnecessary class 2022-08-01 15:02:22 -07:00
isaiM6
d58fc42190 fixed compilation errors in my branch 2022-08-01 14:57:12 -07:00
Jason Casareno
7a9bcd0a5c Modified regex expression 2022-08-01 14:48:58 -07:00
Jason Casareno
e3482f66cc Merge codeday/jason => codeday/isai 2022-08-01 14:47:22 -07:00
Jason Casareno
186803a465 Merge codeday/jason => codeday/isai 2022-08-01 14:46:06 -07:00
isaiM6
bdfdb44743 forced commit 2022-08-01 14:42:48 -07:00
Jason Casareno
4f8b346024 Deleted unwanted file 2022-08-01 14:36:17 -07:00
Jason Casareno
ec1104396e Merge codeday/jason => codeday/adrian 2022-08-01 14:34:56 -07:00
isaiM6
630ab1f4f4 merge commit 2022-08-01 14:11:37 -07:00
isaiM6
cabc775f58 commit for merge 2022-08-01 14:08:29 -07:00
Jason Casareno
f515ac4f52 Renaming variables and parameters 2022-08-01 13:15:01 -07:00
Jason Casareno
11b8bb4571 Merge codeday/jason => codeday/main 2022-08-01 13:08:42 -07:00
Jason Casareno
7bf66d8339 Renamed file and refactored, added new TODO 2022-08-01 13:07:27 -07:00
Jason Casareno
8d81ff3dc2 Merge codeday/jason => codeday/main 2022-08-01 12:09:10 -07:00
Jason Casareno
5538b9a5b9 Bug fix, removed console.logs 2022-08-01 12:08:19 -07:00
Jason Casareno
7077fe4621 Merge codeday/jason => codeday/main 2022-08-01 11:46:20 -07:00
Jason Casareno
42144b724b Modified regex expression to pit path variables match cases 2022-08-01 11:44:24 -07:00
Jason Casareno
14183d8b91 Debugging effective final url (WIP) 2022-07-29 18:07:06 -07:00
Adrian Tuschek
f8e1d78824 Debugging effective fial url (Work in Progress) 2022-07-29 18:03:43 -07:00
Jason Casareno
1e8805ab4f Debugging effective final url (WIP) 2022-07-29 18:03:37 -07:00
isaiM6
a294a2804b added files to start parsing functionality of path variables 2022-07-29 18:01:24 -07:00
Adrian Tuschek
8507278c40 Debug branch merge 2022-07-29 16:20:07 -07:00
Jason Casareno
d22bae2c60 Fixing final endpoint url (WIP) 2022-07-29 16:06:20 -07:00
Adrian Tuschek
2fefa55dce Merge Jasons branch 2022-07-29 14:45:42 -07:00
isaiM6
e0787d7fca commiting change with default variables 2022-07-28 17:27:35 -07:00
Jason Casareno
c9c5df36ab Passing variables into input bar (WIP) 2022-07-28 17:24:27 -07:00
isaiM6
5768274ef1 forced commit 2022-07-28 16:01:22 -07:00
isaiM6
493594b5d7 force commit 2022-07-28 15:58:37 -07:00
Adrian Tuschek
56c96f952d Merging changes 2022-07-28 15:56:10 -07:00
Adrian Tuschek
2c06a66c0a Hoppscotch Update 2022-07-28 15:48:41 -07:00
Jason Jock Nava Casareno
1a26a0e986 Merge branch 'hoppscotch:main' into codeday/main 2022-07-28 15:42:32 -07:00
Jason Jock Nava Casareno
9f1ee724b4 Merge branch 'hoppscotch:main' into codeday/jason 2022-07-28 15:42:18 -07:00
Jason Jock Nava Casareno
d28679de15 Merge branch 'hoppscotch:main' into codeday/isai 2022-07-28 15:42:11 -07:00
Jason Casareno
c8f62c4f04 Created default value for HoppRESTRequest for testing 2022-07-27 17:48:29 -07:00
Jason Casareno
8aa066e2ab Created default value for HoppRESTRequest for testing 2022-07-27 17:47:37 -07:00
isaiM6
a38e6cd427 Merge branch 'hoppscotch:main' into codeday/isai 2022-07-25 14:19:20 -07:00
isaiM6
c3ba45f875 changes to variables.vue 2022-07-25 13:56:15 -07:00
isaiM6
9061511609 changes to variables.vue 2022-07-25 13:55:14 -07:00
Jason Jock Nava Casareno
443e095775 Merge branch 'hoppscotch:main' into codeday/main 2022-07-25 11:06:49 -07:00
Jason Jock Nava Casareno
09e6fb246a Merge branch 'hoppscotch:main' into codeday/jason 2022-07-25 10:27:36 -07:00
Jason Casareno
5413bc584a Added missing dispatcher and function 2022-07-22 12:37:47 -07:00
Jason Casareno
7006fa57e2 Small naming changes 2022-07-21 20:58:53 -07:00
isaiM6
1a629a1219 localy stored variable data 2022-07-21 17:25:25 -07:00
Jason Casareno
9b60dc5f2d Modified HoppRESTRequest data structure 2022-07-21 14:21:45 -07:00
Jason Casareno
21021a3cd9 Removed reference to 'bulk params' 2022-07-20 16:50:34 -07:00
Jason Casareno
fd5db6c8c9 Duplicated and disconnected parameter UI for reuse 2022-07-19 15:56:42 -07:00
12 changed files with 993 additions and 369 deletions

View File

@@ -1,363 +1,13 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.parameter_list") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/parameters"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
svg="edit"
:class="{ '!text-accent': bulkMode }"
@click.native="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
:disabled="bulkMode"
@click.native="addParam"
/>
</div>
</div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
<div v-else>
<draggable
v-model="workingParams"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<div
v-for="(param, index) in workingParams"
:key="`param-${param.id}-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
svg="grip-vertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingParams?.length - 1,
}"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
@change="
updateParam(index, {
id: param.id,
key: $event,
value: param.value,
active: param.active,
})
"
/>
<SmartEnvInput
v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateParam(index, {
id: param.id,
key: param.key,
value: $event,
active: param.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
param.hasOwnProperty('active')
? param.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:svg="
param.hasOwnProperty('active')
? param.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateParam(index, {
id: param.id,
key: param.key,
value: param.value,
active: param.hasOwnProperty('active')
? !param.active
: false,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="deleteParam(index)"
/>
</span>
</div>
</draggable>
<div
v-if="workingParams.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_files.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.parameters')}`"
/>
<span class="pb-4 text-center">{{ t("empty.parameters") }}</span>
<ButtonSecondary
:label="`${t('add.new')}`"
svg="plus"
filled
class="mb-4"
@click.native="addParam"
/>
</div>
</div>
<div>
<HttpQueryParams />
<br />
<HttpPathVariables />
</div>
</template>
<script setup lang="ts">
import { Ref, ref, watch } from "@nuxtjs/composition-api"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import * as E from "fp-ts/Either"
import {
HoppRESTParam,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
RawKeyValueEntry,
} from "@hoppscotch/data"
import isEqual from "lodash/isEqual"
import cloneDeep from "lodash/cloneDeep"
import draggable from "vuedraggable"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { useI18n, useToast, useStream } from "~/helpers/utils/composables"
import { restParams$, setRESTParams } from "~/newstore/RESTSession"
import { throwError } from "~/helpers/functional/error"
import { objRemoveKey } from "~/helpers/functional/object"
const t = useI18n()
const toast = useToast()
const idTicker = ref(0)
const bulkMode = ref(false)
const bulkParams = ref("")
const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(bulkEditor, bulkParams, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
},
linter,
completer: null,
environmentHighlights: true,
})
// The functional parameters list (the parameters actually applied to the session)
const params = useStream(restParams$, [], setRESTParams) as Ref<HoppRESTParam[]>
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<Array<HoppRESTParam & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Rule: Working Params always have last element is always an empty param
watch(workingParams, (paramsList) => {
if (paramsList.length > 0 && paramsList[paramsList.length - 1].key !== "") {
workingParams.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
// Sync logic between params and working/bulk params
watch(
params,
(newParamsList) => {
// Sync should overwrite working params
const filteredWorkingParams: HoppRESTParam[] = pipe(
workingParams.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkParams = pipe(
parseRawKeyValueEntriesE(bulkParams.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newParamsList, filteredWorkingParams)) {
workingParams.value = pipe(
newParamsList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newParamsList, filteredBulkParams)) {
bulkParams.value = rawKeyValueEntriesToString(newParamsList)
}
},
{ immediate: true }
)
watch(workingParams, (newWorkingParams) => {
const fixedParams = pipe(
newWorkingParams,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(params.value, fixedParams)) {
params.value = cloneDeep(fixedParams)
}
})
watch(bulkParams, (newBulkParams) => {
const filteredBulkParams = pipe(
parseRawKeyValueEntriesE(newBulkParams),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(params.value, filteredBulkParams)) {
params.value = filteredBulkParams
}
})
const addParam = () => {
workingParams.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateParam = (index: number, param: HoppRESTParam & { id: number }) => {
workingParams.value = workingParams.value.map((h, i) =>
i === index ? param : h
)
}
const deleteParam = (index: number) => {
const paramsBeforeDeletion = cloneDeep(workingParams.value)
if (
!(
paramsBeforeDeletion.length > 0 &&
index === paramsBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingParams.value = paramsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingParams.value = pipe(
workingParams.value,
A.deleteAt(index),
O.getOrElseW(() => throwError("Working Params Deletion Out of Bounds"))
)
}
const clearContent = () => {
// set params list to the initial state
workingParams.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkParams.value = ""
}
/**
* TODO: Code duplication between QueryParams and Variables
*/
</script>

View File

@@ -0,0 +1,271 @@
<template>
<div>
<div
v-if="envExpandError"
class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
>
{{ nestedVars }}
</div>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight"> My Variables </label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/#"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
@click.native="addVar"
/>
</div>
</div>
<div>
<draggable
v-model="workingVars"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<div
v-for="(variable, index) in workingVars"
:key="`vari-${variable.id}-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
svg="grip-vertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingVars?.length - 1,
}"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="variable.key"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
@change="
updateVar(index, {
id: variable.id,
key: $event,
value: variable.value,
})
"
/>
<SmartEnvInput
v-model="variable.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateVar(index, {
id: variable.id,
key: variable.key,
value: $event,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="deleteVar(index)"
/>
</span>
</div>
</draggable>
<div
v-if="workingVars.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_files.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.parameters')}`"
/>
<span class="pb-4 text-center">{{ emptyVars }}</span>
<ButtonSecondary
:label="`${t('add.new')}`"
svg="plus"
filled
class="mb-4"
@click.native="addVar"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, Ref, ref, watch } from "@nuxtjs/composition-api"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import { HoppRESTVar, parseMyVariablesString } from "@hoppscotch/data"
import draggable from "vuedraggable"
import cloneDeep from "lodash/cloneDeep"
import isEqual from "lodash/isEqual"
import * as E from "fp-ts/Either"
import { useI18n, useStream, useToast } from "~/helpers/utils/composables"
import { throwError } from "~/helpers/functional/error"
import { restVars$, setRESTVars } from "~/newstore/RESTSession"
import { objRemoveKey } from "~/helpers/functional/object"
const t = useI18n()
const toast = useToast()
const emptyVars: string = "Add a new variable"
const nestedVars: string = "nested variables greater than 10 levels"
const idTicker = ref(0)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
// The functional variables list (the variables actually applied to the session)
const vars = useStream(restVars$, [], setRESTVars) as Ref<HoppRESTVar[]>
// The UI representation of the variables list (has the empty end variable)
const workingVars = ref<Array<HoppRESTVar & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
},
])
// Rule: Working vars always have last element is always an empty var
watch(workingVars, (varsList) => {
if (varsList.length > 0 && varsList[varsList.length - 1].key !== "") {
workingVars.value.push({
id: idTicker.value++,
key: "",
value: "",
})
}
})
// Sync logic between params and working/bulk params
watch(
vars,
(newVarsList) => {
// Sync should overwrite working params
const filteredWorkingVars: HoppRESTVar[] = pipe(
workingVars.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(newVarsList, filteredWorkingVars)) {
workingVars.value = pipe(
newVarsList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
},
{ immediate: true }
)
watch(workingVars, (newWorkingVars) => {
const fixedVars = pipe(
newWorkingVars,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(vars.value, fixedVars)) {
vars.value = cloneDeep(fixedVars)
}
})
const addVar = () => {
workingVars.value.push({
id: idTicker.value++,
key: "",
value: "",
})
}
const updateVar = (index: number, vari: HoppRESTVar & { id: number }) => {
workingVars.value = workingVars.value.map((h, i) => (i === index ? vari : h))
}
const deleteVar = (index: number) => {
const varsBeforeDeletion = cloneDeep(workingVars.value)
if (
!(varsBeforeDeletion.length > 0 && index === varsBeforeDeletion.length - 1)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingVars.value = varsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingVars.value = pipe(
workingVars.value,
A.deleteAt(index),
O.getOrElseW(() => throwError("Working Params Deletion Out of Bounds"))
)
}
const envExpandError = computed(() => {
const variables = pipe(vars.value)
return pipe(
variables,
A.exists(({ value }) => E.isLeft(parseMyVariablesString(value, variables)))
)
})
const clearContent = () => {
// set params list to the initial state
workingVars.value = [
{
id: idTicker.value++,
key: "",
value: "",
},
]
}
</script>

View File

@@ -0,0 +1,363 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.parameter_list") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/parameters"
blank
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
svg="trash-2"
@click.native="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
svg="edit"
:class="{ '!text-accent': bulkMode }"
@click.native="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
svg="plus"
:disabled="bulkMode"
@click.native="addParam"
/>
</div>
</div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
<div v-else>
<draggable
v-model="workingParams"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<div
v-for="(param, index) in workingParams"
:key="`param-${param.id}-${index}`"
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
svg="grip-vertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingParams?.length - 1,
}"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
@change="
updateParam(index, {
id: param.id,
key: $event,
value: param.value,
active: param.active,
})
"
/>
<SmartEnvInput
v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateParam(index, {
id: param.id,
key: param.key,
value: $event,
active: param.active,
})
"
/>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
param.hasOwnProperty('active')
? param.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:svg="
param.hasOwnProperty('active')
? param.active
? 'check-circle'
: 'circle'
: 'check-circle'
"
color="green"
@click.native="
updateParam(index, {
id: param.id,
key: param.key,
value: param.value,
active: param.hasOwnProperty('active')
? !param.active
: false,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
svg="trash"
color="red"
@click.native="deleteParam(index)"
/>
</span>
</div>
</draggable>
<div
v-if="workingParams.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${$colorMode.value}/add_files.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.parameters')}`"
/>
<span class="pb-4 text-center">{{ t("empty.parameters") }}</span>
<ButtonSecondary
:label="`${t('add.new')}`"
svg="plus"
filled
class="mb-4"
@click.native="addParam"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Ref, ref, watch } from "@nuxtjs/composition-api"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import * as E from "fp-ts/Either"
import {
HoppRESTParam,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
RawKeyValueEntry,
} from "@hoppscotch/data"
import isEqual from "lodash/isEqual"
import cloneDeep from "lodash/cloneDeep"
import draggable from "vuedraggable"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { useI18n, useToast, useStream } from "~/helpers/utils/composables"
import { restParams$, setRESTParams } from "~/newstore/RESTSession"
import { throwError } from "~/helpers/functional/error"
import { objRemoveKey } from "~/helpers/functional/object"
const t = useI18n()
const toast = useToast()
const idTicker = ref(0)
const bulkMode = ref(false)
const bulkParams = ref("")
const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(bulkEditor, bulkParams, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
},
linter,
completer: null,
environmentHighlights: true,
})
// The functional parameters list (the parameters actually applied to the session)
const params = useStream(restParams$, [], setRESTParams) as Ref<HoppRESTParam[]>
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref<Array<HoppRESTParam & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Rule: Working Params always have last element is always an empty param
watch(workingParams, (paramsList) => {
if (paramsList.length > 0 && paramsList[paramsList.length - 1].key !== "") {
workingParams.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
// Sync logic between params and working/bulk params
watch(
params,
(newParamsList) => {
// Sync should overwrite working params
const filteredWorkingParams: HoppRESTParam[] = pipe(
workingParams.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkParams = pipe(
parseRawKeyValueEntriesE(bulkParams.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newParamsList, filteredWorkingParams)) {
workingParams.value = pipe(
newParamsList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newParamsList, filteredBulkParams)) {
bulkParams.value = rawKeyValueEntriesToString(newParamsList)
}
},
{ immediate: true }
)
watch(workingParams, (newWorkingParams) => {
const fixedParams = pipe(
newWorkingParams,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(params.value, fixedParams)) {
params.value = cloneDeep(fixedParams)
}
})
watch(bulkParams, (newBulkParams) => {
const filteredBulkParams = pipe(
parseRawKeyValueEntriesE(newBulkParams),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(params.value, filteredBulkParams)) {
params.value = filteredBulkParams
}
})
const addParam = () => {
workingParams.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateParam = (index: number, param: HoppRESTParam & { id: number }) => {
workingParams.value = workingParams.value.map((h, i) =>
i === index ? param : h
)
}
const deleteParam = (index: number) => {
const paramsBeforeDeletion = cloneDeep(workingParams.value)
if (
!(
paramsBeforeDeletion.length > 0 &&
index === paramsBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingParams.value = paramsBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingParams.value = pipe(
workingParams.value,
A.deleteAt(index),
O.getOrElseW(() => throwError("Working Params Deletion Out of Bounds"))
)
}
const clearContent = () => {
// set params list to the initial state
workingParams.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkParams.value = ""
}
</script>

View File

@@ -348,7 +348,8 @@ const newSendRequest = async () => {
const ensureMethodInEndpoint = () => {
if (
!/^http[s]?:\/\//.test(newEndpoint.value) &&
!newEndpoint.value.startsWith("<<")
!newEndpoint.value.startsWith("<<") &&
!newEndpoint.value.startsWith("{{")
) {
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {

View File

@@ -7,7 +7,7 @@
<SmartTab
:id="'params'"
:label="`${$t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`"
:info="`${Number(newActiveParamsCount$) + Number(newActiveVarsCount$)}`"
>
<HttpParameters />
</SmartTab>
@@ -50,6 +50,7 @@ import { useReadonlyStream } from "~/helpers/utils/composables"
import {
restActiveHeadersCount$,
restActiveParamsCount$,
restActiveVarsCount$,
usePreRequestScript,
useTestScript,
} from "~/newstore/RESTSession"
@@ -76,6 +77,16 @@ const newActiveParamsCount$ = useReadonlyStream(
null
)
const newActiveVarsCount$ = useReadonlyStream(
restActiveVarsCount$.pipe(
map((e) => {
if (e === 0) return null
return `${e}`
})
),
null
)
const newActiveHeadersCount$ = useReadonlyStream(
restActiveHeadersCount$.pipe(
map((e) => {

View File

@@ -35,10 +35,13 @@ import { EditorState, Extension } from "@codemirror/state"
import clone from "lodash/clone"
import { tooltips } from "@codemirror/tooltip"
import { history, historyKeymap } from "@codemirror/history"
import { HoppRESTVar } from "@hoppscotch/data"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { HoppReactiveVarPlugin } from "~/helpers/editor/extensions/HoppVariable"
import { restVars$ } from "~/newstore/RESTSession"
const props = withDefaults(
defineProps<{
@@ -46,6 +49,7 @@ const props = withDefaults(
placeholder: string
styles: string
envs: { key: string; value: string; source: string }[] | null
vars: { key: string; value: string }[] | null
focus: boolean
readonly: boolean
}>(),
@@ -54,6 +58,7 @@ const props = withDefaults(
placeholder: "",
styles: "",
envs: null,
vars: null,
focus: false,
readonly: false,
}
@@ -109,6 +114,7 @@ let pastedValue: string | null = null
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
AggregateEnvironment[]
>
const aggregateVars = useReadonlyStream(restVars$, []) as Ref<HoppRESTVar[]>
const envVars = computed(() =>
props.envs
@@ -120,7 +126,17 @@ const envVars = computed(() =>
: aggregateEnvs.value
)
const varVars = computed(() =>
props.vars
? props.vars.map((x) => ({
key: x.key,
value: x.value,
}))
: aggregateVars.value
)
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
const varTooltipPlugin = new HoppReactiveVarPlugin(varVars, view)
const initView = (el: any) => {
const extensions: Extension = [
@@ -146,6 +162,7 @@ const initView = (el: any) => {
position: "absolute",
}),
envTooltipPlugin,
varTooltipPlugin,
placeholderExt(props.placeholder),
EditorView.domEventHandlers({
paste(ev) {

View File

@@ -0,0 +1,149 @@
import { watch, Ref } from "@nuxtjs/composition-api"
import { Compartment } from "@codemirror/state"
import { hoverTooltip } from "@codemirror/tooltip"
import {
Decoration,
EditorView,
MatchDecorator,
ViewPlugin,
} from "@codemirror/view"
import * as E from "fp-ts/Either"
import { HoppRESTVar, parseTemplateStringE } from "@hoppscotch/data"
const HOPP_ENVIRONMENT_REGEX = /({{\w+}})/g
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
const HOPP_ENV_HIGHLIGHT_FOUND =
"bg-accentDark text-accentContrast hover:bg-accent"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND =
"bg-red-500 text-accentContrast hover:bg-red-600"
const cursorTooltipField = (aggregateEnvs: HoppRESTVar[]) =>
hoverTooltip(
(view, pos, side) => {
const { from, to, text } = view.state.doc.lineAt(pos)
// TODO: When Codemirror 6 allows this to work (not make the
// popups appear half of the time) use this implementation
// const wordSelection = view.state.wordAt(pos)
// if (!wordSelection) return null
// const word = view.state.doc.sliceString(
// wordSelection.from - 2,
// wordSelection.to + 2
// )
// if (!HOPP_ENVIRONMENT_REGEX.test(word)) return null
// Tracking the start and the end of the words
let start = pos
let end = pos
while (start > from && /\w/.test(text[start - from - 1])) start--
while (end < to && /\w/.test(text[end - from])) end++
if (
(start === pos && side < 0) ||
(end === pos && side > 0) ||
!HOPP_ENVIRONMENT_REGEX.test(
text.slice(start - from - 2, end - from + 2)
)
)
return null
const envValue =
aggregateEnvs.find(
(env) => env.key === text.slice(start - from, end - from)
// env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
)?.value ?? "not found"
const result = parseTemplateStringE(envValue, aggregateEnvs)
const finalEnv = E.isLeft(result) ? "error" : result.right
return {
pos: start,
end: to,
above: true,
arrow: true,
create() {
const dom = document.createElement("span")
const xmp = document.createElement("xmp")
xmp.textContent = finalEnv
dom.appendChild(xmp)
dom.className = "tooltip-theme"
return { dom }
},
}
},
// HACK: This is a hack to fix hover tooltip not coming half of the time
// https://github.com/codemirror/tooltip/blob/765c463fc1d5afcc3ec93cee47d72606bed27e1d/src/tooltip.ts#L622
// Still doesn't fix the not showing up some of the time issue, but this is atleast more consistent
{ hoverTime: 1 } as any
)
function checkEnv(env: string, aggregateEnvs: HoppRESTVar[]) {
const className = aggregateEnvs.find(
(k: { key: string }) => k.key === env.slice(2, -2)
)
? HOPP_ENV_HIGHLIGHT_FOUND
: HOPP_ENV_HIGHLIGHT_NOT_FOUND
return Decoration.mark({
class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
})
}
const getMatchDecorator = (aggregateEnvs: HoppRESTVar[]) =>
new MatchDecorator({
regexp: HOPP_ENVIRONMENT_REGEX,
decoration: (m) => checkEnv(m[0], aggregateEnvs),
})
export const environmentHighlightStyle = (aggregateEnvs: HoppRESTVar[]) => {
const decorator = getMatchDecorator(aggregateEnvs)
return ViewPlugin.define(
(view) => ({
decorations: decorator.createDeco(view),
update(u) {
this.decorations = decorator.updateDeco(u, this.decorations)
},
}),
{
decorations: (v) => v.decorations,
}
)
}
export class HoppReactiveVarPlugin {
private compartment = new Compartment()
private envs: HoppRESTVar[] = []
constructor(
envsRef: Ref<HoppRESTVar[]>,
private editorView: Ref<EditorView | undefined>
) {
watch(
envsRef,
(envs) => {
this.envs = envs
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
},
{ immediate: true }
)
}
get extension() {
return this.compartment.of([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
])
}
}

View File

@@ -34,6 +34,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalHeaders: { key: string; value: string }[]
effectiveFinalParams: { key: string; value: string }[]
effectiveFinalBody: FormData | string | null
effectiveFinalVars: { key: string; value: string }[]
}
/**
@@ -318,15 +319,21 @@ export function getEffectiveRESTRequest(
value: parseTemplateString(x.value, envVariables),
}))
)
const effectiveFinalVars = request.vars
const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
return {
...request,
effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
effectiveFinalURL: parseTemplateString(
request.endpoint,
envVariables,
request.vars
),
effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
effectiveFinalVars,
}
}

View File

@@ -4,6 +4,7 @@ import {
FormDataKeyValue,
HoppRESTHeader,
HoppRESTParam,
HoppRESTVar,
HoppRESTReqBody,
HoppRESTRequest,
RESTReqSchemaVersion,
@@ -29,6 +30,7 @@ export const getDefaultRESTRequest = (): HoppRESTRequest => ({
endpoint: "https://echo.hoppscotch.io",
name: "Untitled request",
params: [],
vars: [],
headers: [],
method: "GET",
auth: {
@@ -80,6 +82,14 @@ const dispatchers = defineDispatchers({
},
}
},
setVars(curr: RESTSession, { entries }: { entries: HoppRESTVar[] }) {
return {
request: {
...curr.request,
vars: entries,
},
}
},
addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
return {
request: {
@@ -88,6 +98,14 @@ const dispatchers = defineDispatchers({
},
}
},
addVar(curr: RESTSession, { newVar }: { newVar: HoppRESTVar }) {
return {
request: {
...curr.request,
vars: [...curr.request.vars, newVar],
},
}
},
updateParam(
curr: RESTSession,
{ index, updatedParam }: { index: number; updatedParam: HoppRESTParam }
@@ -104,6 +122,22 @@ const dispatchers = defineDispatchers({
},
}
},
updateVar(
curr: RESTSession,
{ index, updatedVar }: { index: number; updatedVar: HoppRESTVar }
) {
const newVars = curr.request.vars.map((vari, i) => {
if (i === index) return updatedVar
else return vari
})
return {
request: {
...curr.request,
vars: newVars,
},
}
},
deleteParam(curr: RESTSession, { index }: { index: number }) {
const newParams = curr.request.params.filter((_x, i) => i !== index)
@@ -114,6 +148,16 @@ const dispatchers = defineDispatchers({
},
}
},
deleteVar(curr: RESTSession, { index }: { index: number }) {
const newVars = curr.request.vars.filter((_x, i) => i !== index)
return {
request: {
...curr.request,
vars: newVars,
},
}
},
deleteAllParams(curr: RESTSession) {
return {
request: {
@@ -373,6 +417,14 @@ export function setRESTParams(entries: HoppRESTParam[]) {
},
})
}
export function setRESTVars(entries: HoppRESTVar[]) {
restSessionStore.dispatch({
dispatcher: "setVars",
payload: {
entries,
},
})
}
export function addRESTParam(newParam: HoppRESTParam) {
restSessionStore.dispatch({
@@ -382,6 +434,14 @@ export function addRESTParam(newParam: HoppRESTParam) {
},
})
}
export function addRESTVar(newVar: HoppRESTVar) {
restSessionStore.dispatch({
dispatcher: "addVar",
payload: {
newVar,
},
})
}
export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
restSessionStore.dispatch({
@@ -392,6 +452,15 @@ export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
},
})
}
export function updateRESTVar(index: number, updatedVar: HoppRESTVar) {
restSessionStore.dispatch({
dispatcher: "updateVar",
payload: {
updatedVar,
index,
},
})
}
export function deleteRESTParam(index: number) {
restSessionStore.dispatch({
@@ -402,6 +471,15 @@ export function deleteRESTParam(index: number) {
})
}
export function deleteRESTVar(index: number) {
restSessionStore.dispatch({
dispatcher: "deleteVar",
payload: {
index,
},
})
}
export function deleteAllRESTParams() {
restSessionStore.dispatch({
dispatcher: "deleteAllParams",
@@ -592,12 +670,20 @@ export const restParams$ = restSessionStore.subject$.pipe(
distinctUntilChanged()
)
export const restVars$ = restSessionStore.subject$.pipe(
pluck("request", "vars"),
distinctUntilChanged()
)
export const restActiveParamsCount$ = restParams$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
)
)
export const restActiveVarsCount$ = restVars$.pipe(
map((vars) => vars.filter((x) => x.key !== "" || x.value !== "").length)
)
export const restMethod$ = restSessionStore.subject$.pipe(
pluck("request", "method"),

View File

@@ -315,6 +315,7 @@ completedRESTResponse$.subscribe((res) => {
method: res.req.method,
name: res.req.name,
params: res.req.params,
vars: res.req.vars,
preRequestScript: res.req.preRequestScript,
testScript: res.req.testScript,
v: res.req.v,

View File

@@ -9,7 +9,13 @@ export type Environment = {
}[]
}
export type Variables = {
key: string
value: string
}[]
const REGEX_ENV_VAR = /<<([^>]*)>>/g // "<<myVariable>>"
const REGEX_MY_VAR = /{{([^}]*)}}/g // "{{myVariable}}"
/**
* How much times can we expand environment variables
@@ -59,26 +65,37 @@ export const parseBodyEnvVariables = (
export function parseTemplateStringE(
str: string,
variables: Environment["variables"]
variables: Environment["variables"],
myVariables?: Variables | undefined
) {
if (!variables || !str) {
return E.right(str)
}
let result = str
let depth = 0
let depthEnv = 0
let depthVar = 0
while (result.match(REGEX_ENV_VAR) != null && depth <= ENV_MAX_EXPAND_LIMIT) {
while (result.match(REGEX_ENV_VAR) != null && depthEnv <= ENV_MAX_EXPAND_LIMIT) {
result = decodeURI(encodeURI(result)).replace(
REGEX_ENV_VAR,
(_, p1) => variables.find((x) => x.key === p1)?.value || ""
)
depth++
depthEnv++
}
return depth > ENV_MAX_EXPAND_LIMIT
? E.left(ENV_EXPAND_LOOP)
: E.right(result)
if (myVariables) {
while (result.match(REGEX_MY_VAR) != null && depthVar <= ENV_MAX_EXPAND_LIMIT) {
result = decodeURI(encodeURI(result)).replace(
REGEX_MY_VAR,
(_, p1) => myVariables.find((x) => x.key === p1)?.value || ""
)
depthVar++
}
}
return depthEnv <= ENV_MAX_EXPAND_LIMIT && depthVar <= ENV_MAX_EXPAND_LIMIT ? E.right(result) : E.left(ENV_EXPAND_LOOP);
}
/**
@@ -86,9 +103,33 @@ export function parseTemplateStringE(
*/
export const parseTemplateString = (
str: string,
variables: Environment["variables"]
variables: Environment["variables"],
myVariables?: Variables
) =>
pipe(
parseTemplateStringE(str, variables),
parseTemplateStringE(str, variables, myVariables),
E.getOrElse(() => str)
)
export function parseMyVariablesString(
str: string,
variables: Variables,
) {
if (!variables || !str) {
return E.right(str)
}
let result = str
let depthVar = 0
while (result.match(REGEX_MY_VAR) != null && depthVar <= ENV_MAX_EXPAND_LIMIT) {
result = decodeURI(encodeURI(result)).replace(
REGEX_MY_VAR,
(_, p1) => variables.find((x) => x.key === p1)?.value || ""
)
depthVar++
}
return depthVar <= ENV_MAX_EXPAND_LIMIT ? E.right(result) : E.left(ENV_EXPAND_LOOP);
}

View File

@@ -16,6 +16,11 @@ export type HoppRESTParam = {
active: boolean
}
export type HoppRESTVar = {
key: string
value: string
}
export type HoppRESTHeader = {
key: string
value: string
@@ -51,6 +56,7 @@ export interface HoppRESTRequest {
method: string
endpoint: string
params: HoppRESTParam[]
vars: HoppRESTVar[]
headers: HoppRESTHeader[]
preRequestScript: string
testScript: string
@@ -74,6 +80,10 @@ export const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
(arr) => arr.filter((p) => p.key !== "" && p.value !== ""),
lodashIsEqualEq
),
vars: mapThenEq(
(arr) => arr.filter((v) => v.key !== "" && v.value !== ""),
lodashIsEqualEq
),
method: S.Eq,
name: S.Eq,
preRequestScript: S.Eq,
@@ -126,6 +136,9 @@ export function safelyExtractRESTRequest(
if (x.hasOwnProperty("params") && Array.isArray(x.params))
req.params = x.params // TODO: Deep nested checks
if (x.hasOwnProperty("vars") && Array.isArray(x.vars))
req.vars = x.vars // TODO: Deep nested checks
if (x.hasOwnProperty("headers") && Array.isArray(x.headers))
req.headers = x.headers // TODO: Deep nested checks
}
@@ -186,6 +199,19 @@ export function translateToNewRequest(x: any): HoppRESTRequest {
})
)
const vars: HoppRESTVar[] = (x?.vars ?? []).map(
({
key,
value,
}: {
key: string
value: string
}) => ({
key,
value,
})
)
const name = x?.name ?? "Untitled request"
const method = x?.method ?? ""
@@ -201,6 +227,7 @@ export function translateToNewRequest(x: any): HoppRESTRequest {
endpoint,
headers,
params,
vars,
method,
preRequestScript,
testScript,