Compare commits

...

96 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
Nivedin
052595c076 fix: form data with same key send only last one (#2606)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-08-22 23:02:02 +05:30
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
Andrew Bastin
6b8ae63747 fix: wrong pick emission on save request modal for teams requests (fixes #2579) 2022-08-12 14:00:47 +05:30
Andrew Bastin
c013aa52ac feat: allow quoted key/values for escaping characters and trail/lead whitespaces in raw key value pairs (#2578) 2022-08-12 13:53:40 +05:30
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
Anwarul Islam
017cbb5a71 feat: update keyboard shortcut to navigate to profile page (#2573) 2022-08-10 17:21:43 +05:30
Sagar
2e1ca0cbb0 feat: remember pane sizes (#2556)
Co-authored-by: Sagar <sagar@Sagars-MacBook-Pro.local>
2022-08-10 05:11:03 +05:30
Jason Jock Nava Casareno
abba09ea80 Merge branch 'hoppscotch:main' into codeday/main 2022-08-08 17:28:25 -07:00
Anwarul Islam
a9e1a3002e Hightlight environment variable with a dash '-' in its name (#2560) 2022-08-08 17:23:16 +05:30
Jason Jock Nava Casareno
fb5967294b Merge branch 'hoppscotch:main' into codeday/main 2022-08-05 15:02:01 -07:00
Deepanshu Dhruw
73fdfbd2c8 feat: added delay flag in @hoppscotch/cli and related tests (#2527)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-08-04 19:19:14 +05:30
Patrick Prakash
0c31d9201f docs: fix PWA broken link (#2558) 2022-08-04 18:41:19 +05:30
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
kyteinsky
fa0e7f4785 fix: curl parser x-www-form-urlencoded body parsing (#2528) 2022-07-28 21:03:05 +05:30
Andrew Bastin
e9576dd339 fix: ignore confirm save modal on same request selection even when no session 2022-07-28 17:41:54 +05:30
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
Khusroo Hayat
d335ac1d80 fix: search panel position in response (#2510)
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
2022-07-25 14:28:02 +05:30
Joel Jacob Stephen
c0e3a2be0b fix: disabled search in team collection (#2523)
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
2022-07-25 14:01:26 +05:30
SiderealArt
722864da62 update tw.json (#2511) 2022-07-25 13:37:52 +05:30
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
Akash K
54a12ef6fa fix: team collections tab visible when logging out (#2494) 2022-07-06 22:24:32 +05:30
liyasthomas
d035262e1a refactor: lowercase routes 2022-07-03 18:04:57 +05:30
Andrew Bastin
1ab54b0ce7 fix: i18n breaking on switching between realtime tabs 2022-07-02 21:39:43 +05:30
Andrew Bastin
cac3abd2ab fix: multiple requests appearing on teams (#2455)
Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com>
2022-06-30 18:37:27 +05:30
Nivedin
c34185dc4b fix: environment variables save without pressing 'save' button (#2454)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2022-06-30 14:42:43 +05:30
58 changed files with 1967 additions and 569 deletions

View File

@@ -84,7 +84,7 @@
_Customized themes are synced with cloud / local session_
🔥 **PWA:** Install as a [PWA](https://developers.google.com/web/progressive-web-apps) on your device.
🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device.
- Instant loading with Service Workers
- Offline support

View File

@@ -6,21 +6,26 @@
'!flex-row-reverse': SIDEBAR_ON_LEFT && mdAndLarger,
}"
:horizontal="!mdAndLarger"
@resize="setPaneEvent($event, 'vertical')"
>
<Pane
size="75"
:size="PANE_MAIN_SIZE"
min-size="65"
class="hide-scrollbar !overflow-auto flex flex-col"
>
<Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
<Splitpanes
class="smart-splitter"
:horizontal="COLUMN_LAYOUT"
@resize="setPaneEvent($event, 'horizontal')"
>
<Pane
:size="COLUMN_LAYOUT ? 45 : 50"
:size="PANE_MAIN_TOP_SIZE"
class="hide-scrollbar !overflow-auto flex flex-col"
>
<slot name="primary" />
</Pane>
<Pane
:size="COLUMN_LAYOUT ? 65 : 50"
:size="PANE_MAIN_BOTTOM_SIZE"
class="flex flex-col hide-scrollbar !overflow-auto"
>
<slot name="secondary" />
@@ -29,7 +34,7 @@
</Pane>
<Pane
v-if="SIDEBAR && hasSidebar"
size="25"
:size="PANE_SIDEBAR_SIZE"
min-size="20"
class="hide-scrollbar !overflow-auto flex flex-col"
>
@@ -42,8 +47,9 @@
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { computed, useSlots } from "@nuxtjs/composition-api"
import { computed, useSlots, ref } from "@nuxtjs/composition-api"
import { useSetting } from "~/newstore/settings"
import { setLocalConfig, getLocalConfig } from "~/newstore/localpersistence"
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
@@ -57,4 +63,60 @@ const SIDEBAR = useSetting("SIDEBAR")
const slots = useSlots()
const hasSidebar = computed(() => !!slots.sidebar)
const props = defineProps({
layoutId: {
type: String,
default: null,
},
})
type PaneEvent = {
max: number
min: number
size: number
}
const PANE_SIDEBAR_SIZE = ref(25)
const PANE_MAIN_SIZE = ref(75)
const PANE_MAIN_TOP_SIZE = ref(45)
const PANE_MAIN_BOTTOM_SIZE = ref(65)
if (!COLUMN_LAYOUT.value) {
PANE_MAIN_TOP_SIZE.value = 50
PANE_MAIN_BOTTOM_SIZE.value = 50
}
function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") {
if (!props.layoutId) return
const storageKey = `${props.layoutId}-pane-config-${type}`
setLocalConfig(storageKey, JSON.stringify(event))
}
function populatePaneEvent() {
if (!props.layoutId) return
const verticalPaneData = getPaneData("vertical")
if (verticalPaneData) {
const [mainPane, sidebarPane] = verticalPaneData
PANE_MAIN_SIZE.value = mainPane?.size
PANE_SIDEBAR_SIZE.value = sidebarPane?.size
}
const horizontalPaneData = getPaneData("horizontal")
if (horizontalPaneData) {
const [mainTopPane, mainBottomPane] = horizontalPaneData
PANE_MAIN_TOP_SIZE.value = mainTopPane?.size
PANE_MAIN_BOTTOM_SIZE.value = mainBottomPane?.size
}
}
function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null {
const storageKey = `${props.layoutId}-pane-config-${type}`
const paneEvent = getLocalConfig(storageKey)
if (!paneEvent) return null
return JSON.parse(paneEvent)
}
populatePaneEvent()
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="show">
<div v-show="show">
<SmartTabs
:id="'collections_tab'"
v-model="selectedCollectionTab"

View File

@@ -11,6 +11,7 @@
autocomplete="off"
:placeholder="$t('action.search')"
class="py-2 pl-4 pr-2 bg-transparent"
:disabled="collectionsType.type == 'team-collections'"
/>
</div>
<CollectionsChooseType

View File

@@ -339,7 +339,14 @@ const selectRequest = () => {
confirmChange.value = false
setRestReq(props.request)
} else if (!active.value) {
confirmChange.value = true
// If the current request is the same as the request to be loaded in, there is no data loss
const currentReq = getRESTRequest()
if (isEqualHoppRESTRequest(currentReq, props.request)) {
setRestReq(props.request)
} else {
confirmChange.value = true
}
} else {
const currentReqWithNoChange = active.value.req
const currentFullReq = getRESTRequest()

View File

@@ -261,7 +261,7 @@ const active = useReadonlyStream(restSaveContext$, null)
const isSelected = computed(
() =>
props.picked &&
props.picked.pickedType === "teams-collection" &&
props.picked.pickedType === "teams-request" &&
props.picked.requestID === props.requestIndex
)
@@ -312,7 +312,7 @@ const selectRequest = () => {
if (props.saveRequest) {
emit("select", {
picked: {
pickedType: "teams-collection",
pickedType: "teams-request",
requestID: props.requestIndex,
},
})

View File

@@ -50,18 +50,18 @@
</div>
<div class="border rounded divide-y divide-dividerLight border-divider">
<div
v-for="(variable, index) in vars"
:key="`variable-${index}`"
v-for="({ id, env }, index) in vars"
:key="`variable-${id}-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
v-model="variable.key"
v-model="env.key"
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
:name="'param' + index"
/>
<SmartEnvInput
v-model="variable.value"
v-model="env.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:envs="liveEnvs"
:name="'value' + index"
@@ -119,6 +119,9 @@
import clone from "lodash/clone"
import { computed, ref, watch } from "@nuxtjs/composition-api"
import * as E from "fp-ts/Either"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { pipe, flow } from "fp-ts/function"
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import {
@@ -137,6 +140,14 @@ import {
useToast,
} from "~/helpers/utils/composables"
type EnvironmentVariable = {
id: number
env: {
key: string
value: string
}
}
const t = useI18n()
const toast = useToast()
@@ -159,8 +170,12 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const idTicker = ref(0)
const name = ref<string | null>(null)
const vars = ref([{ key: "", value: "" }])
const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } },
])
const clearIcon = refAutoReset<"trash-2" | "check">("trash-2", 1000)
@@ -187,15 +202,15 @@ const workingEnv = computed(() => {
const envList = useReadonlyStream(environments$, []) || props.envVars()
const evnExpandError = computed(() => {
for (const variable of vars.value) {
const result = parseTemplateStringE(variable.value.toString(), vars.value)
const variables = pipe(
vars.value,
A.map((e) => e.env)
)
if (E.isLeft(result)) {
console.error("error", result.left)
return true
}
}
return false
return pipe(
variables,
A.exists(({ value }) => E.isLeft(parseTemplateStringE(value, variables)))
)
})
const liveEnvs = computed(() => {
@@ -218,21 +233,38 @@ watch(
(show) => {
if (show) {
name.value = workingEnv.value?.name ?? null
vars.value = clone(workingEnv.value?.variables ?? [])
vars.value = pipe(
workingEnv.value?.variables ?? [],
A.map((e) => ({
id: idTicker.value++,
env: clone(e),
}))
)
}
}
)
const clearContent = () => {
vars.value = []
vars.value = [
{
id: idTicker.value++,
env: {
key: "",
value: "",
},
},
]
clearIcon.value = "check"
toast.success(`${t("state.cleared")}`)
}
const addEnvironmentVariable = () => {
vars.value.push({
key: "",
value: "",
id: idTicker.value++,
env: {
key: "",
value: "",
},
})
}
@@ -246,9 +278,19 @@ const saveEnvironment = () => {
return
}
const filterdVariables = pipe(
vars.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.env.key !== ""),
O.map((e) => e.env)
)
)
)
const environmentUpdated: Environment = {
name: name.value,
variables: vars.value,
variables: filterdVariables,
}
if (props.action === "new") {

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

@@ -221,7 +221,7 @@ export const runGQLSubscription = <
createRequest(args.query, args.variables)
)
wonkaPipe(
const sub = wonkaPipe(
source,
subscribe((res) => {
result$.next(
@@ -256,7 +256,8 @@ export const runGQLSubscription = <
})
)
return result$
// Returns the stream and a subscription handle to unsub
return [result$, sub] as const
}
export const useGQLQuery = <DocType, DocVarType, DocErrorType extends string>(

View File

@@ -809,6 +809,37 @@ const samples = [
testScript: "",
}),
},
{
command: `curl https://example.com -d "alpha=beta&request_id=4"`,
response: makeRESTRequest({
method: "POST",
name: "Untitled request",
endpoint: "https://example.com/",
auth: {
authType: "none",
authActive: true,
},
body: {
contentType: "application/x-www-form-urlencoded",
body: rawKeyValueEntriesToString([
{
active: true,
key: "alpha",
value: "beta",
},
{
active: true,
key: "request_id",
value: "4",
},
]),
},
params: [],
headers: [],
preRequestScript: "",
testScript: "",
}),
},
]
describe("Parse curl command to Hopp REST Request", () => {

View File

@@ -93,7 +93,8 @@ export const parseCurlCommand = (curlCommand: string) => {
hasBodyBeenParsed = true
} else if (
rawContentType.includes("application/x-www-form-urlencoded") &&
!!pairs
!!pairs &&
Array.isArray(rawData)
) {
body = pairs.map((p) => p.join(": ")).join("\n") || null
contentType = "application/x-www-form-urlencoded"

View File

@@ -16,7 +16,7 @@ import {
getAggregateEnvs,
} from "~/newstore/environments"
const HOPP_ENVIRONMENT_REGEX = /(<<\w+>>)/g
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
@@ -44,8 +44,9 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
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++
while (start > from && /[a-zA-Z0-9-_]+/.test(text[start - from - 1]))
start--
while (end < to && /[a-zA-Z0-9-_]+/.test(text[end - from])) end++
if (
(start === pos && side < 0) ||

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

@@ -61,6 +61,8 @@ export const baseTheme = EditorView.theme({
},
".cm-panels.cm-panels-top": {
borderBottom: "1px solid var(--divider-light-color)",
top: "var(--lower-tertiary-sticky-fold) !important",
"z-index": "10",
},
".cm-panels.cm-panels-bottom": {
borderTop: "1px solid var(--divider-light-color)",
@@ -388,5 +390,7 @@ export const basicSetup: Extension = [
...completionKeymap,
...lintKeymap,
]),
search(),
search({
top: true,
}),
]

View File

@@ -1,3 +1,11 @@
/**
* Converts an array of key-value tuples (for e.g ["key", "value"]), into a record.
* (for eg. output -> { "key": "value" })
* NOTE: This function will discard duplicate key occurances and only keep the last occurance. If you do not want that behaviour,
* use `tupleWithSamesKeysToRecord`.
* @param tuples Array of tuples ([key, value])
* @returns A record with value corresponding to the last occurance of that key
*/
export const tupleToRecord = <
KeyType extends string | number | symbol,
ValueType
@@ -5,5 +13,32 @@ export const tupleToRecord = <
tuples: [KeyType, ValueType][]
): Record<KeyType, ValueType> =>
tuples.length > 0
? (Object.assign as any)(...tuples.map(([key, val]) => ({ [key]: val })))
? (Object.assign as any)(...tuples.map(([key, val]) => ({ [key]: val }))) // This is technically valid, but we have no way of telling TypeScript it is valid. Hence the assertion
: {}
/**
* Converts an array of key-value tuples (for e.g ["key", "value"]), into a record.
* (for eg. output -> { "key": ["value"] })
* NOTE: If you do not want the array as values (because of duplicate keys) and want to instead get the last occurance, use `tupleToRecord`
* @param tuples Array of tuples ([key, value])
* @returns A Record with values being arrays corresponding to each key occurance
*/
export const tupleWithSameKeysToRecord = <
KeyType extends string | number | symbol,
ValueType
>(
tuples: [KeyType, ValueType][]
): Record<KeyType, ValueType[]> => {
// By the end of the function we do ensure this typing, this can't be infered now though, hence the assertion
const out = {} as Record<KeyType, ValueType[]>
for (const [key, value] of tuples) {
if (!out[key]) {
out[key] = [value]
} else {
out[key].push(value)
}
}
return out
}

View File

@@ -56,6 +56,7 @@ export const bindings: {
"alt-q": "navigation.jump.graphql",
"alt-w": "navigation.jump.realtime",
"alt-d": "navigation.jump.documentation",
"alt-m": "navigation.jump.profile",
"alt-s": "navigation.jump.settings",
}

View File

@@ -1,5 +1,6 @@
import * as E from "fp-ts/Either"
import { BehaviorSubject, Subscription } from "rxjs"
import { Subscription as WSubscription } from "wonka"
import { GQLError, runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
import {
GetUserShortcodesQuery,
@@ -22,6 +23,9 @@ export default class ShortcodeListAdapter {
private myShortcodesCreated: Subscription | null
private myShortcodesRevoked: Subscription | null
private myShortcodesCreatedSub: WSubscription | null
private myShortcodesRevokedSub: WSubscription | null
constructor(deferInit: boolean = false) {
this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
this.loading$ = new BehaviorSubject<boolean>(false)
@@ -33,6 +37,8 @@ export default class ShortcodeListAdapter {
this.isDispose = false
this.myShortcodesCreated = null
this.myShortcodesRevoked = null
this.myShortcodesCreatedSub = null
this.myShortcodesRevokedSub = null
if (!deferInit) this.initialize()
}
@@ -40,6 +46,8 @@ export default class ShortcodeListAdapter {
unsubscribeSubscriptions() {
this.myShortcodesCreated?.unsubscribe()
this.myShortcodesRevoked?.unsubscribe()
this.myShortcodesCreatedSub?.unsubscribe()
this.myShortcodesRevokedSub?.unsubscribe()
}
initialize() {
@@ -124,9 +132,12 @@ export default class ShortcodeListAdapter {
}
private registerSubscriptions() {
this.myShortcodesCreated = runGQLSubscription({
const [myShortcodeCreated$, myShortcodeCreatedSub] = runGQLSubscription({
query: ShortcodeCreatedDocument,
}).subscribe((result) => {
})
this.myShortcodesCreatedSub = myShortcodeCreatedSub
this.myShortcodesCreated = myShortcodeCreated$.subscribe((result) => {
if (E.isLeft(result)) {
console.error(result.left)
throw new Error(`Shortcode Create Error ${result.left}`)
@@ -135,9 +146,12 @@ export default class ShortcodeListAdapter {
this.createShortcode(result.right.myShortcodesCreated)
})
this.myShortcodesRevoked = runGQLSubscription({
const [myShortcodesRevoked$, myShortcodeRevokedSub] = runGQLSubscription({
query: ShortcodeDeletedDocument,
}).subscribe((result) => {
})
this.myShortcodesRevokedSub = myShortcodeRevokedSub
this.myShortcodesRevoked = myShortcodesRevoked$.subscribe((result) => {
if (E.isLeft(result)) {
console.error(result.left)
throw new Error(`Shortcode Delete Error ${result.left}`)

View File

@@ -103,7 +103,7 @@ export default [
label: "shortcut.navigation.settings",
},
{
keys: [getPlatformAlternateKey(), "P"],
keys: [getPlatformAlternateKey(), "M"],
label: "shortcut.navigation.profile",
},
],
@@ -171,7 +171,7 @@ export const spotlight = [
icon: "arrow-right",
},
{
keys: [getPlatformAlternateKey(), "P"],
keys: [getPlatformAlternateKey(), "M"],
label: "shortcut.navigation.profile",
action: "navigation.jump.profile",
icon: "arrow-right",
@@ -267,7 +267,7 @@ export const fuse = [
tags: ["settings", "jump", "page", "navigation", "account", "theme", "go"],
},
{
keys: [getPlatformAlternateKey(), "P"],
keys: [getPlatformAlternateKey(), "M"],
label: "shortcut.navigation.profile",
action: "navigation.jump.profile",
icon: "arrow-right",

View File

@@ -3,6 +3,7 @@ import { BehaviorSubject, Subscription } from "rxjs"
import { translateToNewRequest } from "@hoppscotch/data"
import pull from "lodash/pull"
import remove from "lodash/remove"
import { Subscription as WSubscription } from "wonka"
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
import { TeamCollection } from "./TeamCollection"
import { TeamRequest } from "./TeamRequest"
@@ -193,6 +194,13 @@ export default class NewTeamCollectionAdapter {
private teamRequestUpdated$: Subscription | null
private teamRequestDeleted$: Subscription | null
private teamCollectionAddedSub: WSubscription | null
private teamCollectionUpdatedSub: WSubscription | null
private teamCollectionRemovedSub: WSubscription | null
private teamRequestAddedSub: WSubscription | null
private teamRequestUpdatedSub: WSubscription | null
private teamRequestDeletedSub: WSubscription | null
constructor(private teamID: string | null) {
this.collections$ = new BehaviorSubject<TeamCollection[]>([])
this.loadingCollections$ = new BehaviorSubject<string[]>([])
@@ -204,6 +212,13 @@ export default class NewTeamCollectionAdapter {
this.teamRequestDeleted$ = null
this.teamRequestUpdated$ = null
this.teamCollectionAddedSub = null
this.teamCollectionUpdatedSub = null
this.teamCollectionRemovedSub = null
this.teamRequestAddedSub = null
this.teamRequestDeletedSub = null
this.teamRequestUpdatedSub = null
if (this.teamID) this.initialize()
}
@@ -228,6 +243,13 @@ export default class NewTeamCollectionAdapter {
this.teamRequestAdded$?.unsubscribe()
this.teamRequestDeleted$?.unsubscribe()
this.teamRequestUpdated$?.unsubscribe()
this.teamCollectionAddedSub?.unsubscribe()
this.teamCollectionUpdatedSub?.unsubscribe()
this.teamCollectionRemovedSub?.unsubscribe()
this.teamRequestAddedSub?.unsubscribe()
this.teamRequestDeletedSub?.unsubscribe()
this.teamRequestUpdatedSub?.unsubscribe()
}
private async initialize() {
@@ -406,12 +428,16 @@ export default class NewTeamCollectionAdapter {
private registerSubscriptions() {
if (!this.teamID) return
this.teamCollectionAdded$ = runGQLSubscription({
const [teamCollAdded$, teamCollAddedSub] = runGQLSubscription({
query: TeamCollectionAddedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamCollectionAddedSub = teamCollAddedSub
this.teamCollectionAdded$ = teamCollAdded$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Collection Added Error: ${result.left}`)
@@ -426,12 +452,15 @@ export default class NewTeamCollectionAdapter {
)
})
this.teamCollectionUpdated$ = runGQLSubscription({
const [teamCollUpdated$, teamCollUpdatedSub] = runGQLSubscription({
query: TeamCollectionUpdatedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamCollectionUpdatedSub = teamCollUpdatedSub
this.teamCollectionUpdated$ = teamCollUpdated$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Collection Updated Error: ${result.left}`)
@@ -441,24 +470,30 @@ export default class NewTeamCollectionAdapter {
})
})
this.teamCollectionRemoved$ = runGQLSubscription({
const [teamCollRemoved$, teamCollRemovedSub] = runGQLSubscription({
query: TeamCollectionRemovedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamCollectionRemovedSub = teamCollRemovedSub
this.teamCollectionRemoved$ = teamCollRemoved$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Collection Removed Error: ${result.left}`)
this.removeCollection(result.right.teamCollectionRemoved)
})
this.teamRequestAdded$ = runGQLSubscription({
const [teamReqAdded$, teamReqAddedSub] = runGQLSubscription({
query: TeamRequestAddedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamRequestAddedSub = teamReqAddedSub
this.teamRequestAdded$ = teamReqAdded$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Request Added Error: ${result.left}`)
@@ -472,12 +507,15 @@ export default class NewTeamCollectionAdapter {
})
})
this.teamRequestUpdated$ = runGQLSubscription({
const [teamReqUpdated$, teamReqUpdatedSub] = runGQLSubscription({
query: TeamRequestUpdatedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamRequestUpdatedSub = teamReqUpdatedSub
this.teamRequestUpdated$ = teamReqUpdated$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Request Updated Error: ${result.left}`)
@@ -489,12 +527,15 @@ export default class NewTeamCollectionAdapter {
})
})
this.teamRequestDeleted$ = runGQLSubscription({
const [teamReqDeleted$, teamReqDeleted] = runGQLSubscription({
query: TeamRequestDeletedDocument,
variables: {
teamID: this.teamID,
},
}).subscribe((result) => {
})
this.teamRequestUpdatedSub = teamReqDeleted
this.teamRequestDeleted$ = teamReqDeleted$.subscribe((result) => {
if (E.isLeft(result))
throw new Error(`Team Request Deleted Error ${result.left}`)

View File

@@ -1,6 +1,10 @@
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import * as S from "fp-ts/string"
import qs from "qs"
import { pipe } from "fp-ts/function"
import { flow, pipe } from "fp-ts/function"
import { combineLatest, Observable } from "rxjs"
import { map } from "rxjs/operators"
import {
@@ -9,14 +13,15 @@ import {
HoppRESTRequest,
parseTemplateString,
parseBodyEnvVariables,
parseRawKeyValueEntries,
Environment,
HoppRESTHeader,
HoppRESTParam,
parseRawKeyValueEntriesE,
parseTemplateStringE,
} from "@hoppscotch/data"
import { arrayFlatMap, arraySort } from "../functional/array"
import { toFormData } from "../functional/formData"
import { tupleToRecord } from "../functional/record"
import { tupleWithSameKeysToRecord } from "../functional/record"
import { getGlobalVariables } from "~/newstore/environments"
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
@@ -29,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 }[]
}
/**
@@ -210,25 +216,40 @@ function getFinalBodyFromRequest(
}
if (request.body.contentType === "application/x-www-form-urlencoded") {
return pipe(
const parsedBodyRecord = pipe(
request.body.body,
parseRawKeyValueEntries,
parseRawKeyValueEntriesE,
E.map(
flow(
RA.toArray,
/**
* Filtering out empty keys and non-active pairs.
*/
A.filter(({ active, key }) => active && !S.isEmpty(key)),
// Filter out active
A.filter((x) => x.active),
// Convert to tuple
A.map(
({ key, value }) =>
[
parseTemplateString(key, envVariables),
parseTemplateString(value, envVariables),
] as [string, string]
),
// Tuple to Record object
tupleToRecord,
// Stringify
qs.stringify
/**
* Mapping each key-value to template-string-parser with either on array,
* which will be resolved in further steps.
*/
A.map(({ key, value }) => [
parseTemplateStringE(key, envVariables),
parseTemplateStringE(value, envVariables),
]),
/**
* Filtering and mapping only right-eithers for each key-value as [string, string].
*/
A.filterMap(([key, value]) =>
E.isRight(key) && E.isRight(value)
? O.some([key.right, value.right] as [string, string])
: O.none
),
tupleWithSameKeysToRecord,
(obj) => qs.stringify(obj, { indices: false })
)
)
)
return E.isRight(parsedBodyRecord) ? parsedBodyRecord.right : null
}
if (request.body.contentType === "multipart/form-data") {
@@ -298,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

@@ -143,6 +143,15 @@ export function useStreamSubscriber(): {
}
}
export function useI18nPathInfo() {
const { localePath, getRouteBaseName } = useContext() as any
return {
localePath: localePath as (x: string) => string,
getRouteBaseName: getRouteBaseName as (x?: any) => string, // Should be a route
}
}
export function useI18n() {
const {
app: { i18n },

View File

@@ -1,5 +1,6 @@
{
"action": {
"autoscroll": "自動捲動",
"cancel": "取消",
"choose_file": "選擇一個檔案",
"clear": "清除",
@@ -9,10 +10,11 @@
"delete": "刪除",
"disconnect": "斷開連線",
"dismiss": "忽略",
"download_file": "下載檔案",
"dont_save": "不要儲存",
"download_file": "下載檔案",
"duplicate": "複製",
"edit": "編輯",
"filter_response": "篩選回應",
"go_back": "返回",
"label": "標籤",
"learn_more": "瞭解更多",
@@ -20,11 +22,14 @@
"more": "更多",
"new": "新增",
"no": "否",
"open_workspace": "開啟工作區",
"paste": "貼上",
"prettify": "美化",
"remove": "移除",
"restore": "還原",
"save": "儲存",
"scroll_to_bottom": "捲動至底部",
"scroll_to_top": "捲動至頂部",
"search": "搜尋",
"send": "傳送",
"start": "開始",
@@ -46,10 +51,10 @@
"contact_us": "聯絡我們",
"copy": "複製",
"copy_user_id": "複製使用者驗證權杖",
"discord": "Discord",
"documentation": "幫助文件",
"developer_option": "開發者選項",
"developer_option_description": "協助開發和維護 Hoppscotch 的工具。",
"discord": "Discord",
"documentation": "幫助文件",
"github": "GitHub",
"help": "幫助與回饋",
"home": "主頁",
@@ -164,6 +169,7 @@
"profile": "登入以檢視您的設定檔",
"protocols": "協議為空",
"schema": "連線至 GraphQL 端點",
"shortcodes": "Shortcodes 為空",
"team_name": "團隊名稱為空",
"teams": "團隊為空",
"tests": "沒有針對該請求的測試"
@@ -197,9 +203,11 @@
"invalid_link": "連結無效",
"invalid_link_description": "您點擊的連結無效或已過期。",
"json_prettify_invalid_body": "無法美化無效的請求主體,處理 JSON 語法錯誤並重試",
"json_parsing_failed": "JSON 無效",
"network_error": "似乎有網路錯誤。請再試一次。",
"network_fail": "無法傳送請求",
"no_duration": "無持續時間",
"no_results_found": "找不到結果",
"script_fail": "無法執行預請求指令碼",
"something_went_wrong": "發生了一些錯誤",
"test_script_fail": "無法執行測試指令碼"
@@ -266,15 +274,19 @@
"from_url": "從網址匯入",
"gist_url": "輸入 Gist 網址",
"json_description": "從 Hoppscotch 組合 JSON 檔匯入組合",
"title": "匯入"
"title": "匯入",
"import_from_url_success": "已匯入組合",
"import_from_url_invalid_file_format": "匯入組合時發生錯誤",
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
"import_from_url_invalid_fetch": "無法從網址取得資料"
},
"layout": {
"column": "垂直布局",
"row": "水平布局",
"zen_mode": "專注模式",
"collapse_sidebar": "隱藏或顯示側邊欄",
"collapse_collection": "隱藏或顯示組合",
"name": "配置"
"collapse_sidebar": "隱藏或顯示側邊欄",
"column": "垂直布局",
"name": "配置",
"row": "水平布局",
"zen_mode": "專注模式"
},
"modal": {
"collections": "組合",
@@ -331,6 +343,11 @@
"body": "請求本體",
"choose_language": "選擇語言",
"content_type": "內容類型",
"content_type_titles": {
"others": "其他",
"structured": "結構",
"text": "文字"
},
"copy_link": "複製連結",
"duration": "持續時間",
"enter_curl": "輸入 cURL",
@@ -341,6 +358,9 @@
"method": "方法",
"name": "請求名稱",
"new": "新請求",
"override": "覆寫",
"override_help": "在標頭設置 <xmp>Content-Type</xmp>",
"overriden": "已覆寫",
"parameter_list": "查詢參數",
"parameters": "參數",
"path": "路徑",
@@ -358,12 +378,11 @@
"type": "請求類型",
"url": "網址",
"variables": "變數",
"override": "覆寫",
"override_help": "在標頭設置 <xmp>Content-Type</xmp>",
"overriden": "已覆寫"
"view_my_links": "檢視我的連結"
},
"response": {
"body": "回應本體",
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
"headers": "回應標頭",
"html": "HTML",
"image": "影像",
@@ -415,6 +434,8 @@
"proxy_use_toggle": "使用 Proxy 中介軟體傳送請求",
"read_the": "閱讀",
"reset_default": "重置為預設",
"short_codes": "快捷碼",
"short_codes_description": "我們為您打造的快捷碼。",
"sidebar_on_left": "左側邊欄",
"sync": "同步",
"sync_collections": "組合",
@@ -447,7 +468,7 @@
"documentation": "前往文件頁面",
"forward": "前往下一頁面",
"graphql": "前往 GraphQL 頁面",
"profile": "Go to Profile page",
"profile": "前往個人檔案頁面",
"realtime": "前往實時頁面",
"rest": "前往 REST 頁面",
"settings": "前往設定頁面",
@@ -476,6 +497,15 @@
"title": "主題"
}
},
"shortcodes":{
"actions":"操作",
"created_on": "建立於",
"deleted" : "已刪除快捷碼",
"method": "方法",
"not_found":"找不到快捷碼",
"short_code":"快捷碼",
"url": "網址"
},
"show": {
"code": "顯示程式碼",
"more": "顯示更多",
@@ -487,7 +517,8 @@
"event_name": "事件名稱",
"events": "事件",
"log": "日誌",
"url": "網址"
"url": "網址",
"connection_not_authorized": "此 SocketIO 連線未使用任何驗證。"
},
"sse": {
"event_type": "事件類型",
@@ -517,7 +548,19 @@
"loading": "正在載入……",
"none": "無",
"nothing_found": "沒有找到",
"waiting_send_request": "等待傳送請求"
"waiting_send_request": "等待傳送請求",
"subscribed_success": "成功訂閱此主題:{topic}",
"unsubscribed_success": "成功取消訂閱此主題:{topic}",
"subscribed_failed": "無法訂閱此主題:{topic}",
"unsubscribed_failed": "無法取消訂閱此主題:{topic}",
"published_message": "已將此訊息:{message} 發布至主題:{topic}",
"published_error": "將訊息:{topic} 發布至主題:{message} 時發生錯誤",
"message_received": "訊息:{message}已抵達主題:{topic}",
"mqtt_subscription_failed": "訂閱此主題時發生錯誤:{topic}",
"connection_lost": "失去連線",
"connection_failed": "連線失敗",
"connection_error": "連線失敗",
"reconnection_error": "重新連線失敗"
},
"support": {
"changelog": "閱讀更多有關最新版本的內容",

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

@@ -58,7 +58,7 @@
"@codemirror/tooltip": "^0.19.16",
"@codemirror/view": "^0.19.48",
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.2.0",
"@hoppscotch/data": "workspace:^0.4.2",
"@hoppscotch/data": "workspace:^0.4.3",
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.32.0",

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="docs">
<template #primary>
<div class="flex items-start justify-between p-4">
<label>

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="graphql">
<template #primary>
<GraphqlRequest :conn="gqlConn" />
<GraphqlRequestOptions :conn="gqlConn" />

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="http">
<template #primary>
<HttpRequest />
<HttpRequestOptions />

View File

@@ -13,9 +13,10 @@
<script setup lang="ts">
import { watch, ref, useRouter, useRoute } from "@nuxtjs/composition-api"
import { useI18n } from "~/helpers/utils/composables"
import { useI18n, useI18nPathInfo } from "~/helpers/utils/composables"
const t = useI18n()
const { localePath, getRouteBaseName } = useI18nPathInfo()
const router = useRouter()
const route = useRoute()
@@ -44,17 +45,21 @@ const currentTab = ref<RealtimeNavTab>("websocket")
// Update the router when the tab is updated
watch(currentTab, (newTab) => {
router.push(`/realtime/${newTab}`)
router.push(localePath(`/realtime/${newTab}`))
})
// Update the tab when router is upgrad
watch(
route,
(updateRoute) => {
if (updateRoute.path === "/realtime") router.replace("/realtime/websocket")
const path = getRouteBaseName(updateRoute)
const destination: string | undefined =
updateRoute.path.split("/realtime/")[1]
if (path.endsWith("realtime")) {
router.replace(localePath(`/realtime/websocket`))
return
}
const destination: string | undefined = path.split("realtime-")[1]
const target = REALTIME_NAVIGATION.find(
({ target }) => target === destination

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="mqtt">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="socketio">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="sse">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"

View File

@@ -1,5 +1,5 @@
<template>
<AppPaneLayout>
<AppPaneLayout layout-id="websocket">
<template #primary>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/cli",
"version": "0.2.1",
"version": "0.3.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"main": "dist/index.js",
@@ -36,7 +36,7 @@
"license": "MIT",
"private": false,
"devDependencies": {
"@hoppscotch/data": "workspace:^0.4.2",
"@hoppscotch/data": "workspace:^0.4.3",
"@hoppscotch/js-sandbox": "workspace:^2.0.0",
"@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.2.181",

View File

@@ -92,12 +92,41 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
// test("No errors occured (exit code 0).", async () => {
// const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
// const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
// const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
// const { error } = await execAsync(cmd);
test("No errors occured (exit code 0).", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error } = await execAsync(cmd);
// expect(error).toBeNull();
// });
expect(error).toBeNull();
});
});
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
"passes.json"
)}`;
test("No value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Invalid value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stdout } = await execAsync(cmd);
const out = getErrorCode(stdout);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Valid value passed to delay flag.", async () => {
const cmd = `${VALID_TEST_CMD} --delay 1`;
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});
});

View File

@@ -0,0 +1,30 @@
import { hrtime } from "process";
import { getDurationInSeconds } from "../../../utils/getters";
import { delayPromiseFunction } from "../../../utils/request";
describe("describePromiseFunction", () => {
let promiseFunc = (): Promise<number> => new Promise((resolve) => resolve(2));
beforeEach(() => {
promiseFunc = (): Promise<number> => new Promise((resolve) => resolve(2));
});
it("Should resolve the promise<number> after 2 seconds.", async () => {
const start = hrtime();
const res = await delayPromiseFunction(promiseFunc, 2000);
const end = hrtime(start);
const duration = getDurationInSeconds(end);
expect(Math.floor(duration)).toEqual(2);
expect(typeof res).toBe("number");
});
it("Should resolve the promise<number> after 4 seconds.", async () => {
const start = hrtime();
const res = await delayPromiseFunction(promiseFunc, 4000);
const end = hrtime(start);
const duration = getDurationInSeconds(end);
expect(Math.floor(duration)).toEqual(4);
expect(typeof res).toBe("number");
});
});

View File

@@ -58,7 +58,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/collection/path")()
processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/collection/path",
delay: 0,
})()
).resolves.toMatchObject({
report: {
result: true,
@@ -79,7 +84,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/collection/path")()
processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/collection/path",
delay: 0,
})()
).resolves.toMatchObject({
envs: {
selected: [{ key: "ENDPOINT", value: "https://example.com" }],
@@ -96,7 +106,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect(
processRequest(SAMPLE_REQUEST, DEFAULT_ENVS, "fake/request/path")()
processRequest({
request: SAMPLE_REQUEST,
envs: DEFAULT_ENVS,
path: "fake/request/path",
delay: 0,
})()
).resolves.toMatchObject({
report: { result: false },
});

View File

@@ -9,12 +9,14 @@ import { handleError } from "../handlers/error";
import { parseCollectionData } from "../utils/mutators";
import { parseEnvsData } from "../options/test/env";
import { TestCmdOptions } from "../types/commands";
import { parseDelayOption } from "../options/test/delay";
export const test = (path: string, options: TestCmdOptions) => async () => {
await pipe(
TE.Do,
TE.bind("envs", () => parseEnvsData(options.env)),
TE.bind("collections", () => parseCollectionData(path)),
TE.bind("delay", () => parseDelayOption(options.delay)),
TE.chainTaskK(collectionsRunner),
TE.chainW(flow(collectionsRunnerResult, collectionsRunnerExit, TE.of)),
TE.mapLeft((e) => {

View File

@@ -50,6 +50,10 @@ program
"path to a hoppscotch collection.json file for CI testing"
)
.option("-e, --env <file_path>", "path to an environment variables json file")
.option(
"-d, --delay <delay_in_ms>",
"delay in milliseconds(ms) between consecutive requests within a collection"
)
.allowExcessArguments(false)
.allowUnknownOption(false)
.description("running hoppscotch collection.json file")

View File

@@ -0,0 +1,20 @@
import * as TE from "fp-ts/TaskEither";
import * as S from "fp-ts/string";
import { pipe } from "fp-ts/function";
import { error, HoppCLIError } from "../../types/errors";
export const parseDelayOption = (
delay: unknown
): TE.TaskEither<HoppCLIError, number> =>
!S.isString(delay)
? TE.right(0)
: pipe(
delay,
Number,
TE.fromPredicate(Number.isSafeInteger, () =>
error({
code: "INVALID_ARGUMENT",
data: "Expected '-d, --delay' value to be number",
})
)
);

View File

@@ -4,6 +4,7 @@ import { HoppEnvs } from "./request";
export type CollectionRunnerParam = {
collections: HoppCollection<HoppRESTRequest>[];
envs: HoppEnvs;
delay?: number;
};
export type HoppCollectionFileExt = "json";

View File

@@ -1,5 +1,6 @@
export type TestCmdOptions = {
env: string;
delay: number;
};
export type HoppEnvFileExt = "json";

View File

@@ -26,3 +26,10 @@ export type RequestReport = {
result: boolean;
duration: { test: number; request: number; preRequest: number };
};
export type ProcessRequestParams = {
request: HoppRESTRequest;
envs: HoppEnvs;
path: string;
delay: number;
};

View File

@@ -5,7 +5,12 @@ import { bold } from "chalk";
import { log } from "console";
import round from "lodash/round";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { HoppEnvs, CollectionStack, RequestReport } from "../types/request";
import {
HoppEnvs,
CollectionStack,
RequestReport,
ProcessRequestParams,
} from "../types/request";
import {
getRequestMetrics,
preProcessRequest,
@@ -41,6 +46,7 @@ export const collectionsRunner =
(param: CollectionRunnerParam): T.Task<RequestReport[]> =>
async () => {
const envs: HoppEnvs = param.envs;
const delay = param.delay ?? 0;
const requestsReport: RequestReport[] = [];
const collectionStack: CollectionStack[] = getCollectionStack(
param.collections
@@ -54,12 +60,18 @@ export const collectionsRunner =
for (const request of collection.requests) {
const _request = preProcessRequest(request);
const requestPath = `${path}/${_request.name}`;
const processRequestParams: ProcessRequestParams = {
path: requestPath,
request: _request,
envs,
delay,
};
// Request processing initiated message.
log(WARN(`\nRunning: ${bold(requestPath)}`));
// Processing current request.
const result = await processRequest(_request, envs, requestPath)();
const result = await processRequest(processRequestParams)();
// Updating global & selected envs with new envs from processed-request output.
const { global, selected } = result.envs;

View File

@@ -129,3 +129,8 @@ export const getDurationInSeconds = (
const durationInSeconds = (end[0] * 1e9 + end[1]) / 1e9;
return round(durationInSeconds, precision);
};
export const roundDuration = (
duration: number,
precision: number = DEFAULT_DURATION_PRECISION
) => round(duration, precision);

View File

@@ -12,7 +12,11 @@ import { testRunner, getTestScriptParams, hasFailedTestCases } from "./test";
import { RequestConfig, EffectiveHoppRESTRequest } from "../interfaces/request";
import { RequestRunnerResponse } from "../interfaces/response";
import { preRequestScriptRunner } from "./pre-request";
import { HoppEnvs, RequestReport } from "../types/request";
import {
HoppEnvs,
ProcessRequestParams,
RequestReport,
} from "../types/request";
import {
printPreRequestRunner,
printRequestRunner,
@@ -189,11 +193,11 @@ const getRequest = {
*/
export const processRequest =
(
request: HoppRESTRequest,
envs: HoppEnvs,
path: string
params: ProcessRequestParams
): T.Task<{ envs: HoppEnvs; report: RequestReport }> =>
async () => {
const { envs, path, request, delay } = params;
// Initialising updatedEnvs with given parameter envs, will eventually get updated.
const result = {
envs: <HoppEnvs>envs,
@@ -247,7 +251,9 @@ export const processRequest =
duration: 0,
};
// Executing request-runner.
const requestRunnerRes = await requestRunner(requestConfig)();
const requestRunnerRes = await delayPromiseFunction<
E.Either<HoppCLIError, RequestRunnerResponse>
>(requestRunner(requestConfig), delay);
if (E.isLeft(requestRunnerRes)) {
// Updating report for errors & current result
report.errors.push(requestRunnerRes.left);
@@ -358,3 +364,15 @@ export const getRequestMetrics = (
hasReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
(requests) => <RequestMetrics>{ requests, duration }
);
/**
* A function to execute promises with specific delay in milliseconds.
* @param func Function with promise with return type T.
* @param delay TIme in milliseconds to delay function.
* @returns Promise of type same as func.
*/
export const delayPromiseFunction = <T>(
func: () => Promise<T>,
delay: number
): Promise<T> =>
new Promise((resolve) => setTimeout(() => resolve(func()), delay));

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/data",
"version": "0.4.2",
"version": "0.4.3",
"description": "Data Types, Validations and Migrations for Hoppscotch Public Data Structures",
"main": "dist/index.js",
"module": "true",

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

@@ -8,6 +8,12 @@ import * as E from "fp-ts/Either"
import * as P from "parser-ts/Parser"
import * as S from "parser-ts/string"
import * as C from "parser-ts/char"
import { recordUpdate } from "./utils/record"
/**
* Special characters in the Raw Key Value Grammar
*/
const SPECIAL_CHARS = ["#", ":"] as const
export type RawKeyValueEntry = {
key: string
@@ -31,14 +37,31 @@ const stringTakeUntilCharsInclusive = flow(
P.chainFirst(() => P.sat(() => true)),
)
const quotedString = pipe(
S.doubleQuotedString,
P.map((x) => JSON.parse(`"${x}"`))
)
const key = pipe(
stringTakeUntilChars([":", "\n"]),
P.map(Str.trim)
wsSurround(quotedString),
P.alt(() =>
pipe(
stringTakeUntilChars([":", "\n"]),
P.map(Str.trim)
)
)
)
const value = pipe(
stringTakeUntilChars(["\n"]),
P.map(Str.trim)
wsSurround(quotedString),
P.alt(() =>
pipe(
stringTakeUntilChars(["\n"]),
P.map(Str.trim)
)
)
)
const commented = pipe(
@@ -105,6 +128,37 @@ const tolerantFile = pipe(
/* End of Parser Definitions */
/**
* Detect whether the string needs to have escape characters in raw key value strings
* @param input The string to check against
*/
const stringNeedsEscapingForRawKVString = (input: string) => {
// If there are any of our special characters, it needs to be escaped definitely
if (SPECIAL_CHARS.some((x) => input.includes(x)))
return true
// The theory behind this impl is that if we apply JSON.stringify on a string
// it does escaping and then return a JSON string representation.
// We remove the quotes of the JSON and see if it can be matched against the input string
const stringified = JSON.stringify(input)
const y = stringified
.substring(1, stringified.length - 1)
.trim()
return y !== input
}
/**
* Applies Raw Key Value escaping (via quotes + escape chars) if needed
* @param input The input to apply escape on
* @returns If needed, the escaped string, else the input string itself
*/
const applyEscapeIfNeeded = (input: string) =>
stringNeedsEscapingForRawKVString(input)
? JSON.stringify(input)
: input
/**
* Converts Raw Key Value Entries to the file string format
* @param entries The entries array
@@ -113,8 +167,13 @@ const tolerantFile = pipe(
export const rawKeyValueEntriesToString = (entries: RawKeyValueEntry[]) =>
pipe(
entries,
A.map(({ key, value, active }) =>
active ? `${key}: ${value}` : `# ${key}: ${value}`
A.map(
flow(
recordUpdate("key", applyEscapeIfNeeded),
recordUpdate("value", applyEscapeIfNeeded),
({ key, value, active }) =>
active ? `${(key)}: ${value}` : `# ${key}: ${value}`
)
),
stringArrayJoin("\n")
)

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,

View File

@@ -0,0 +1,17 @@
/**
* Modify a record value with a mapping function
* @param name The key to update
* @param func The value to update
* @returns The updated record
*/
export const recordUpdate =
<
X extends {},
K extends keyof X,
R
>(name: K, func: (input: X[K]) => R) =>
(x: X): Omit<X, K> & { [x in K]: R } => ({
...x,
[name]: func(x[name])
})

View File

@@ -40,7 +40,7 @@
"author": "Hoppscotch (support@hoppscotch.io)",
"license": "MIT",
"dependencies": {
"@hoppscotch/data": "workspace:^0.4.2",
"@hoppscotch/data": "workspace:^0.4.3",
"fp-ts": "^2.11.10",
"lodash": "^4.17.21",
"quickjs-emscripten": "^0.15.0",

376
pnpm-lock.yaml generated
View File

@@ -76,7 +76,7 @@ importers:
'@graphql-codegen/urql-introspection': ^2.1.1
'@graphql-typed-document-node/core': ^3.1.1
'@hoppscotch/codemirror-lang-graphql': workspace:^0.2.0
'@hoppscotch/data': workspace:^0.4.2
'@hoppscotch/data': workspace:^0.4.3
'@hoppscotch/js-sandbox': workspace:^2.0.0
'@nuxt/types': ^2.15.8
'@nuxt/typescript-build': ^2.1.0
@@ -357,7 +357,7 @@ importers:
packages/hoppscotch-cli:
specifiers:
'@hoppscotch/data': workspace:^0.4.2
'@hoppscotch/data': workspace:^0.4.3
'@hoppscotch/js-sandbox': workspace:^2.0.0
'@relmify/jest-fp-ts': ^2.0.2
'@swc/core': ^1.2.181
@@ -427,7 +427,7 @@ importers:
packages/hoppscotch-js-sandbox:
specifiers:
'@digitak/esrun': ^3.1.2
'@hoppscotch/data': workspace:^0.4.2
'@hoppscotch/data': workspace:^0.4.3
'@relmify/jest-fp-ts': ^2.0.1
'@types/jest': ^27.4.1
'@types/lodash': ^4.14.181
@@ -539,6 +539,13 @@ packages:
dependencies:
'@babel/highlight': 7.17.9
/@babel/code-frame/7.18.6:
resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.18.6
dev: true
/@babel/compat-data/7.16.0:
resolution: {integrity: sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==}
engines: {node: '>=6.9.0'}
@@ -556,6 +563,11 @@ packages:
resolution: {integrity: sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==}
engines: {node: '>=6.9.0'}
/@babel/compat-data/7.18.8:
resolution: {integrity: sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/core/7.17.9:
resolution: {integrity: sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==}
engines: {node: '>=6.9.0'}
@@ -578,6 +590,29 @@ packages:
transitivePeerDependencies:
- supports-color
/@babel/core/7.18.10:
resolution: {integrity: sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==}
engines: {node: '>=6.9.0'}
dependencies:
'@ampproject/remapping': 2.1.2
'@babel/code-frame': 7.18.6
'@babel/generator': 7.18.12
'@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.10
'@babel/helper-module-transforms': 7.18.9
'@babel/helpers': 7.18.9
'@babel/parser': 7.18.11
'@babel/template': 7.18.10
'@babel/traverse': 7.18.11
'@babel/types': 7.18.10
convert-source-map: 1.8.0
debug: 4.3.4
gensync: 1.0.0-beta.2
json5: 2.2.1
semver: 6.3.0
transitivePeerDependencies:
- supports-color
dev: true
/@babel/generator/7.16.5:
resolution: {integrity: sha512-kIvCdjZqcdKqoDbVVdt5R99icaRtrtYhYK/xux5qiWCBmfdvEYMFZ68QCrpE5cbFM1JsuArUNs1ZkuKtTtUcZA==}
engines: {node: '>=6.9.0'}
@@ -594,6 +629,15 @@ packages:
jsesc: 2.5.2
source-map: 0.5.7
/@babel/generator/7.18.12:
resolution: {integrity: sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.10
'@jridgewell/gen-mapping': 0.3.2
jsesc: 2.5.2
dev: true
/@babel/helper-annotate-as-pure/7.16.7:
resolution: {integrity: sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==}
engines: {node: '>=6.9.0'}
@@ -631,6 +675,19 @@ packages:
browserslist: 4.20.2
semver: 6.3.0
/@babel/helper-compilation-targets/7.18.9_@babel+core@7.18.10:
resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/compat-data': 7.18.8
'@babel/core': 7.18.10
'@babel/helper-validator-option': 7.18.6
browserslist: 4.20.2
semver: 6.3.0
dev: true
/@babel/helper-create-class-features-plugin/7.16.10_@babel+core@7.17.9:
resolution: {integrity: sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg==}
engines: {node: '>=6.9.0'}
@@ -716,6 +773,11 @@ packages:
dependencies:
'@babel/types': 7.17.0
/@babel/helper-environment-visitor/7.18.9:
resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-explode-assignable-expression/7.16.7:
resolution: {integrity: sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==}
engines: {node: '>=6.9.0'}
@@ -745,6 +807,14 @@ packages:
'@babel/template': 7.16.7
'@babel/types': 7.17.0
/@babel/helper-function-name/7.18.9:
resolution: {integrity: sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.18.10
'@babel/types': 7.18.10
dev: true
/@babel/helper-get-function-arity/7.16.0:
resolution: {integrity: sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ==}
engines: {node: '>=6.9.0'}
@@ -769,6 +839,13 @@ packages:
dependencies:
'@babel/types': 7.17.0
/@babel/helper-hoist-variables/7.18.6:
resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.10
dev: true
/@babel/helper-member-expression-to-functions/7.16.7:
resolution: {integrity: sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==}
engines: {node: '>=6.9.0'}
@@ -788,6 +865,13 @@ packages:
dependencies:
'@babel/types': 7.17.0
/@babel/helper-module-imports/7.18.6:
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.10
dev: true
/@babel/helper-module-transforms/7.16.7:
resolution: {integrity: sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==}
engines: {node: '>=6.9.0'}
@@ -818,6 +902,22 @@ packages:
transitivePeerDependencies:
- supports-color
/@babel/helper-module-transforms/7.18.9:
resolution: {integrity: sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-environment-visitor': 7.18.9
'@babel/helper-module-imports': 7.18.6
'@babel/helper-simple-access': 7.18.6
'@babel/helper-split-export-declaration': 7.18.6
'@babel/helper-validator-identifier': 7.18.6
'@babel/template': 7.18.10
'@babel/traverse': 7.18.11
'@babel/types': 7.18.10
transitivePeerDependencies:
- supports-color
dev: true
/@babel/helper-optimise-call-expression/7.16.7:
resolution: {integrity: sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==}
engines: {node: '>=6.9.0'}
@@ -862,6 +962,13 @@ packages:
dependencies:
'@babel/types': 7.17.0
/@babel/helper-simple-access/7.18.6:
resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.10
dev: true
/@babel/helper-skip-transparent-expression-wrappers/7.16.0:
resolution: {integrity: sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==}
engines: {node: '>=6.9.0'}
@@ -880,14 +987,36 @@ packages:
dependencies:
'@babel/types': 7.17.0
/@babel/helper-split-export-declaration/7.18.6:
resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.10
dev: true
/@babel/helper-string-parser/7.18.10:
resolution: {integrity: sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-identifier/7.16.7:
resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==}
engines: {node: '>=6.9.0'}
/@babel/helper-validator-identifier/7.18.6:
resolution: {integrity: sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-option/7.16.7:
resolution: {integrity: sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==}
engines: {node: '>=6.9.0'}
/@babel/helper-validator-option/7.18.6:
resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-wrap-function/7.16.8:
resolution: {integrity: sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==}
engines: {node: '>=6.9.0'}
@@ -909,6 +1038,17 @@ packages:
transitivePeerDependencies:
- supports-color
/@babel/helpers/7.18.9:
resolution: {integrity: sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.18.10
'@babel/traverse': 7.18.11
'@babel/types': 7.18.10
transitivePeerDependencies:
- supports-color
dev: true
/@babel/highlight/7.17.9:
resolution: {integrity: sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==}
engines: {node: '>=6.9.0'}
@@ -917,6 +1057,15 @@ packages:
chalk: 2.4.2
js-tokens: 4.0.0
/@babel/highlight/7.18.6:
resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.18.6
chalk: 2.4.2
js-tokens: 4.0.0
dev: true
/@babel/parser/7.16.2:
resolution: {integrity: sha512-RUVpT0G2h6rOZwqLDTrKk7ksNv7YpAilTnYe1/Q+eDjxEceRMKVWbCsX7t8h6C1qCFi/1Y8WZjcEPBAFG27GPw==}
engines: {node: '>=6.0.0'}
@@ -940,6 +1089,14 @@ packages:
dependencies:
'@babel/types': 7.17.0
/@babel/parser/7.18.11:
resolution: {integrity: sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.18.10
dev: true
/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.17.9:
resolution: {integrity: sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==}
engines: {node: '>=6.9.0'}
@@ -1844,8 +2001,8 @@ packages:
dependencies:
regenerator-runtime: 0.13.9
/@babel/standalone/7.17.9:
resolution: {integrity: sha512-9wL9AtDlga8avxUrBvQJmhUtJWrelsUL0uV+TcP+49Sb6Pj8/bNIzQzU4dDp0NAPOvnZR/7msFIKsKoCl/W1/w==}
/@babel/standalone/7.18.12:
resolution: {integrity: sha512-wDh3K5IUJiSMAY0MLYBFoCaj2RCZwvDz5BHn2uHat9KOsGWEVDFgFQFIOO+81Js2phFKNppLC45iOCsZVfJniw==}
engines: {node: '>=6.9.0'}
dev: true
@@ -1863,6 +2020,15 @@ packages:
debug: 4.3.3
globals: 11.12.0
/@babel/template/7.18.10:
resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.18.6
'@babel/parser': 7.18.11
'@babel/types': 7.18.10
dev: true
/@babel/traverse/7.17.3:
resolution: {integrity: sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==}
engines: {node: '>=6.9.0'}
@@ -1898,6 +2064,24 @@ packages:
transitivePeerDependencies:
- supports-color
/@babel/traverse/7.18.11:
resolution: {integrity: sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.18.6
'@babel/generator': 7.18.12
'@babel/helper-environment-visitor': 7.18.9
'@babel/helper-function-name': 7.18.9
'@babel/helper-hoist-variables': 7.18.6
'@babel/helper-split-export-declaration': 7.18.6
'@babel/parser': 7.18.11
'@babel/types': 7.18.10
debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
dev: true
/@babel/types/7.16.0:
resolution: {integrity: sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==}
engines: {node: '>=6.9.0'}
@@ -1920,6 +2104,15 @@ packages:
'@babel/helper-validator-identifier': 7.16.7
to-fast-properties: 2.0.0
/@babel/types/7.18.10:
resolution: {integrity: sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.18.10
'@babel/helper-validator-identifier': 7.18.6
to-fast-properties: 2.0.0
dev: true
/@bcoe/v8-coverage/0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
@@ -3715,13 +3908,34 @@ packages:
chalk: 4.1.2
dev: true
/@jridgewell/gen-mapping/0.3.2:
resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.11
'@jridgewell/trace-mapping': 0.3.15
dev: true
/@jridgewell/resolve-uri/3.0.5:
resolution: {integrity: sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==}
engines: {node: '>=6.0.0'}
/@jridgewell/set-array/1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/sourcemap-codec/1.4.11:
resolution: {integrity: sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==}
/@jridgewell/trace-mapping/0.3.15:
resolution: {integrity: sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==}
dependencies:
'@jridgewell/resolve-uri': 3.0.5
'@jridgewell/sourcemap-codec': 1.4.11
dev: true
/@jridgewell/trace-mapping/0.3.4:
resolution: {integrity: sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==}
dependencies:
@@ -4028,12 +4242,12 @@ packages:
ufo: 0.7.11
dev: false
/@nuxt/kit-edge/3.0.0-rc.4-27605536.8c2c80e:
resolution: {integrity: sha512-Fu9ygT3Gi5zbthzZC5PVzaDhVUxLunF1mgfF9b7RoHaO+UoQSWI7AptRwx2jxkUHpftLZjELtDV6MW96xZiWqg==}
/@nuxt/kit-edge/3.0.0-rc.7-27670958.b0bf25c:
resolution: {integrity: sha512-4sbICutKR7fOgnva7M6QBdwwDkMFULgaTafZ1oen+av+LTsOJVdNruPjlEKLhn/6gfLmxT3cLuY61jhTz4qP7A==}
engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0 || ^18.0.0}
dependencies:
'@nuxt/schema': /@nuxt/schema-edge/3.0.0-rc.4-27605536.8c2c80e
c12: 0.2.7
'@nuxt/schema': /@nuxt/schema-edge/3.0.0-rc.7-27670958.b0bf25c
c12: 0.2.9
consola: 2.15.3
defu: 6.0.0
globby: 13.1.2
@@ -4042,14 +4256,14 @@ packages:
jiti: 1.14.0
knitwork: 0.1.2
lodash.template: 4.5.0
mlly: 0.5.3
pathe: 0.3.0
mlly: 0.5.10
pathe: 0.3.4
pkg-types: 0.3.3
scule: 0.2.1
scule: 0.3.2
semver: 7.3.7
unctx: 1.1.4
unimport: 0.3.0
untyped: 0.4.4
unctx: 2.0.1
unimport: 0.6.7
untyped: 0.4.5
transitivePeerDependencies:
- esbuild
- rollup
@@ -4082,20 +4296,20 @@ packages:
- encoding
dev: false
/@nuxt/schema-edge/3.0.0-rc.4-27605536.8c2c80e:
resolution: {integrity: sha512-KOFpjN2efx9lXj84kSHhJV/XWJ8n0zztnJjjmEY3RhgBTd7mYtdI7BsYPtZ30Tz5vJGMlHrIGkLZW6c+IYAKzw==}
/@nuxt/schema-edge/3.0.0-rc.7-27670958.b0bf25c:
resolution: {integrity: sha512-GwZWyVPqpFWNDsPx1zwczv4DIv2ync/0xTTsec8Rnbg14W83apS9vw2GppHpcDAH7R3Hx8a8pHpeg7nPyD9uCg==}
engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0 || ^18.0.0}
dependencies:
c12: 0.2.7
c12: 0.2.9
create-require: 1.1.1
defu: 6.0.0
jiti: 1.14.0
pathe: 0.3.0
pathe: 0.3.4
postcss-import-resolver: 2.0.0
scule: 0.2.1
scule: 0.3.2
std-env: 3.1.1
ufo: 0.8.4
unimport: 0.3.0
ufo: 0.8.5
unimport: 0.6.7
transitivePeerDependencies:
- esbuild
- rollup
@@ -6515,6 +6729,12 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
/acorn/8.8.0:
resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
/after/0.8.2:
resolution: {integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=}
dev: false
@@ -7496,15 +7716,15 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/c12/0.2.7:
resolution: {integrity: sha512-ih1nuHbZ6Ltf8Wss96JH6YvKIW5+9+uLAA08LUQAoDrFPGSyvPvQv/QBIRE+dCBWOK4PcwH0ylRkSa9huI1Acw==}
/c12/0.2.9:
resolution: {integrity: sha512-6jYdexgAKr+3kYoTmvC5eDtDHUg7GmFQSdeQqZzAKiPlFAN1heGUoXDbAYYwUCfefZy+WgVJbmAej5TTQpp3jA==}
dependencies:
defu: 6.0.0
dotenv: 16.0.0
dotenv: 16.0.1
gittar: 0.1.1
jiti: 1.14.0
mlly: 0.5.3
pathe: 0.2.0
mlly: 0.5.10
pathe: 0.3.4
rc9: 1.2.2
dev: true
@@ -8250,7 +8470,7 @@ packages:
lodash: ^4.17.20
marko: ^3.14.4
mote: ^0.2.0
mustache: ^4.0.1
mustache: ^3.0.0
nunjucks: ^3.2.2
plates: ~0.4.11
pug: ^3.0.0
@@ -9376,8 +9596,8 @@ packages:
engines: {node: '>=10'}
dev: true
/dotenv/16.0.0:
resolution: {integrity: sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==}
/dotenv/16.0.1:
resolution: {integrity: sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==}
engines: {node: '>=12'}
dev: true
@@ -10272,6 +10492,10 @@ packages:
/estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
/estree-walker/3.0.1:
resolution: {integrity: sha512-woY0RUD87WzMBUiZLx8NsYr23N5BKsOMZHhu2hoNRVh6NXGfoiT1KOL8G3UHlJAnEDGmfa5ubNA/AacfG+Kb0g==}
dev: true
/esutils/2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -13632,8 +13856,8 @@ packages:
emojis-list: 3.0.0
json5: 2.2.1
/local-pkg/0.4.1:
resolution: {integrity: sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==}
/local-pkg/0.4.2:
resolution: {integrity: sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==}
engines: {node: '>=14'}
dev: true
@@ -14329,11 +14553,13 @@ packages:
engines: {node: '>=10'}
hasBin: true
/mlly/0.5.3:
resolution: {integrity: sha512-im69tuLD9EJh9fc9TZRpJEFvsBcGMez7glUCWDcHWWCKzhvPmNvyaYjp/+h0qJJN/Xovrs//GzGjOOKmFw4Gog==}
/mlly/0.5.10:
resolution: {integrity: sha512-mY6i+bwcgn0XAdZTiiBt6kyoUjLsm3Cuv0T4CchQJcq/UCSUcGPapSxc4g7whtIsUfcsJ2kGqZAdmqCF/VNC/Q==}
dependencies:
pathe: 0.2.0
acorn: 8.8.0
pathe: 0.3.4
pkg-types: 0.3.3
ufo: 0.8.5
dev: true
/mocha/9.2.2:
@@ -14707,7 +14933,7 @@ packages:
/nuxt-windicss/2.2.11:
resolution: {integrity: sha512-xobq725D6vqpIgYOrLJ6CVlR4xLlFGwuq//gZikXKOdoVRpoK8C+NpHazPd4+f17urGQ4H0LqGBCIujTvV1V0g==}
dependencies:
'@nuxt/kit': /@nuxt/kit-edge/3.0.0-rc.4-27605536.8c2c80e
'@nuxt/kit': /@nuxt/kit-edge/3.0.0-rc.7-27670958.b0bf25c
'@windicss/plugin-utils': 1.8.4
consola: 2.15.3
defu: 6.0.0
@@ -15285,8 +15511,8 @@ packages:
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
dev: true
/pathe/0.3.0:
resolution: {integrity: sha512-3vUjp552BJzCw9vqKsO5sttHkbYqqsZtH0x1PNtItgqx8BXEXzoY1SYRKcL6BTyVh4lGJGLj0tM42elUDMvcYA==}
/pathe/0.3.4:
resolution: {integrity: sha512-YWgqEdxf36R6vcsyj0A+yT/rDRPe0wui4J9gRR7T4whjU5Lx/jZOr75ckEgTNaLVQABAwsrlzHRpIKcCdXAQ5A==}
dev: true
/pause-stream/0.0.11:
@@ -15383,8 +15609,8 @@ packages:
resolution: {integrity: sha512-6AJcCMnjUQPQv/Wk960w0TOmjhdjbeaQJoSKWRQv9N3rgkessCu6J0Ydsog/nw1MbpnxHuPzYbfOn2KmlZO1FA==}
dependencies:
jsonc-parser: 3.0.0
mlly: 0.5.3
pathe: 0.3.0
mlly: 0.5.10
pathe: 0.3.4
dev: true
/pluralize/8.0.0:
@@ -17120,6 +17346,11 @@ packages:
/scule/0.2.1:
resolution: {integrity: sha512-M9gnWtn3J0W+UhJOHmBxBTwv8mZCan5i1Himp60t6vvZcor0wr+IM0URKmIglsWJ7bRujNAVVN77fp+uZaWoKg==}
dev: false
/scule/0.3.2:
resolution: {integrity: sha512-zIvPdjOH8fv8CgrPT5eqtxHQXmPNnV/vHJYffZhE43KZkvULvpCTvOt1HPlFaCZx287INL9qaqrZg34e8NgI4g==}
dev: true
/selenium-webdriver/4.0.0-rc-1:
resolution: {integrity: sha512-bcrwFPRax8fifRP60p7xkWDGSJJoMkPAzufMlk5K2NyLPht/YZzR2WcIk1+3gR8VOCLlst1P2PI+MXACaFzpIw==}
@@ -17934,7 +18165,7 @@ packages:
/strip-literal/0.4.0:
resolution: {integrity: sha512-ql/sBDoJOybTKSIOWrrh8kgUEMjXMwRAkZTD0EwiwxQH/6tTPkZvMIEjp0CRlpi6V5FMiJyvxeRkEi1KrGISoA==}
dependencies:
acorn: 8.7.1
acorn: 8.8.0
dev: true
/style-mod/4.0.0:
@@ -19039,8 +19270,8 @@ packages:
/ufo/0.8.3:
resolution: {integrity: sha512-AIkk06G21y/P+NCatfU+1qldCmI0XCszZLn8AkuKotffF3eqCvlce0KuwM7ZemLE/my0GSYADOAeM5zDYWMB+A==}
/ufo/0.8.4:
resolution: {integrity: sha512-/+BmBDe8GvlB2nIflWasLLAInjYG0bC9HRnfEpNi4sw77J2AJNnEVnTDReVrehoh825+Q/evF3THXTAweyam2g==}
/ufo/0.8.5:
resolution: {integrity: sha512-e4+UtA5IRO+ha6hYklwj6r7BjiGMxS0O+UaSg9HbaTefg4kMkzj4tXzEBajRR+wkxf+golgAWKzLbytCUDMJAA==}
dev: true
/uglify-js/3.14.3:
@@ -19062,13 +19293,13 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/unctx/1.1.4:
resolution: {integrity: sha512-fQMML+GjUpIjQa0HBrrJezo2dFpTAbQbU0/KFKw4T5wpc9deGjLHSYthdfNAo2xSWM34csI6arzedezQkqtfGw==}
/unctx/2.0.1:
resolution: {integrity: sha512-4VkJKSG+lh1yYkvdI0Xd3Gm7y7PU6F0mG5SoJqCI1j2jtIaHvTLAdBfbhDjbHxT93BsRkzcaxaeBtu8W/mX1Sg==}
dependencies:
acorn: 8.7.1
estree-walker: 2.0.2
acorn: 8.8.0
estree-walker: 3.0.1
magic-string: 0.26.2
unplugin: 0.6.3
unplugin: 0.8.1
transitivePeerDependencies:
- esbuild
- rollup
@@ -19113,19 +19344,19 @@ packages:
engines: {node: '>= 0.4.12'}
dev: true
/unimport/0.3.0:
resolution: {integrity: sha512-RxvfvKBY+CyBmIuYSuBeosSiudgcVakdhVofy5mO5sJ3purQRc5yjLw0Lir7MKHnqe6XT1++8flgAvpxu1UkqQ==}
/unimport/0.6.7:
resolution: {integrity: sha512-EMoVqDjswHkU+nD098QYHXH7Mkw7KwGDQAyeRF2lgairJnuO+wpkhIcmCqrD1OPJmsjkTbJ2tW6Ap8St0PuWZA==}
dependencies:
'@rollup/pluginutils': 4.2.1
escape-string-regexp: 5.0.0
fast-glob: 3.2.11
local-pkg: 0.4.1
local-pkg: 0.4.2
magic-string: 0.26.2
mlly: 0.5.3
pathe: 0.3.0
scule: 0.2.1
mlly: 0.5.10
pathe: 0.3.4
scule: 0.3.2
strip-literal: 0.4.0
unplugin: 0.7.0
unplugin: 0.9.0
transitivePeerDependencies:
- esbuild
- rollup
@@ -19236,12 +19467,12 @@ packages:
webpack-virtual-modules: 0.4.3
dev: false
/unplugin/0.6.3:
resolution: {integrity: sha512-CoW88FQfCW/yabVc4bLrjikN9HC8dEvMU4O7B6K2jsYMPK0l6iAnd9dpJwqGcmXJKRCU9vwSsy653qg+RK0G6A==}
/unplugin/0.8.1:
resolution: {integrity: sha512-o7rUZoPLG1fH4LKinWgb77gDtTE6mw/iry0Pq0Z5UPvZ9+HZ1/4+7fic7t58s8/CGkPrDpGq+RltO+DmswcR4g==}
peerDependencies:
esbuild: '>=0.13'
rollup: ^2.50.0
vite: ^2.3.0
vite: ^2.3.0 || ^3.0.0-0
webpack: 4 || 5
peerDependenciesMeta:
esbuild:
@@ -19253,17 +19484,18 @@ packages:
webpack:
optional: true
dependencies:
acorn: 8.8.0
chokidar: 3.5.3
webpack-sources: 3.2.3
webpack-virtual-modules: 0.4.3
webpack-virtual-modules: 0.4.4
dev: true
/unplugin/0.7.0:
resolution: {integrity: sha512-OsiFrgybmqm5bGuaodvbLYhqUrvGuRHRMZDhddKEXTDbuQ1x+hR7M1WpQguXj03whVYjEYChhFo738cZH5RNig==}
/unplugin/0.9.0:
resolution: {integrity: sha512-6o7q8Y9yxdPi5yCPmRuFfeNnVzGumRNZSK6hIkvZ6hd0cfigVdm0qBx/GgQ/NEjs54eUV1qTjvMYKRs9yh3rzw==}
peerDependencies:
esbuild: '>=0.13'
rollup: ^2.50.0
vite: ^2.3.0
vite: ^2.3.0 || ^3.0.0-0
webpack: 4 || 5
peerDependenciesMeta:
esbuild:
@@ -19275,10 +19507,10 @@ packages:
webpack:
optional: true
dependencies:
acorn: 8.7.1
acorn: 8.8.0
chokidar: 3.5.3
webpack-sources: 3.2.3
webpack-virtual-modules: 0.4.3
webpack-virtual-modules: 0.4.4
dev: true
/unquote/1.1.1:
@@ -19291,13 +19523,13 @@ packages:
has-value: 0.3.1
isobject: 3.0.1
/untyped/0.4.4:
resolution: {integrity: sha512-sY6u8RedwfLfBis0copfU/fzROieyAndqPs8Kn2PfyzTjtA88vCk81J1b5z+8/VJc+cwfGy23/AqOCpvAbkNVw==}
/untyped/0.4.5:
resolution: {integrity: sha512-buq9URfOj4xAnVfu6BYNKzHZLHAzsCbHsDc/kHy66ESMqRpj00oD9qWf2M2qm0pC0DigsVxRF3uhOa5HJtrwGA==}
dependencies:
'@babel/core': 7.17.9
'@babel/standalone': 7.17.9
'@babel/types': 7.17.0
scule: 0.2.1
'@babel/core': 7.18.10
'@babel/standalone': 7.18.12
'@babel/types': 7.18.10
scule: 0.3.2
transitivePeerDependencies:
- supports-color
dev: true
@@ -19924,6 +20156,10 @@ packages:
/webpack-virtual-modules/0.4.3:
resolution: {integrity: sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==}
/webpack-virtual-modules/0.4.4:
resolution: {integrity: sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==}
dev: true
/webpack/4.46.0:
resolution: {integrity: sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==}
engines: {node: '>=6.11.5'}