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_ _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 - Instant loading with Service Workers
- Offline support - Offline support

View File

@@ -6,21 +6,26 @@
'!flex-row-reverse': SIDEBAR_ON_LEFT && mdAndLarger, '!flex-row-reverse': SIDEBAR_ON_LEFT && mdAndLarger,
}" }"
:horizontal="!mdAndLarger" :horizontal="!mdAndLarger"
@resize="setPaneEvent($event, 'vertical')"
> >
<Pane <Pane
size="75" :size="PANE_MAIN_SIZE"
min-size="65" min-size="65"
class="hide-scrollbar !overflow-auto flex flex-col" 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 <Pane
:size="COLUMN_LAYOUT ? 45 : 50" :size="PANE_MAIN_TOP_SIZE"
class="hide-scrollbar !overflow-auto flex flex-col" class="hide-scrollbar !overflow-auto flex flex-col"
> >
<slot name="primary" /> <slot name="primary" />
</Pane> </Pane>
<Pane <Pane
:size="COLUMN_LAYOUT ? 65 : 50" :size="PANE_MAIN_BOTTOM_SIZE"
class="flex flex-col hide-scrollbar !overflow-auto" class="flex flex-col hide-scrollbar !overflow-auto"
> >
<slot name="secondary" /> <slot name="secondary" />
@@ -29,7 +34,7 @@
</Pane> </Pane>
<Pane <Pane
v-if="SIDEBAR && hasSidebar" v-if="SIDEBAR && hasSidebar"
size="25" :size="PANE_SIDEBAR_SIZE"
min-size="20" min-size="20"
class="hide-scrollbar !overflow-auto flex flex-col" class="hide-scrollbar !overflow-auto flex flex-col"
> >
@@ -42,8 +47,9 @@
import { Splitpanes, Pane } from "splitpanes" import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css" import "splitpanes/dist/splitpanes.css"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core" 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 { useSetting } from "~/newstore/settings"
import { setLocalConfig, getLocalConfig } from "~/newstore/localpersistence"
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT") const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
@@ -57,4 +63,60 @@ const SIDEBAR = useSetting("SIDEBAR")
const slots = useSlots() const slots = useSlots()
const hasSidebar = computed(() => !!slots.sidebar) 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> </script>

View File

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

View File

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

View File

@@ -339,7 +339,14 @@ const selectRequest = () => {
confirmChange.value = false confirmChange.value = false
setRestReq(props.request) setRestReq(props.request)
} else if (!active.value) { } 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 { } else {
const currentReqWithNoChange = active.value.req const currentReqWithNoChange = active.value.req
const currentFullReq = getRESTRequest() const currentFullReq = getRESTRequest()

View File

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

View File

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

View File

@@ -1,363 +1,13 @@
<template> <template>
<div class="flex flex-col flex-1"> <div>
<div <HttpQueryParams />
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold" <br />
> <HttpPathVariables />
<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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Ref, ref, watch } from "@nuxtjs/composition-api" /**
import { flow, pipe } from "fp-ts/function" * TODO: Code duplication between QueryParams and Variables
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> </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 = () => { const ensureMethodInEndpoint = () => {
if ( if (
!/^http[s]?:\/\//.test(newEndpoint.value) && !/^http[s]?:\/\//.test(newEndpoint.value) &&
!newEndpoint.value.startsWith("<<") !newEndpoint.value.startsWith("<<") &&
!newEndpoint.value.startsWith("{{")
) { ) {
const domain = newEndpoint.value.split(/[/:#?]+/)[0] const domain = newEndpoint.value.split(/[/:#?]+/)[0]
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) { if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {

View File

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

View File

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

View File

@@ -221,7 +221,7 @@ export const runGQLSubscription = <
createRequest(args.query, args.variables) createRequest(args.query, args.variables)
) )
wonkaPipe( const sub = wonkaPipe(
source, source,
subscribe((res) => { subscribe((res) => {
result$.next( 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>( export const useGQLQuery = <DocType, DocVarType, DocErrorType extends string>(

View File

@@ -809,6 +809,37 @@ const samples = [
testScript: "", 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", () => { describe("Parse curl command to Hopp REST Request", () => {

View File

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

View File

@@ -16,7 +16,7 @@ import {
getAggregateEnvs, getAggregateEnvs,
} from "~/newstore/environments" } from "~/newstore/environments"
const HOPP_ENVIRONMENT_REGEX = /(<<\w+>>)/g const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
const HOPP_ENV_HIGHLIGHT = const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 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 start = pos
let end = pos let end = pos
while (start > from && /\w/.test(text[start - from - 1])) start-- while (start > from && /[a-zA-Z0-9-_]+/.test(text[start - from - 1]))
while (end < to && /\w/.test(text[end - from])) end++ start--
while (end < to && /[a-zA-Z0-9-_]+/.test(text[end - from])) end++
if ( if (
(start === pos && side < 0) || (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": { ".cm-panels.cm-panels-top": {
borderBottom: "1px solid var(--divider-light-color)", borderBottom: "1px solid var(--divider-light-color)",
top: "var(--lower-tertiary-sticky-fold) !important",
"z-index": "10",
}, },
".cm-panels.cm-panels-bottom": { ".cm-panels.cm-panels-bottom": {
borderTop: "1px solid var(--divider-light-color)", borderTop: "1px solid var(--divider-light-color)",
@@ -388,5 +390,7 @@ export const basicSetup: Extension = [
...completionKeymap, ...completionKeymap,
...lintKeymap, ...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 = < export const tupleToRecord = <
KeyType extends string | number | symbol, KeyType extends string | number | symbol,
ValueType ValueType
@@ -5,5 +13,32 @@ export const tupleToRecord = <
tuples: [KeyType, ValueType][] tuples: [KeyType, ValueType][]
): Record<KeyType, ValueType> => ): Record<KeyType, ValueType> =>
tuples.length > 0 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-q": "navigation.jump.graphql",
"alt-w": "navigation.jump.realtime", "alt-w": "navigation.jump.realtime",
"alt-d": "navigation.jump.documentation", "alt-d": "navigation.jump.documentation",
"alt-m": "navigation.jump.profile",
"alt-s": "navigation.jump.settings", "alt-s": "navigation.jump.settings",
} }

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
import * as A from "fp-ts/Array" 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 qs from "qs"
import { pipe } from "fp-ts/function" import { flow, pipe } from "fp-ts/function"
import { combineLatest, Observable } from "rxjs" import { combineLatest, Observable } from "rxjs"
import { map } from "rxjs/operators" import { map } from "rxjs/operators"
import { import {
@@ -9,14 +13,15 @@ import {
HoppRESTRequest, HoppRESTRequest,
parseTemplateString, parseTemplateString,
parseBodyEnvVariables, parseBodyEnvVariables,
parseRawKeyValueEntries,
Environment, Environment,
HoppRESTHeader, HoppRESTHeader,
HoppRESTParam, HoppRESTParam,
parseRawKeyValueEntriesE,
parseTemplateStringE,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import { arrayFlatMap, arraySort } from "../functional/array" import { arrayFlatMap, arraySort } from "../functional/array"
import { toFormData } from "../functional/formData" import { toFormData } from "../functional/formData"
import { tupleToRecord } from "../functional/record" import { tupleWithSameKeysToRecord } from "../functional/record"
import { getGlobalVariables } from "~/newstore/environments" import { getGlobalVariables } from "~/newstore/environments"
export interface EffectiveHoppRESTRequest extends HoppRESTRequest { export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
@@ -29,6 +34,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalHeaders: { key: string; value: string }[] effectiveFinalHeaders: { key: string; value: string }[]
effectiveFinalParams: { key: string; value: string }[] effectiveFinalParams: { key: string; value: string }[]
effectiveFinalBody: FormData | string | null effectiveFinalBody: FormData | string | null
effectiveFinalVars: { key: string; value: string }[]
} }
/** /**
@@ -210,25 +216,40 @@ function getFinalBodyFromRequest(
} }
if (request.body.contentType === "application/x-www-form-urlencoded") { if (request.body.contentType === "application/x-www-form-urlencoded") {
return pipe( const parsedBodyRecord = pipe(
request.body.body, 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), * Mapping each key-value to template-string-parser with either on array,
// Convert to tuple * which will be resolved in further steps.
A.map( */
({ key, value }) => A.map(({ key, value }) => [
[ parseTemplateStringE(key, envVariables),
parseTemplateString(key, envVariables), parseTemplateStringE(value, envVariables),
parseTemplateString(value, envVariables), ]),
] as [string, string]
), /**
// Tuple to Record object * Filtering and mapping only right-eithers for each key-value as [string, string].
tupleToRecord, */
// Stringify A.filterMap(([key, value]) =>
qs.stringify 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") { if (request.body.contentType === "multipart/form-data") {
@@ -298,15 +319,21 @@ export function getEffectiveRESTRequest(
value: parseTemplateString(x.value, envVariables), value: parseTemplateString(x.value, envVariables),
})) }))
) )
const effectiveFinalVars = request.vars
const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables) const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
return { return {
...request, ...request,
effectiveFinalURL: parseTemplateString(request.endpoint, envVariables), effectiveFinalURL: parseTemplateString(
request.endpoint,
envVariables,
request.vars
),
effectiveFinalHeaders, effectiveFinalHeaders,
effectiveFinalParams, effectiveFinalParams,
effectiveFinalBody, 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() { export function useI18n() {
const { const {
app: { i18n }, app: { i18n },

View File

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

View File

@@ -4,6 +4,7 @@ import {
FormDataKeyValue, FormDataKeyValue,
HoppRESTHeader, HoppRESTHeader,
HoppRESTParam, HoppRESTParam,
HoppRESTVar,
HoppRESTReqBody, HoppRESTReqBody,
HoppRESTRequest, HoppRESTRequest,
RESTReqSchemaVersion, RESTReqSchemaVersion,
@@ -29,6 +30,7 @@ export const getDefaultRESTRequest = (): HoppRESTRequest => ({
endpoint: "https://echo.hoppscotch.io", endpoint: "https://echo.hoppscotch.io",
name: "Untitled request", name: "Untitled request",
params: [], params: [],
vars: [],
headers: [], headers: [],
method: "GET", method: "GET",
auth: { 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 }) { addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
return { return {
request: { request: {
@@ -88,6 +98,14 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
addVar(curr: RESTSession, { newVar }: { newVar: HoppRESTVar }) {
return {
request: {
...curr.request,
vars: [...curr.request.vars, newVar],
},
}
},
updateParam( updateParam(
curr: RESTSession, curr: RESTSession,
{ index, updatedParam }: { index: number; updatedParam: HoppRESTParam } { 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 }) { deleteParam(curr: RESTSession, { index }: { index: number }) {
const newParams = curr.request.params.filter((_x, i) => i !== index) 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) { deleteAllParams(curr: RESTSession) {
return { return {
request: { 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) { export function addRESTParam(newParam: HoppRESTParam) {
restSessionStore.dispatch({ 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) { export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
restSessionStore.dispatch({ 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) { export function deleteRESTParam(index: number) {
restSessionStore.dispatch({ 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() { export function deleteAllRESTParams() {
restSessionStore.dispatch({ restSessionStore.dispatch({
dispatcher: "deleteAllParams", dispatcher: "deleteAllParams",
@@ -592,12 +670,20 @@ export const restParams$ = restSessionStore.subject$.pipe(
distinctUntilChanged() distinctUntilChanged()
) )
export const restVars$ = restSessionStore.subject$.pipe(
pluck("request", "vars"),
distinctUntilChanged()
)
export const restActiveParamsCount$ = restParams$.pipe( export const restActiveParamsCount$ = restParams$.pipe(
map( map(
(params) => (params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length 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( export const restMethod$ = restSessionStore.subject$.pipe(
pluck("request", "method"), pluck("request", "method"),

View File

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

View File

@@ -58,7 +58,7 @@
"@codemirror/tooltip": "^0.19.16", "@codemirror/tooltip": "^0.19.16",
"@codemirror/view": "^0.19.48", "@codemirror/view": "^0.19.48",
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.2.0", "@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", "@hoppscotch/js-sandbox": "workspace:^2.0.0",
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.32.0", "@nuxtjs/composition-api": "^0.32.0",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppPaneLayout> <AppPaneLayout layout-id="mqtt">
<template #primary> <template #primary>
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar" 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> <template>
<AppPaneLayout> <AppPaneLayout layout-id="socketio">
<template #primary> <template #primary>
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar" 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> <template>
<AppPaneLayout> <AppPaneLayout layout-id="sse">
<template #primary> <template #primary>
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar" 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> <template>
<AppPaneLayout> <AppPaneLayout layout-id="websocket">
<template #primary> <template #primary>
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar" 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", "name": "@hoppscotch/cli",
"version": "0.2.1", "version": "0.3.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.", "description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io", "homepage": "https://hoppscotch.io",
"main": "dist/index.js", "main": "dist/index.js",
@@ -36,7 +36,7 @@
"license": "MIT", "license": "MIT",
"private": false, "private": false,
"devDependencies": { "devDependencies": {
"@hoppscotch/data": "workspace:^0.4.2", "@hoppscotch/data": "workspace:^0.4.3",
"@hoppscotch/js-sandbox": "workspace:^2.0.0", "@hoppscotch/js-sandbox": "workspace:^2.0.0",
"@relmify/jest-fp-ts": "^2.0.2", "@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.2.181", "@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"); expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
}); });
// test("No errors occured (exit code 0).", async () => { test("No errors occured (exit code 0).", async () => {
// const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json"); const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
// const ENV_PATH = getTestJsonFilePath("env-flag-envs.json"); const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
// const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`; const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
// const { error } = await execAsync(cmd); 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); (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect( 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({ ).resolves.toMatchObject({
report: { report: {
result: true, result: true,
@@ -79,7 +84,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE); (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect( 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({ ).resolves.toMatchObject({
envs: { envs: {
selected: [{ key: "ENDPOINT", value: "https://example.com" }], selected: [{ key: "ENDPOINT", value: "https://example.com" }],
@@ -96,7 +106,12 @@ describe("processRequest", () => {
(axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE); (axios as unknown as jest.Mock).mockResolvedValue(DEFAULT_RESPONSE);
return expect( 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({ ).resolves.toMatchObject({
report: { result: false }, report: { result: false },
}); });

View File

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

View File

@@ -50,6 +50,10 @@ program
"path to a hoppscotch collection.json file for CI testing" "path to a hoppscotch collection.json file for CI testing"
) )
.option("-e, --env <file_path>", "path to an environment variables json file") .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) .allowExcessArguments(false)
.allowUnknownOption(false) .allowUnknownOption(false)
.description("running hoppscotch collection.json file") .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 = { export type CollectionRunnerParam = {
collections: HoppCollection<HoppRESTRequest>[]; collections: HoppCollection<HoppRESTRequest>[];
envs: HoppEnvs; envs: HoppEnvs;
delay?: number;
}; };
export type HoppCollectionFileExt = "json"; export type HoppCollectionFileExt = "json";

View File

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

View File

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

View File

@@ -129,3 +129,8 @@ export const getDurationInSeconds = (
const durationInSeconds = (end[0] * 1e9 + end[1]) / 1e9; const durationInSeconds = (end[0] * 1e9 + end[1]) / 1e9;
return round(durationInSeconds, precision); 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 { RequestConfig, EffectiveHoppRESTRequest } from "../interfaces/request";
import { RequestRunnerResponse } from "../interfaces/response"; import { RequestRunnerResponse } from "../interfaces/response";
import { preRequestScriptRunner } from "./pre-request"; import { preRequestScriptRunner } from "./pre-request";
import { HoppEnvs, RequestReport } from "../types/request"; import {
HoppEnvs,
ProcessRequestParams,
RequestReport,
} from "../types/request";
import { import {
printPreRequestRunner, printPreRequestRunner,
printRequestRunner, printRequestRunner,
@@ -189,11 +193,11 @@ const getRequest = {
*/ */
export const processRequest = export const processRequest =
( (
request: HoppRESTRequest, params: ProcessRequestParams
envs: HoppEnvs,
path: string
): T.Task<{ envs: HoppEnvs; report: RequestReport }> => ): T.Task<{ envs: HoppEnvs; report: RequestReport }> =>
async () => { async () => {
const { envs, path, request, delay } = params;
// Initialising updatedEnvs with given parameter envs, will eventually get updated. // Initialising updatedEnvs with given parameter envs, will eventually get updated.
const result = { const result = {
envs: <HoppEnvs>envs, envs: <HoppEnvs>envs,
@@ -247,7 +251,9 @@ export const processRequest =
duration: 0, duration: 0,
}; };
// Executing request-runner. // Executing request-runner.
const requestRunnerRes = await requestRunner(requestConfig)(); const requestRunnerRes = await delayPromiseFunction<
E.Either<HoppCLIError, RequestRunnerResponse>
>(requestRunner(requestConfig), delay);
if (E.isLeft(requestRunnerRes)) { if (E.isLeft(requestRunnerRes)) {
// Updating report for errors & current result // Updating report for errors & current result
report.errors.push(requestRunnerRes.left); report.errors.push(requestRunnerRes.left);
@@ -358,3 +364,15 @@ export const getRequestMetrics = (
hasReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 }, hasReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
(requests) => <RequestMetrics>{ requests, duration } (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", "name": "@hoppscotch/data",
"version": "0.4.2", "version": "0.4.3",
"description": "Data Types, Validations and Migrations for Hoppscotch Public Data Structures", "description": "Data Types, Validations and Migrations for Hoppscotch Public Data Structures",
"main": "dist/index.js", "main": "dist/index.js",
"module": "true", "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_ENV_VAR = /<<([^>]*)>>/g // "<<myVariable>>"
const REGEX_MY_VAR = /{{([^}]*)}}/g // "{{myVariable}}"
/** /**
* How much times can we expand environment variables * How much times can we expand environment variables
@@ -59,26 +65,37 @@ export const parseBodyEnvVariables = (
export function parseTemplateStringE( export function parseTemplateStringE(
str: string, str: string,
variables: Environment["variables"] variables: Environment["variables"],
myVariables?: Variables | undefined
) { ) {
if (!variables || !str) { if (!variables || !str) {
return E.right(str) return E.right(str)
} }
let result = 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( result = decodeURI(encodeURI(result)).replace(
REGEX_ENV_VAR, REGEX_ENV_VAR,
(_, p1) => variables.find((x) => x.key === p1)?.value || "" (_, p1) => variables.find((x) => x.key === p1)?.value || ""
) )
depth++ depthEnv++
} }
return depth > ENV_MAX_EXPAND_LIMIT if (myVariables) {
? E.left(ENV_EXPAND_LOOP) while (result.match(REGEX_MY_VAR) != null && depthVar <= ENV_MAX_EXPAND_LIMIT) {
: E.right(result) 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 = ( export const parseTemplateString = (
str: string, str: string,
variables: Environment["variables"] variables: Environment["variables"],
myVariables?: Variables
) => ) =>
pipe( pipe(
parseTemplateStringE(str, variables), parseTemplateStringE(str, variables, myVariables),
E.getOrElse(() => str) 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 P from "parser-ts/Parser"
import * as S from "parser-ts/string" import * as S from "parser-ts/string"
import * as C from "parser-ts/char" 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 = { export type RawKeyValueEntry = {
key: string key: string
@@ -31,14 +37,31 @@ const stringTakeUntilCharsInclusive = flow(
P.chainFirst(() => P.sat(() => true)), P.chainFirst(() => P.sat(() => true)),
) )
const quotedString = pipe(
S.doubleQuotedString,
P.map((x) => JSON.parse(`"${x}"`))
)
const key = pipe( const key = pipe(
stringTakeUntilChars([":", "\n"]), wsSurround(quotedString),
P.map(Str.trim)
P.alt(() =>
pipe(
stringTakeUntilChars([":", "\n"]),
P.map(Str.trim)
)
)
) )
const value = pipe( const value = pipe(
stringTakeUntilChars(["\n"]), wsSurround(quotedString),
P.map(Str.trim)
P.alt(() =>
pipe(
stringTakeUntilChars(["\n"]),
P.map(Str.trim)
)
)
) )
const commented = pipe( const commented = pipe(
@@ -105,6 +128,37 @@ const tolerantFile = pipe(
/* End of Parser Definitions */ /* 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 * Converts Raw Key Value Entries to the file string format
* @param entries The entries array * @param entries The entries array
@@ -113,8 +167,13 @@ const tolerantFile = pipe(
export const rawKeyValueEntriesToString = (entries: RawKeyValueEntry[]) => export const rawKeyValueEntriesToString = (entries: RawKeyValueEntry[]) =>
pipe( pipe(
entries, entries,
A.map(({ key, value, active }) => A.map(
active ? `${key}: ${value}` : `# ${key}: ${value}` flow(
recordUpdate("key", applyEscapeIfNeeded),
recordUpdate("value", applyEscapeIfNeeded),
({ key, value, active }) =>
active ? `${(key)}: ${value}` : `# ${key}: ${value}`
)
), ),
stringArrayJoin("\n") stringArrayJoin("\n")
) )

View File

@@ -16,6 +16,11 @@ export type HoppRESTParam = {
active: boolean active: boolean
} }
export type HoppRESTVar = {
key: string
value: string
}
export type HoppRESTHeader = { export type HoppRESTHeader = {
key: string key: string
value: string value: string
@@ -51,6 +56,7 @@ export interface HoppRESTRequest {
method: string method: string
endpoint: string endpoint: string
params: HoppRESTParam[] params: HoppRESTParam[]
vars: HoppRESTVar[]
headers: HoppRESTHeader[] headers: HoppRESTHeader[]
preRequestScript: string preRequestScript: string
testScript: string testScript: string
@@ -74,6 +80,10 @@ export const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
(arr) => arr.filter((p) => p.key !== "" && p.value !== ""), (arr) => arr.filter((p) => p.key !== "" && p.value !== ""),
lodashIsEqualEq lodashIsEqualEq
), ),
vars: mapThenEq(
(arr) => arr.filter((v) => v.key !== "" && v.value !== ""),
lodashIsEqualEq
),
method: S.Eq, method: S.Eq,
name: S.Eq, name: S.Eq,
preRequestScript: S.Eq, preRequestScript: S.Eq,
@@ -126,6 +136,9 @@ export function safelyExtractRESTRequest(
if (x.hasOwnProperty("params") && Array.isArray(x.params)) if (x.hasOwnProperty("params") && Array.isArray(x.params))
req.params = x.params // TODO: Deep nested checks 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)) if (x.hasOwnProperty("headers") && Array.isArray(x.headers))
req.headers = x.headers // TODO: Deep nested checks 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 name = x?.name ?? "Untitled request"
const method = x?.method ?? "" const method = x?.method ?? ""
@@ -201,6 +227,7 @@ export function translateToNewRequest(x: any): HoppRESTRequest {
endpoint, endpoint,
headers, headers,
params, params,
vars,
method, method,
preRequestScript, preRequestScript,
testScript, 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)", "author": "Hoppscotch (support@hoppscotch.io)",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hoppscotch/data": "workspace:^0.4.2", "@hoppscotch/data": "workspace:^0.4.3",
"fp-ts": "^2.11.10", "fp-ts": "^2.11.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"quickjs-emscripten": "^0.15.0", "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-codegen/urql-introspection': ^2.1.1
'@graphql-typed-document-node/core': ^3.1.1 '@graphql-typed-document-node/core': ^3.1.1
'@hoppscotch/codemirror-lang-graphql': workspace:^0.2.0 '@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 '@hoppscotch/js-sandbox': workspace:^2.0.0
'@nuxt/types': ^2.15.8 '@nuxt/types': ^2.15.8
'@nuxt/typescript-build': ^2.1.0 '@nuxt/typescript-build': ^2.1.0
@@ -357,7 +357,7 @@ importers:
packages/hoppscotch-cli: packages/hoppscotch-cli:
specifiers: specifiers:
'@hoppscotch/data': workspace:^0.4.2 '@hoppscotch/data': workspace:^0.4.3
'@hoppscotch/js-sandbox': workspace:^2.0.0 '@hoppscotch/js-sandbox': workspace:^2.0.0
'@relmify/jest-fp-ts': ^2.0.2 '@relmify/jest-fp-ts': ^2.0.2
'@swc/core': ^1.2.181 '@swc/core': ^1.2.181
@@ -427,7 +427,7 @@ importers:
packages/hoppscotch-js-sandbox: packages/hoppscotch-js-sandbox:
specifiers: specifiers:
'@digitak/esrun': ^3.1.2 '@digitak/esrun': ^3.1.2
'@hoppscotch/data': workspace:^0.4.2 '@hoppscotch/data': workspace:^0.4.3
'@relmify/jest-fp-ts': ^2.0.1 '@relmify/jest-fp-ts': ^2.0.1
'@types/jest': ^27.4.1 '@types/jest': ^27.4.1
'@types/lodash': ^4.14.181 '@types/lodash': ^4.14.181
@@ -539,6 +539,13 @@ packages:
dependencies: dependencies:
'@babel/highlight': 7.17.9 '@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: /@babel/compat-data/7.16.0:
resolution: {integrity: sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==} resolution: {integrity: sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -556,6 +563,11 @@ packages:
resolution: {integrity: sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==} resolution: {integrity: sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==}
engines: {node: '>=6.9.0'} 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: /@babel/core/7.17.9:
resolution: {integrity: sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==} resolution: {integrity: sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -578,6 +590,29 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: /@babel/generator/7.16.5:
resolution: {integrity: sha512-kIvCdjZqcdKqoDbVVdt5R99icaRtrtYhYK/xux5qiWCBmfdvEYMFZ68QCrpE5cbFM1JsuArUNs1ZkuKtTtUcZA==} resolution: {integrity: sha512-kIvCdjZqcdKqoDbVVdt5R99icaRtrtYhYK/xux5qiWCBmfdvEYMFZ68QCrpE5cbFM1JsuArUNs1ZkuKtTtUcZA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -594,6 +629,15 @@ packages:
jsesc: 2.5.2 jsesc: 2.5.2
source-map: 0.5.7 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: /@babel/helper-annotate-as-pure/7.16.7:
resolution: {integrity: sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==} resolution: {integrity: sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -631,6 +675,19 @@ packages:
browserslist: 4.20.2 browserslist: 4.20.2
semver: 6.3.0 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: /@babel/helper-create-class-features-plugin/7.16.10_@babel+core@7.17.9:
resolution: {integrity: sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg==} resolution: {integrity: sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -716,6 +773,11 @@ packages:
dependencies: dependencies:
'@babel/types': 7.17.0 '@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: /@babel/helper-explode-assignable-expression/7.16.7:
resolution: {integrity: sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==} resolution: {integrity: sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -745,6 +807,14 @@ packages:
'@babel/template': 7.16.7 '@babel/template': 7.16.7
'@babel/types': 7.17.0 '@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: /@babel/helper-get-function-arity/7.16.0:
resolution: {integrity: sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ==} resolution: {integrity: sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -769,6 +839,13 @@ packages:
dependencies: dependencies:
'@babel/types': 7.17.0 '@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: /@babel/helper-member-expression-to-functions/7.16.7:
resolution: {integrity: sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==} resolution: {integrity: sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -788,6 +865,13 @@ packages:
dependencies: dependencies:
'@babel/types': 7.17.0 '@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: /@babel/helper-module-transforms/7.16.7:
resolution: {integrity: sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==} resolution: {integrity: sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -818,6 +902,22 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: /@babel/helper-optimise-call-expression/7.16.7:
resolution: {integrity: sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==} resolution: {integrity: sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -862,6 +962,13 @@ packages:
dependencies: dependencies:
'@babel/types': 7.17.0 '@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: /@babel/helper-skip-transparent-expression-wrappers/7.16.0:
resolution: {integrity: sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==} resolution: {integrity: sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -880,14 +987,36 @@ packages:
dependencies: dependencies:
'@babel/types': 7.17.0 '@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: /@babel/helper-validator-identifier/7.16.7:
resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==} resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==}
engines: {node: '>=6.9.0'} 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: /@babel/helper-validator-option/7.16.7:
resolution: {integrity: sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==} resolution: {integrity: sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==}
engines: {node: '>=6.9.0'} 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: /@babel/helper-wrap-function/7.16.8:
resolution: {integrity: sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==} resolution: {integrity: sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -909,6 +1038,17 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: /@babel/highlight/7.17.9:
resolution: {integrity: sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==} resolution: {integrity: sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -917,6 +1057,15 @@ packages:
chalk: 2.4.2 chalk: 2.4.2
js-tokens: 4.0.0 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: /@babel/parser/7.16.2:
resolution: {integrity: sha512-RUVpT0G2h6rOZwqLDTrKk7ksNv7YpAilTnYe1/Q+eDjxEceRMKVWbCsX7t8h6C1qCFi/1Y8WZjcEPBAFG27GPw==} resolution: {integrity: sha512-RUVpT0G2h6rOZwqLDTrKk7ksNv7YpAilTnYe1/Q+eDjxEceRMKVWbCsX7t8h6C1qCFi/1Y8WZjcEPBAFG27GPw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@@ -940,6 +1089,14 @@ packages:
dependencies: dependencies:
'@babel/types': 7.17.0 '@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: /@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==} resolution: {integrity: sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1844,8 +2001,8 @@ packages:
dependencies: dependencies:
regenerator-runtime: 0.13.9 regenerator-runtime: 0.13.9
/@babel/standalone/7.17.9: /@babel/standalone/7.18.12:
resolution: {integrity: sha512-9wL9AtDlga8avxUrBvQJmhUtJWrelsUL0uV+TcP+49Sb6Pj8/bNIzQzU4dDp0NAPOvnZR/7msFIKsKoCl/W1/w==} resolution: {integrity: sha512-wDh3K5IUJiSMAY0MLYBFoCaj2RCZwvDz5BHn2uHat9KOsGWEVDFgFQFIOO+81Js2phFKNppLC45iOCsZVfJniw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dev: true dev: true
@@ -1863,6 +2020,15 @@ packages:
debug: 4.3.3 debug: 4.3.3
globals: 11.12.0 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: /@babel/traverse/7.17.3:
resolution: {integrity: sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==} resolution: {integrity: sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1898,6 +2064,24 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: /@babel/types/7.16.0:
resolution: {integrity: sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==} resolution: {integrity: sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1920,6 +2104,15 @@ packages:
'@babel/helper-validator-identifier': 7.16.7 '@babel/helper-validator-identifier': 7.16.7
to-fast-properties: 2.0.0 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: /@bcoe/v8-coverage/0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true dev: true
@@ -3715,13 +3908,34 @@ packages:
chalk: 4.1.2 chalk: 4.1.2
dev: true 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: /@jridgewell/resolve-uri/3.0.5:
resolution: {integrity: sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==} resolution: {integrity: sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==}
engines: {node: '>=6.0.0'} 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: /@jridgewell/sourcemap-codec/1.4.11:
resolution: {integrity: sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==} 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: /@jridgewell/trace-mapping/0.3.4:
resolution: {integrity: sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==} resolution: {integrity: sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==}
dependencies: dependencies:
@@ -4028,12 +4242,12 @@ packages:
ufo: 0.7.11 ufo: 0.7.11
dev: false dev: false
/@nuxt/kit-edge/3.0.0-rc.4-27605536.8c2c80e: /@nuxt/kit-edge/3.0.0-rc.7-27670958.b0bf25c:
resolution: {integrity: sha512-Fu9ygT3Gi5zbthzZC5PVzaDhVUxLunF1mgfF9b7RoHaO+UoQSWI7AptRwx2jxkUHpftLZjELtDV6MW96xZiWqg==} resolution: {integrity: sha512-4sbICutKR7fOgnva7M6QBdwwDkMFULgaTafZ1oen+av+LTsOJVdNruPjlEKLhn/6gfLmxT3cLuY61jhTz4qP7A==}
engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0 || ^18.0.0} engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0 || ^18.0.0}
dependencies: dependencies:
'@nuxt/schema': /@nuxt/schema-edge/3.0.0-rc.4-27605536.8c2c80e '@nuxt/schema': /@nuxt/schema-edge/3.0.0-rc.7-27670958.b0bf25c
c12: 0.2.7 c12: 0.2.9
consola: 2.15.3 consola: 2.15.3
defu: 6.0.0 defu: 6.0.0
globby: 13.1.2 globby: 13.1.2
@@ -4042,14 +4256,14 @@ packages:
jiti: 1.14.0 jiti: 1.14.0
knitwork: 0.1.2 knitwork: 0.1.2
lodash.template: 4.5.0 lodash.template: 4.5.0
mlly: 0.5.3 mlly: 0.5.10
pathe: 0.3.0 pathe: 0.3.4
pkg-types: 0.3.3 pkg-types: 0.3.3
scule: 0.2.1 scule: 0.3.2
semver: 7.3.7 semver: 7.3.7
unctx: 1.1.4 unctx: 2.0.1
unimport: 0.3.0 unimport: 0.6.7
untyped: 0.4.4 untyped: 0.4.5
transitivePeerDependencies: transitivePeerDependencies:
- esbuild - esbuild
- rollup - rollup
@@ -4082,20 +4296,20 @@ packages:
- encoding - encoding
dev: false dev: false
/@nuxt/schema-edge/3.0.0-rc.4-27605536.8c2c80e: /@nuxt/schema-edge/3.0.0-rc.7-27670958.b0bf25c:
resolution: {integrity: sha512-KOFpjN2efx9lXj84kSHhJV/XWJ8n0zztnJjjmEY3RhgBTd7mYtdI7BsYPtZ30Tz5vJGMlHrIGkLZW6c+IYAKzw==} resolution: {integrity: sha512-GwZWyVPqpFWNDsPx1zwczv4DIv2ync/0xTTsec8Rnbg14W83apS9vw2GppHpcDAH7R3Hx8a8pHpeg7nPyD9uCg==}
engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0 || ^18.0.0} engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0 || ^18.0.0}
dependencies: dependencies:
c12: 0.2.7 c12: 0.2.9
create-require: 1.1.1 create-require: 1.1.1
defu: 6.0.0 defu: 6.0.0
jiti: 1.14.0 jiti: 1.14.0
pathe: 0.3.0 pathe: 0.3.4
postcss-import-resolver: 2.0.0 postcss-import-resolver: 2.0.0
scule: 0.2.1 scule: 0.3.2
std-env: 3.1.1 std-env: 3.1.1
ufo: 0.8.4 ufo: 0.8.5
unimport: 0.3.0 unimport: 0.6.7
transitivePeerDependencies: transitivePeerDependencies:
- esbuild - esbuild
- rollup - rollup
@@ -6515,6 +6729,12 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true 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: /after/0.8.2:
resolution: {integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=} resolution: {integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=}
dev: false dev: false
@@ -7496,15 +7716,15 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dev: false dev: false
/c12/0.2.7: /c12/0.2.9:
resolution: {integrity: sha512-ih1nuHbZ6Ltf8Wss96JH6YvKIW5+9+uLAA08LUQAoDrFPGSyvPvQv/QBIRE+dCBWOK4PcwH0ylRkSa9huI1Acw==} resolution: {integrity: sha512-6jYdexgAKr+3kYoTmvC5eDtDHUg7GmFQSdeQqZzAKiPlFAN1heGUoXDbAYYwUCfefZy+WgVJbmAej5TTQpp3jA==}
dependencies: dependencies:
defu: 6.0.0 defu: 6.0.0
dotenv: 16.0.0 dotenv: 16.0.1
gittar: 0.1.1 gittar: 0.1.1
jiti: 1.14.0 jiti: 1.14.0
mlly: 0.5.3 mlly: 0.5.10
pathe: 0.2.0 pathe: 0.3.4
rc9: 1.2.2 rc9: 1.2.2
dev: true dev: true
@@ -8250,7 +8470,7 @@ packages:
lodash: ^4.17.20 lodash: ^4.17.20
marko: ^3.14.4 marko: ^3.14.4
mote: ^0.2.0 mote: ^0.2.0
mustache: ^4.0.1 mustache: ^3.0.0
nunjucks: ^3.2.2 nunjucks: ^3.2.2
plates: ~0.4.11 plates: ~0.4.11
pug: ^3.0.0 pug: ^3.0.0
@@ -9376,8 +9596,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/dotenv/16.0.0: /dotenv/16.0.1:
resolution: {integrity: sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==} resolution: {integrity: sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
@@ -10272,6 +10492,10 @@ packages:
/estree-walker/2.0.2: /estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 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: /esutils/2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -13632,8 +13856,8 @@ packages:
emojis-list: 3.0.0 emojis-list: 3.0.0
json5: 2.2.1 json5: 2.2.1
/local-pkg/0.4.1: /local-pkg/0.4.2:
resolution: {integrity: sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==} resolution: {integrity: sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==}
engines: {node: '>=14'} engines: {node: '>=14'}
dev: true dev: true
@@ -14329,11 +14553,13 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
/mlly/0.5.3: /mlly/0.5.10:
resolution: {integrity: sha512-im69tuLD9EJh9fc9TZRpJEFvsBcGMez7glUCWDcHWWCKzhvPmNvyaYjp/+h0qJJN/Xovrs//GzGjOOKmFw4Gog==} resolution: {integrity: sha512-mY6i+bwcgn0XAdZTiiBt6kyoUjLsm3Cuv0T4CchQJcq/UCSUcGPapSxc4g7whtIsUfcsJ2kGqZAdmqCF/VNC/Q==}
dependencies: dependencies:
pathe: 0.2.0 acorn: 8.8.0
pathe: 0.3.4
pkg-types: 0.3.3 pkg-types: 0.3.3
ufo: 0.8.5
dev: true dev: true
/mocha/9.2.2: /mocha/9.2.2:
@@ -14707,7 +14933,7 @@ packages:
/nuxt-windicss/2.2.11: /nuxt-windicss/2.2.11:
resolution: {integrity: sha512-xobq725D6vqpIgYOrLJ6CVlR4xLlFGwuq//gZikXKOdoVRpoK8C+NpHazPd4+f17urGQ4H0LqGBCIujTvV1V0g==} resolution: {integrity: sha512-xobq725D6vqpIgYOrLJ6CVlR4xLlFGwuq//gZikXKOdoVRpoK8C+NpHazPd4+f17urGQ4H0LqGBCIujTvV1V0g==}
dependencies: 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 '@windicss/plugin-utils': 1.8.4
consola: 2.15.3 consola: 2.15.3
defu: 6.0.0 defu: 6.0.0
@@ -15285,8 +15511,8 @@ packages:
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
dev: true dev: true
/pathe/0.3.0: /pathe/0.3.4:
resolution: {integrity: sha512-3vUjp552BJzCw9vqKsO5sttHkbYqqsZtH0x1PNtItgqx8BXEXzoY1SYRKcL6BTyVh4lGJGLj0tM42elUDMvcYA==} resolution: {integrity: sha512-YWgqEdxf36R6vcsyj0A+yT/rDRPe0wui4J9gRR7T4whjU5Lx/jZOr75ckEgTNaLVQABAwsrlzHRpIKcCdXAQ5A==}
dev: true dev: true
/pause-stream/0.0.11: /pause-stream/0.0.11:
@@ -15383,8 +15609,8 @@ packages:
resolution: {integrity: sha512-6AJcCMnjUQPQv/Wk960w0TOmjhdjbeaQJoSKWRQv9N3rgkessCu6J0Ydsog/nw1MbpnxHuPzYbfOn2KmlZO1FA==} resolution: {integrity: sha512-6AJcCMnjUQPQv/Wk960w0TOmjhdjbeaQJoSKWRQv9N3rgkessCu6J0Ydsog/nw1MbpnxHuPzYbfOn2KmlZO1FA==}
dependencies: dependencies:
jsonc-parser: 3.0.0 jsonc-parser: 3.0.0
mlly: 0.5.3 mlly: 0.5.10
pathe: 0.3.0 pathe: 0.3.4
dev: true dev: true
/pluralize/8.0.0: /pluralize/8.0.0:
@@ -17120,6 +17346,11 @@ packages:
/scule/0.2.1: /scule/0.2.1:
resolution: {integrity: sha512-M9gnWtn3J0W+UhJOHmBxBTwv8mZCan5i1Himp60t6vvZcor0wr+IM0URKmIglsWJ7bRujNAVVN77fp+uZaWoKg==} 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: /selenium-webdriver/4.0.0-rc-1:
resolution: {integrity: sha512-bcrwFPRax8fifRP60p7xkWDGSJJoMkPAzufMlk5K2NyLPht/YZzR2WcIk1+3gR8VOCLlst1P2PI+MXACaFzpIw==} resolution: {integrity: sha512-bcrwFPRax8fifRP60p7xkWDGSJJoMkPAzufMlk5K2NyLPht/YZzR2WcIk1+3gR8VOCLlst1P2PI+MXACaFzpIw==}
@@ -17934,7 +18165,7 @@ packages:
/strip-literal/0.4.0: /strip-literal/0.4.0:
resolution: {integrity: sha512-ql/sBDoJOybTKSIOWrrh8kgUEMjXMwRAkZTD0EwiwxQH/6tTPkZvMIEjp0CRlpi6V5FMiJyvxeRkEi1KrGISoA==} resolution: {integrity: sha512-ql/sBDoJOybTKSIOWrrh8kgUEMjXMwRAkZTD0EwiwxQH/6tTPkZvMIEjp0CRlpi6V5FMiJyvxeRkEi1KrGISoA==}
dependencies: dependencies:
acorn: 8.7.1 acorn: 8.8.0
dev: true dev: true
/style-mod/4.0.0: /style-mod/4.0.0:
@@ -19039,8 +19270,8 @@ packages:
/ufo/0.8.3: /ufo/0.8.3:
resolution: {integrity: sha512-AIkk06G21y/P+NCatfU+1qldCmI0XCszZLn8AkuKotffF3eqCvlce0KuwM7ZemLE/my0GSYADOAeM5zDYWMB+A==} resolution: {integrity: sha512-AIkk06G21y/P+NCatfU+1qldCmI0XCszZLn8AkuKotffF3eqCvlce0KuwM7ZemLE/my0GSYADOAeM5zDYWMB+A==}
/ufo/0.8.4: /ufo/0.8.5:
resolution: {integrity: sha512-/+BmBDe8GvlB2nIflWasLLAInjYG0bC9HRnfEpNi4sw77J2AJNnEVnTDReVrehoh825+Q/evF3THXTAweyam2g==} resolution: {integrity: sha512-e4+UtA5IRO+ha6hYklwj6r7BjiGMxS0O+UaSg9HbaTefg4kMkzj4tXzEBajRR+wkxf+golgAWKzLbytCUDMJAA==}
dev: true dev: true
/uglify-js/3.14.3: /uglify-js/3.14.3:
@@ -19062,13 +19293,13 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/unctx/1.1.4: /unctx/2.0.1:
resolution: {integrity: sha512-fQMML+GjUpIjQa0HBrrJezo2dFpTAbQbU0/KFKw4T5wpc9deGjLHSYthdfNAo2xSWM34csI6arzedezQkqtfGw==} resolution: {integrity: sha512-4VkJKSG+lh1yYkvdI0Xd3Gm7y7PU6F0mG5SoJqCI1j2jtIaHvTLAdBfbhDjbHxT93BsRkzcaxaeBtu8W/mX1Sg==}
dependencies: dependencies:
acorn: 8.7.1 acorn: 8.8.0
estree-walker: 2.0.2 estree-walker: 3.0.1
magic-string: 0.26.2 magic-string: 0.26.2
unplugin: 0.6.3 unplugin: 0.8.1
transitivePeerDependencies: transitivePeerDependencies:
- esbuild - esbuild
- rollup - rollup
@@ -19113,19 +19344,19 @@ packages:
engines: {node: '>= 0.4.12'} engines: {node: '>= 0.4.12'}
dev: true dev: true
/unimport/0.3.0: /unimport/0.6.7:
resolution: {integrity: sha512-RxvfvKBY+CyBmIuYSuBeosSiudgcVakdhVofy5mO5sJ3purQRc5yjLw0Lir7MKHnqe6XT1++8flgAvpxu1UkqQ==} resolution: {integrity: sha512-EMoVqDjswHkU+nD098QYHXH7Mkw7KwGDQAyeRF2lgairJnuO+wpkhIcmCqrD1OPJmsjkTbJ2tW6Ap8St0PuWZA==}
dependencies: dependencies:
'@rollup/pluginutils': 4.2.1 '@rollup/pluginutils': 4.2.1
escape-string-regexp: 5.0.0 escape-string-regexp: 5.0.0
fast-glob: 3.2.11 fast-glob: 3.2.11
local-pkg: 0.4.1 local-pkg: 0.4.2
magic-string: 0.26.2 magic-string: 0.26.2
mlly: 0.5.3 mlly: 0.5.10
pathe: 0.3.0 pathe: 0.3.4
scule: 0.2.1 scule: 0.3.2
strip-literal: 0.4.0 strip-literal: 0.4.0
unplugin: 0.7.0 unplugin: 0.9.0
transitivePeerDependencies: transitivePeerDependencies:
- esbuild - esbuild
- rollup - rollup
@@ -19236,12 +19467,12 @@ packages:
webpack-virtual-modules: 0.4.3 webpack-virtual-modules: 0.4.3
dev: false dev: false
/unplugin/0.6.3: /unplugin/0.8.1:
resolution: {integrity: sha512-CoW88FQfCW/yabVc4bLrjikN9HC8dEvMU4O7B6K2jsYMPK0l6iAnd9dpJwqGcmXJKRCU9vwSsy653qg+RK0G6A==} resolution: {integrity: sha512-o7rUZoPLG1fH4LKinWgb77gDtTE6mw/iry0Pq0Z5UPvZ9+HZ1/4+7fic7t58s8/CGkPrDpGq+RltO+DmswcR4g==}
peerDependencies: peerDependencies:
esbuild: '>=0.13' esbuild: '>=0.13'
rollup: ^2.50.0 rollup: ^2.50.0
vite: ^2.3.0 vite: ^2.3.0 || ^3.0.0-0
webpack: 4 || 5 webpack: 4 || 5
peerDependenciesMeta: peerDependenciesMeta:
esbuild: esbuild:
@@ -19253,17 +19484,18 @@ packages:
webpack: webpack:
optional: true optional: true
dependencies: dependencies:
acorn: 8.8.0
chokidar: 3.5.3 chokidar: 3.5.3
webpack-sources: 3.2.3 webpack-sources: 3.2.3
webpack-virtual-modules: 0.4.3 webpack-virtual-modules: 0.4.4
dev: true dev: true
/unplugin/0.7.0: /unplugin/0.9.0:
resolution: {integrity: sha512-OsiFrgybmqm5bGuaodvbLYhqUrvGuRHRMZDhddKEXTDbuQ1x+hR7M1WpQguXj03whVYjEYChhFo738cZH5RNig==} resolution: {integrity: sha512-6o7q8Y9yxdPi5yCPmRuFfeNnVzGumRNZSK6hIkvZ6hd0cfigVdm0qBx/GgQ/NEjs54eUV1qTjvMYKRs9yh3rzw==}
peerDependencies: peerDependencies:
esbuild: '>=0.13' esbuild: '>=0.13'
rollup: ^2.50.0 rollup: ^2.50.0
vite: ^2.3.0 vite: ^2.3.0 || ^3.0.0-0
webpack: 4 || 5 webpack: 4 || 5
peerDependenciesMeta: peerDependenciesMeta:
esbuild: esbuild:
@@ -19275,10 +19507,10 @@ packages:
webpack: webpack:
optional: true optional: true
dependencies: dependencies:
acorn: 8.7.1 acorn: 8.8.0
chokidar: 3.5.3 chokidar: 3.5.3
webpack-sources: 3.2.3 webpack-sources: 3.2.3
webpack-virtual-modules: 0.4.3 webpack-virtual-modules: 0.4.4
dev: true dev: true
/unquote/1.1.1: /unquote/1.1.1:
@@ -19291,13 +19523,13 @@ packages:
has-value: 0.3.1 has-value: 0.3.1
isobject: 3.0.1 isobject: 3.0.1
/untyped/0.4.4: /untyped/0.4.5:
resolution: {integrity: sha512-sY6u8RedwfLfBis0copfU/fzROieyAndqPs8Kn2PfyzTjtA88vCk81J1b5z+8/VJc+cwfGy23/AqOCpvAbkNVw==} resolution: {integrity: sha512-buq9URfOj4xAnVfu6BYNKzHZLHAzsCbHsDc/kHy66ESMqRpj00oD9qWf2M2qm0pC0DigsVxRF3uhOa5HJtrwGA==}
dependencies: dependencies:
'@babel/core': 7.17.9 '@babel/core': 7.18.10
'@babel/standalone': 7.17.9 '@babel/standalone': 7.18.12
'@babel/types': 7.17.0 '@babel/types': 7.18.10
scule: 0.2.1 scule: 0.3.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
@@ -19924,6 +20156,10 @@ packages:
/webpack-virtual-modules/0.4.3: /webpack-virtual-modules/0.4.3:
resolution: {integrity: sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==} 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: /webpack/4.46.0:
resolution: {integrity: sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==} resolution: {integrity: sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==}
engines: {node: '>=6.11.5'} engines: {node: '>=6.11.5'}