![]()
diff --git a/packages/hoppscotch-common/src/components/http/Parameters.vue b/packages/hoppscotch-common/src/components/http/Parameters.vue
index 87fccf142..030ec6888 100644
--- a/packages/hoppscotch-common/src/components/http/Parameters.vue
+++ b/packages/hoppscotch-common/src/components/http/Parameters.vue
@@ -179,7 +179,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
import IconCircle from "~icons/lucide/circle"
import IconTrash from "~icons/lucide/trash"
import IconWrapText from "~icons/lucide/wrap-text"
-import { reactive, Ref, ref, watch } from "vue"
+import { reactive, ref, watch } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -198,10 +198,9 @@ import { useCodemirror } from "@composables/codemirror"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
-import { useStream } from "@composables/stream"
-import { restParams$, setRESTParams } from "~/newstore/RESTSession"
import { throwError } from "@functional/error"
import { objRemoveKey } from "@functional/object"
+import { useVModel } from "@vueuse/core"
const colorMode = useColorMode()
@@ -232,8 +231,16 @@ useCodemirror(
})
)
+const props = defineProps<{
+ modelValue: HoppRESTParam[]
+}>()
+
+const emit = defineEmits<{
+ (e: "update:modelValue", value: Array
): void
+}>()
+
// The functional parameters list (the parameters actually applied to the session)
-const params = useStream(restParams$, [], setRESTParams) as Ref
+const params = useVModel(props, "modelValue", emit)
// The UI representation of the parameters list (has the empty end param)
const workingParams = ref>([
diff --git a/packages/hoppscotch-common/src/components/http/PreRequestScript.vue b/packages/hoppscotch-common/src/components/http/PreRequestScript.vue
index 62e8f25bc..9146e3663 100644
--- a/packages/hoppscotch-common/src/components/http/PreRequestScript.vue
+++ b/packages/hoppscotch-common/src/components/http/PreRequestScript.vue
@@ -66,16 +66,23 @@ import IconHelpCircle from "~icons/lucide/help-circle"
import IconWrapText from "~icons/lucide/wrap-text"
import IconTrash2 from "~icons/lucide/trash-2"
import { reactive, ref } from "vue"
-import { usePreRequestScript } from "~/newstore/RESTSession"
import snippets from "@helpers/preRequestScriptSnippets"
import { useCodemirror } from "@composables/codemirror"
import linter from "~/helpers/editor/linting/preRequest"
import completer from "~/helpers/editor/completion/preRequest"
import { useI18n } from "@composables/i18n"
+import { useVModel } from "@vueuse/core"
const t = useI18n()
-const preRequestScript = usePreRequestScript()
+const props = defineProps<{
+ modelValue: string
+}>()
+const emit = defineEmits<{
+ (e: "update:modelValue", value: string): void
+}>()
+
+const preRequestScript = useVModel(props, "modelValue", emit)
const preRequestEditor = ref(null)
const linewrapEnabled = ref(true)
diff --git a/packages/hoppscotch-common/src/components/http/RawBody.vue b/packages/hoppscotch-common/src/components/http/RawBody.vue
index b71f27497..0d442d920 100644
--- a/packages/hoppscotch-common/src/components/http/RawBody.vue
+++ b/packages/hoppscotch-common/src/components/http/RawBody.vue
@@ -35,7 +35,7 @@
'application/hal+json',
'application/vnd.api+json',
'application/xml',
- ].includes(contentType)
+ ].includes(body.contentType)
"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
@@ -74,16 +74,14 @@ import IconInfo from "~icons/lucide/info"
import { computed, reactive, Ref, ref, watch } from "vue"
import * as TO from "fp-ts/TaskOption"
import { pipe } from "fp-ts/function"
-import { ValidContentTypes } from "@hoppscotch/data"
-import { refAutoReset } from "@vueuse/core"
+import { HoppRESTReqBody, ValidContentTypes } from "@hoppscotch/data"
+import { refAutoReset, useVModel } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror"
import { getEditorLangForMimeType } from "@helpers/editorutils"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { isJSONContentType } from "~/helpers/utils/contenttypes"
-import { useRESTRequestBody } from "~/newstore/RESTSession"
-
import jsonLinter from "~/helpers/editor/linting/json"
import { readFileAsText } from "~/helpers/functional/files"
@@ -92,27 +90,35 @@ type PossibleContentTypes = Exclude<
"multipart/form-data" | "application/x-www-form-urlencoded"
>
+type Body = HoppRESTReqBody & { contentType: PossibleContentTypes }
+
+const props = defineProps<{
+ modelValue: Body
+}>()
+
+const emit = defineEmits<{
+ (e: "update:modelValue", val: Body): void
+}>()
+
+const body = useVModel(props, "modelValue", emit)
+
const t = useI18n()
const payload = ref(null)
-const props = defineProps<{
- contentType: PossibleContentTypes
-}>()
-
const toast = useToast()
-const rawParamsBody = pluckRef(useRESTRequestBody(), "body")
+const rawParamsBody = pluckRef(body, "body")
const prettifyIcon = refAutoReset<
typeof IconWand2 | typeof IconCheck | typeof IconInfo
>(IconWand2, 1000)
const rawInputEditorLang = computed(() =>
- getEditorLangForMimeType(props.contentType)
+ getEditorLangForMimeType(body.value.contentType)
)
const langLinter = computed(() =>
- isJSONContentType(props.contentType) ? jsonLinter : null
+ isJSONContentType(body.value.contentType) ? jsonLinter : null
)
const linewrapEnabled = ref(true)
@@ -175,10 +181,10 @@ const uploadPayload = async (e: Event) => {
const prettifyRequestBody = () => {
let prettifyBody = ""
try {
- if (props.contentType.endsWith("json")) {
+ if (body.value.contentType.endsWith("json")) {
const jsonObj = JSON.parse(rawParamsBody.value as string)
prettifyBody = JSON.stringify(jsonObj, null, 2)
- } else if (props.contentType == "application/xml") {
+ } else if (body.value.contentType == "application/xml") {
prettifyBody = prettifyXML(rawParamsBody.value as string)
}
rawParamsBody.value = prettifyBody
diff --git a/packages/hoppscotch-common/src/components/http/Request.vue b/packages/hoppscotch-common/src/components/http/Request.vue
index 921f81625..ed98b98cf 100644
--- a/packages/hoppscotch-common/src/components/http/Request.vue
+++ b/packages/hoppscotch-common/src/components/http/Request.vue
@@ -17,10 +17,10 @@
@@ -36,7 +36,7 @@
:label="method"
@click="
() => {
- onSelectMethod(method)
+ updateMethod(method)
hide()
}
"
@@ -50,7 +50,7 @@
class="flex flex-1 overflow-auto transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
>
{
showSaveRequestModal = true
@@ -227,55 +225,40 @@
diff --git a/packages/hoppscotch-common/src/components/http/RequestOptions.vue b/packages/hoppscotch-common/src/components/http/RequestOptions.vue
index a8a26a829..28e238b8f 100644
--- a/packages/hoppscotch-common/src/components/http/RequestOptions.vue
+++ b/packages/hoppscotch-common/src/components/http/RequestOptions.vue
@@ -9,51 +9,52 @@
:label="`${t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`"
>
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/packages/hoppscotch-common/src/components/http/RequestTab.vue b/packages/hoppscotch-common/src/components/http/RequestTab.vue
new file mode 100644
index 000000000..65fb68fd1
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/http/RequestTab.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/http/Response.vue b/packages/hoppscotch-common/src/components/http/Response.vue
index 2f6c75152..69e47cefd 100644
--- a/packages/hoppscotch-common/src/components/http/Response.vue
+++ b/packages/hoppscotch-common/src/components/http/Response.vue
@@ -1,34 +1,42 @@
-
+
diff --git a/packages/hoppscotch-common/src/components/http/ResponseMeta.vue b/packages/hoppscotch-common/src/components/http/ResponseMeta.vue
index 493f7ce50..91f122415 100644
--- a/packages/hoppscotch-common/src/components/http/ResponseMeta.vue
+++ b/packages/hoppscotch-common/src/components/http/ResponseMeta.vue
@@ -107,7 +107,7 @@ const t = useI18n()
const colorMode = useColorMode()
const props = defineProps<{
- response: HoppRESTResponse | null
+ response: HoppRESTResponse | null | undefined
}>()
/**
@@ -119,6 +119,7 @@ const props = defineProps<{
const readableResponseSize = computed(() => {
if (
props.response === null ||
+ props.response === undefined ||
props.response.type === "loading" ||
props.response.type === "network_fail" ||
props.response.type === "script_fail" ||
@@ -137,6 +138,7 @@ const readableResponseSize = computed(() => {
const statusCategory = computed(() => {
if (
props.response === null ||
+ props.response === undefined ||
props.response.type === "loading" ||
props.response.type === "network_fail" ||
props.response.type === "script_fail" ||
diff --git a/packages/hoppscotch-common/src/components/http/Sidebar.vue b/packages/hoppscotch-common/src/components/http/Sidebar.vue
index c4465ec3d..d0ecb4bbe 100644
--- a/packages/hoppscotch-common/src/components/http/Sidebar.vue
+++ b/packages/hoppscotch-common/src/components/http/Sidebar.vue
@@ -5,13 +5,6 @@
vertical
render-inactive-tabs
>
-
-
-
+
+
+
@@ -40,5 +40,5 @@ const t = useI18n()
type RequestOptionTabs = "history" | "collections" | "env"
-const selectedNavigationTab = ref("history")
+const selectedNavigationTab = ref("collections")
diff --git a/packages/hoppscotch-common/src/components/http/TestResult.vue b/packages/hoppscotch-common/src/components/http/TestResult.vue
index f8f9b3941..fb4cd7eb1 100644
--- a/packages/hoppscotch-common/src/components/http/TestResult.vue
+++ b/packages/hoppscotch-common/src/components/http/TestResult.vue
@@ -216,7 +216,6 @@ import {
setGlobalEnvVariables,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
-import { restTestResults$, setRESTTestResults } from "~/newstore/RESTSession"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
import IconTrash2 from "~icons/lucide/trash-2"
@@ -226,6 +225,17 @@ import IconCheck from "~icons/lucide/check"
import IconClose from "~icons/lucide/x"
import { useColorMode } from "~/composables/theming"
+import { useVModel } from "@vueuse/core"
+
+const props = defineProps<{
+ modelValue: HoppTestResult | null | undefined
+}>()
+
+const emit = defineEmits<{
+ (e: "update:modelValue", val: HoppTestResult | null | undefined): void
+}>()
+
+const testResults = useVModel(props, "modelValue", emit)
const t = useI18n()
const colorMode = useColorMode()
@@ -236,11 +246,6 @@ const displayModalAdd = (shouldDisplay: boolean) => {
showModalDetails.value = shouldDisplay
}
-const testResults = useReadonlyStream(
- restTestResults$,
- null
-) as Ref
-
/**
* Get the "addition" environment variables
* @returns Array of objects with key-value pairs of arguments
@@ -250,7 +255,9 @@ const getAdditionVars = () =>
? testResults.value.envDiff.selected.additions
: []
-const clearContent = () => setRESTTestResults(null)
+const clearContent = () => {
+ testResults.value = null
+}
const haveEnvVariables = computed(() => {
if (!testResults.value) return false
diff --git a/packages/hoppscotch-common/src/components/http/Tests.vue b/packages/hoppscotch-common/src/components/http/Tests.vue
index b56245b64..15a5b738d 100644
--- a/packages/hoppscotch-common/src/components/http/Tests.vue
+++ b/packages/hoppscotch-common/src/components/http/Tests.vue
@@ -66,17 +66,20 @@ import IconHelpCircle from "~icons/lucide/help-circle"
import IconWrapText from "~icons/lucide/wrap-text"
import IconTrash2 from "~icons/lucide/trash-2"
import { reactive, ref } from "vue"
-import { useTestScript } from "~/newstore/RESTSession"
import testSnippets from "~/helpers/testSnippets"
import { useCodemirror } from "@composables/codemirror"
import linter from "~/helpers/editor/linting/testScript"
import completer from "~/helpers/editor/completion/testScript"
import { useI18n } from "@composables/i18n"
+import { useVModel } from "@vueuse/core"
const t = useI18n()
-const testScript = useTestScript()
-
+const props = defineProps<{
+ modelValue: string
+}>()
+const emit = defineEmits(["update:modelValue"])
+const testScript = useVModel(props, "modelValue", emit)
const testScriptEditor = ref(null)
const linewrapEnabled = ref(true)
diff --git a/packages/hoppscotch-common/src/components/http/URLEncodedParams.vue b/packages/hoppscotch-common/src/components/http/URLEncodedParams.vue
index 8957eb9c4..7ba5cdc77 100644
--- a/packages/hoppscotch-common/src/components/http/URLEncodedParams.vue
+++ b/packages/hoppscotch-common/src/components/http/URLEncodedParams.vue
@@ -181,6 +181,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
import { computed, reactive, ref, watch } from "vue"
import { isEqual, cloneDeep } from "lodash-es"
import {
+ HoppRESTReqBody,
parseRawKeyValueEntries,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
@@ -194,13 +195,27 @@ import * as E from "fp-ts/Either"
import draggable from "vuedraggable-es"
import { useCodemirror } from "@composables/codemirror"
import linter from "~/helpers/editor/linting/rawKeyValue"
-import { useRESTRequestBody } from "~/newstore/RESTSession"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming"
import { objRemoveKey } from "~/helpers/functional/object"
import { throwError } from "~/helpers/functional/error"
+import { useVModel } from "@vueuse/core"
+
+type Body = HoppRESTReqBody & {
+ contentType: "application/x-www-form-urlencoded"
+}
+
+const props = defineProps<{
+ modelValue: Body
+}>()
+
+const emit = defineEmits<{
+ (e: "update:modelValue", val: Body): void
+}>()
+
+const body = useVModel(props, "modelValue", emit)
const t = useI18n()
const toast = useToast()
@@ -231,7 +246,7 @@ useCodemirror(
)
// The functional urlEncodedParams list (the urlEncodedParams actually in the system)
-const urlEncodedParamsRaw = pluckRef(useRESTRequestBody(), "body")
+const urlEncodedParamsRaw = pluckRef(body, "body")
const urlEncodedParams = computed({
get() {
diff --git a/packages/hoppscotch-common/src/components/http/authorization/ApiKey.vue b/packages/hoppscotch-common/src/components/http/authorization/ApiKey.vue
new file mode 100644
index 000000000..d5aa48225
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/http/authorization/ApiKey.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ auth.addTo = 'Headers'
+ hide()
+ }
+ "
+ />
+ {
+ auth.addTo = 'Query params'
+ hide()
+ }
+ "
+ />
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/http/authorization/Basic.vue b/packages/hoppscotch-common/src/components/http/authorization/Basic.vue
new file mode 100644
index 000000000..b3db29a6b
--- /dev/null
+++ b/packages/hoppscotch-common/src/components/http/authorization/Basic.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue b/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue
index 31a0400a2..6ad7ef987 100644
--- a/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue
+++ b/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue
@@ -1,6 +1,6 @@
@@ -11,7 +11,10 @@
:label="t(lens.lensName)"
class="flex flex-col flex-1 w-full h-full"
>
-
+
-
+
@@ -49,44 +52,48 @@ import {
getLensRenderers,
Lens,
} from "~/helpers/lenses/lenses"
-import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
-import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
-import { restTestResults$ } from "~/newstore/RESTSession"
+import { useVModel } from "@vueuse/core"
+import { HoppRESTTab } from "~/helpers/rest/tab"
const props = defineProps<{
- response: HoppRESTResponse | null
+ tab: HoppRESTTab
selectedTabPreference: string | null
}>()
const emit = defineEmits<{
+ (e: "update:tab", val: HoppRESTTab): void
(e: "update:selectedTabPreference", newTab: string): void
}>()
+const tab = useVModel(props, "tab", emit)
+const selectedTabPreference = useVModel(props, "selectedTabPreference", emit)
+
const allLensRenderers = getLensRenderers()
function lensRendererFor(name: string) {
return allLensRenderers[name]
}
-const testResults = useReadonlyStream(restTestResults$, null)
-
const t = useI18n()
const selectedLensTab = ref("")
const maybeHeaders = computed(() => {
if (
- !props.response ||
- !(props.response.type === "success" || props.response.type === "fail")
+ !tab.value.response ||
+ !(
+ tab.value.response.type === "success" ||
+ tab.value.response.type === "fail"
+ )
)
return null
- return props.response.headers
+ return tab.value.response.headers
})
const validLenses = computed(() => {
- if (!props.response) return []
- return getSuitableLenses(props.response)
+ if (!tab.value.response) return []
+ return getSuitableLenses(tab.value.response)
})
watch(
@@ -101,10 +108,10 @@ watch(
]
if (
- props.selectedTabPreference &&
- validRenderers.includes(props.selectedTabPreference)
+ selectedTabPreference.value &&
+ validRenderers.includes(selectedTabPreference.value)
) {
- selectedLensTab.value = props.selectedTabPreference
+ selectedLensTab.value = selectedTabPreference.value
} else {
selectedLensTab.value = newLenses[0].renderer
}
@@ -113,6 +120,6 @@ watch(
)
watch(selectedLensTab, (newLensID) => {
- emit("update:selectedTabPreference", newLensID)
+ selectedTabPreference.value = newLensID
})
diff --git a/packages/hoppscotch-common/src/components/workspace/Selector.vue b/packages/hoppscotch-common/src/components/workspace/Selector.vue
index 2724e2347..733d4d49d 100644
--- a/packages/hoppscotch-common/src/components/workspace/Selector.vue
+++ b/packages/hoppscotch-common/src/components/workspace/Selector.vue
@@ -19,6 +19,12 @@
v-if="!loading && myTeams.length === 0"
class="flex flex-col items-center justify-center flex-1 p-4 text-secondaryLight"
>
+
{{ t("empty.teams") }}
@@ -78,12 +84,14 @@ import { useI18n } from "@composables/i18n"
import IconUser from "~icons/lucide/user"
import IconUsers from "~icons/lucide/users"
import IconPlus from "~icons/lucide/plus"
+import { useColorMode } from "@composables/theming"
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconDone from "~icons/lucide/check"
import { useLocalState } from "~/newstore/localstate"
const t = useI18n()
+const colorMode = useColorMode()
const showModalAdd = ref(false)
diff --git a/packages/hoppscotch-common/src/helpers/RESTExtURLParams.ts b/packages/hoppscotch-common/src/helpers/RESTExtURLParams.ts
index 1b9bf5c2b..fbb81e5ac 100644
--- a/packages/hoppscotch-common/src/helpers/RESTExtURLParams.ts
+++ b/packages/hoppscotch-common/src/helpers/RESTExtURLParams.ts
@@ -1,6 +1,6 @@
import { FormDataKeyValue, HoppRESTRequest } from "@hoppscotch/data"
+import { getDefaultRESTRequest } from "./rest/default"
import { isJSONContentType } from "./utils/contenttypes"
-import { getDefaultRESTRequest } from "~/newstore/RESTSession"
/**
* Handles translations for all the hopp.io REST Shareable URL params
diff --git a/packages/hoppscotch-common/src/helpers/RESTRequest.ts b/packages/hoppscotch-common/src/helpers/RESTRequest.ts
new file mode 100644
index 000000000..d44a2d55d
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/RESTRequest.ts
@@ -0,0 +1,293 @@
+import {
+ FormDataKeyValue,
+ HoppRESTAuth,
+ HoppRESTHeader,
+ HoppRESTParam,
+ HoppRESTReqBody,
+ HoppRESTRequest,
+ RESTReqSchemaVersion,
+ ValidContentTypes,
+} from "@hoppscotch/data"
+import { BehaviorSubject, combineLatest, map } from "rxjs"
+import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
+import { HoppRESTResponse } from "./types/HoppRESTResponse"
+
+export class RESTRequest {
+ public v$ = new BehaviorSubject(
+ RESTReqSchemaVersion
+ )
+ public name$ = new BehaviorSubject("Untitled")
+ public endpoint$ = new BehaviorSubject("https://echo.hoppscotch.io/")
+ public params$ = new BehaviorSubject([])
+ public headers$ = new BehaviorSubject([])
+ public method$ = new BehaviorSubject("GET")
+ public auth$ = new BehaviorSubject({
+ authType: "none",
+ authActive: true,
+ })
+ public preRequestScript$ = new BehaviorSubject("")
+ public testScript$ = new BehaviorSubject("")
+ public body$ = new BehaviorSubject({
+ contentType: null,
+ body: null,
+ })
+
+ public response$ = new BehaviorSubject(null)
+
+ get request$() {
+ // any of above changes construct requests
+ return combineLatest([
+ this.v$,
+ this.name$,
+ this.endpoint$,
+ this.params$,
+ this.headers$,
+ this.method$,
+ this.auth$,
+ this.preRequestScript$,
+ this.testScript$,
+ this.body$,
+ ]).pipe(
+ map(
+ ([
+ v,
+ name,
+ endpoint,
+ params,
+ headers,
+ method,
+ auth,
+ preRequestScript,
+ testScript,
+ body,
+ ]) => ({
+ v,
+ name,
+ endpoint,
+ params,
+ headers,
+ method,
+ auth,
+ preRequestScript,
+ testScript,
+ body,
+ })
+ )
+ )
+ }
+
+ get contentType$() {
+ return this.body$.pipe(map((body) => body.contentType))
+ }
+
+ get bodyContent$() {
+ return this.body$.pipe(map((body) => body.body))
+ }
+
+ get headersCount$() {
+ return this.headers$.pipe(
+ map(
+ (params) =>
+ params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
+ .length
+ )
+ )
+ }
+
+ get paramsCount$() {
+ return this.params$.pipe(
+ map(
+ (params) =>
+ params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
+ .length
+ )
+ )
+ }
+
+ setName(name: string) {
+ this.name$.next(name)
+ }
+
+ setEndpoint(newURL: string) {
+ this.endpoint$.next(newURL)
+ }
+
+ setMethod(newMethod: string) {
+ this.method$.next(newMethod)
+ }
+
+ setParams(entries: HoppRESTParam[]) {
+ this.params$.next(entries)
+ }
+
+ addParam(newParam: HoppRESTParam) {
+ const newParams = this.params$.value.concat(newParam)
+ this.params$.next(newParams)
+ }
+
+ updateParam(index: number, updatedParam: HoppRESTParam) {
+ const newParams = this.params$.value.map((param, i) =>
+ i === index ? updatedParam : param
+ )
+ this.params$.next(newParams)
+ }
+
+ deleteParam(index: number) {
+ const newParams = this.params$.value.filter((_, i) => i !== index)
+ this.params$.next(newParams)
+ }
+
+ deleteAllParams() {
+ this.params$.next([])
+ }
+
+ setHeaders(entries: HoppRESTHeader[]) {
+ this.headers$.next(entries)
+ }
+
+ addHeader(newHeader: HoppRESTHeader) {
+ const newHeaders = this.headers$.value.concat(newHeader)
+ this.headers$.next(newHeaders)
+ }
+
+ updateHeader(index: number, updatedHeader: HoppRESTHeader) {
+ const newHeaders = this.headers$.value.map((header, i) =>
+ i === index ? updatedHeader : header
+ )
+ this.headers$.next(newHeaders)
+ }
+
+ deleteHeader(index: number) {
+ const newHeaders = this.headers$.value.filter((_, i) => i !== index)
+ this.headers$.next(newHeaders)
+ }
+
+ deleteAllHeaders() {
+ this.headers$.next([])
+ }
+
+ setContentType(newContentType: ValidContentTypes | null) {
+ // TODO: persist body evenafter switching content typees
+ this.body$.next(applyBodyTransition(this.body$.value, newContentType))
+ }
+
+ setBody(newBody: string | FormDataKeyValue[] | null) {
+ const body = { ...this.body$.value }
+ body.body = newBody
+ this.body$.next({ ...body })
+ }
+
+ addFormDataEntry(entry: FormDataKeyValue) {
+ if (this.body$.value.contentType !== "multipart/form-data") return {}
+ const body: HoppRESTReqBody = {
+ contentType: "multipart/form-data",
+ body: [...this.body$.value.body, entry],
+ }
+ this.body$.next(body)
+ }
+
+ deleteFormDataEntry(index: number) {
+ // Only perform update if the current content-type is formdata
+ if (this.body$.value.contentType !== "multipart/form-data") return {}
+
+ const body: HoppRESTReqBody = {
+ contentType: "multipart/form-data",
+ body: [...this.body$.value.body.filter((_, i) => i !== index)],
+ }
+
+ this.body$.next(body)
+ }
+
+ updateFormDataEntry(index: number, entry: FormDataKeyValue) {
+ // Only perform update if the current content-type is formdata
+ if (this.body$.value.contentType !== "multipart/form-data") return {}
+
+ const body: HoppRESTReqBody = {
+ contentType: "multipart/form-data",
+ body: [
+ ...this.body$.value.body.map((oldEntry, i) =>
+ i === index ? entry : oldEntry
+ ),
+ ],
+ }
+ this.body$.next(body)
+ }
+
+ deleteAllFormDataEntries() {
+ // Only perform update if the current content-type is formdata
+ if (this.body$.value.contentType !== "multipart/form-data") return {}
+
+ const body: HoppRESTReqBody = {
+ contentType: "multipart/form-data",
+ body: [],
+ }
+ this.body$.next(body)
+ }
+
+ setRequestBody(newBody: HoppRESTReqBody) {
+ this.body$.next(newBody)
+ }
+
+ setAuth(newAuth: HoppRESTAuth) {
+ this.auth$.next(newAuth)
+ }
+
+ setPreRequestScript(newScript: string) {
+ this.preRequestScript$.next(newScript)
+ }
+
+ setTestScript(newScript: string) {
+ this.testScript$.next(newScript)
+ }
+
+ updateResponse(response: HoppRESTResponse | null) {
+ this.response$.next(response)
+ }
+
+ setRequest(request: HoppRESTRequest) {
+ this.v$.next(RESTReqSchemaVersion)
+ this.name$.next(request.name)
+ this.endpoint$.next(request.endpoint)
+ this.params$.next(request.params)
+ this.headers$.next(request.headers)
+ this.method$.next(request.method)
+ this.auth$.next(request.auth)
+ this.preRequestScript$.next(request.preRequestScript)
+ this.testScript$.next(request.testScript)
+ this.body$.next(request.body)
+ }
+
+ getRequest() {
+ return {
+ v: this.v$.value,
+ name: this.name$.value,
+ endpoint: this.endpoint$.value,
+ params: this.params$.value,
+ headers: this.headers$.value,
+ method: this.method$.value,
+ auth: this.auth$.value,
+ preRequestScript: this.preRequestScript$.value,
+ testScript: this.testScript$.value,
+ body: this.body$.value,
+ }
+ }
+
+ resetRequest() {
+ this.v$.next(RESTReqSchemaVersion)
+ this.name$.next("")
+ this.endpoint$.next("")
+ this.params$.next([])
+ this.headers$.next([])
+ this.method$.next("GET")
+ this.auth$.next({
+ authType: "none",
+ authActive: false,
+ })
+ this.preRequestScript$.next("")
+ this.testScript$.next("")
+ this.body$.next({
+ contentType: null,
+ body: null,
+ })
+ }
+}
diff --git a/packages/hoppscotch-common/src/helpers/RequestRunner.ts b/packages/hoppscotch-common/src/helpers/RequestRunner.ts
index 4b6144d81..1ee02cbe4 100644
--- a/packages/hoppscotch-common/src/helpers/RequestRunner.ts
+++ b/packages/hoppscotch-common/src/helpers/RequestRunner.ts
@@ -1,6 +1,6 @@
-import { Observable } from "rxjs"
+import { Observable, Subject } from "rxjs"
import { filter } from "rxjs/operators"
-import { chain, right, TaskEither } from "fp-ts/lib/TaskEither"
+import * as TE from "fp-ts/lib/TaskEither"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -22,7 +22,6 @@ import { createRESTNetworkRequestStream } from "./network"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { isJSONContentType } from "./utils/contenttypes"
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
-import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
import {
environmentsStore,
getCurrentEnvironment,
@@ -31,6 +30,8 @@ import {
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
+import { HoppRESTTab } from "./rest/tab"
+import { Ref } from "vue"
const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" }
@@ -64,20 +65,26 @@ const combineEnvVariables = (env: {
selected: Environment["variables"]
}) => [...env.selected, ...env.global]
-export const runRESTRequest$ = (): TaskEither<
- string | Error,
- Observable
-> =>
+export const executedResponses$ = new Subject<
+ HoppRESTResponse & { type: "success" | "fail " }
+>()
+
+export const runRESTRequest$ = (
+ tab: Ref
+): TE.TaskEither> =>
pipe(
getFinalEnvsFromPreRequest(
- getRESTRequest().preRequestScript,
+ tab.value.document.request.preRequestScript,
getCombinedEnvVariables()
),
- chain((envs) => {
- const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
- name: "Env",
- variables: combineEnvVariables(envs),
- })
+ TE.chain((envs) => {
+ const effectiveRequest = getEffectiveRESTRequest(
+ tab.value.document.request,
+ {
+ name: "Env",
+ variables: combineEnvVariables(envs),
+ }
+ )
const stream = createRESTNetworkRequestStream(effectiveRequest)
@@ -86,6 +93,11 @@ export const runRESTRequest$ = (): TaskEither<
.pipe(filter((res) => res.type === "success" || res.type === "fail"))
.subscribe(async (res) => {
if (res.type === "success" || res.type === "fail") {
+ executedResponses$.next(
+ // @ts-expect-error Typescript can't figure out this inference for some reason
+ res
+ )
+
const runResult = await runTestScript(res.req.testScript, envs, {
status: res.statusCode,
body: getTestableBody(res),
@@ -93,7 +105,9 @@ export const runRESTRequest$ = (): TaskEither<
})()
if (isRight(runResult)) {
- setRESTTestResults(translateToSandboxTestResults(runResult.right))
+ tab.value.testResults = translateToSandboxTestResults(
+ runResult.right
+ )
setGlobalEnvVariables(runResult.right.envs.global)
@@ -128,7 +142,7 @@ export const runRESTRequest$ = (): TaskEither<
)()
}
} else {
- setRESTTestResults({
+ tab.value.testResults = {
description: "",
expectResults: [],
tests: [],
@@ -145,14 +159,14 @@ export const runRESTRequest$ = (): TaskEither<
},
},
scriptError: true,
- })
+ }
}
subscription.unsubscribe()
}
})
- return right(stream)
+ return TE.right(stream)
})
)
diff --git a/packages/hoppscotch-common/src/helpers/collection/affectedIndex.ts b/packages/hoppscotch-common/src/helpers/collection/affectedIndex.ts
new file mode 100644
index 000000000..aa354e462
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/collection/affectedIndex.ts
@@ -0,0 +1,21 @@
+/**
+ * Get the indexes that are affected by the reorder
+ * @param from index of the item before reorder
+ * @param to index of the item after reorder
+ * @returns Map of from to to
+ */
+
+export function getAffectedIndexes(from: number, to: number) {
+ const indexes = new Map()
+ indexes.set(from, to)
+ if (from < to) {
+ for (let i = from + 1; i <= to; i++) {
+ indexes.set(i, i - 1)
+ }
+ } else {
+ for (let i = from - 1; i >= to; i--) {
+ indexes.set(i, i + 1)
+ }
+ }
+ return indexes
+}
diff --git a/packages/hoppscotch-common/src/helpers/collection/collection.ts b/packages/hoppscotch-common/src/helpers/collection/collection.ts
new file mode 100644
index 000000000..31de08e72
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/collection/collection.ts
@@ -0,0 +1,141 @@
+import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
+import { getTabsRefTo } from "../rest/tab"
+import { getAffectedIndexes } from "./affectedIndex"
+
+/**
+ * Resolve save context on reorder
+ * @param payload
+ * @param payload.lastIndex
+ * @param payload.newIndex
+ * @param folderPath
+ * @param payload.length
+ * @returns
+ */
+
+export function resolveSaveContextOnCollectionReorder(
+ payload: {
+ lastIndex: number
+ newIndex: number
+ folderPath: string
+ length?: number // better way to do this? now it could be undefined
+ },
+ type: "remove" | "drop" = "remove"
+) {
+ const { lastIndex, folderPath, length } = payload
+ let { newIndex } = payload
+
+ if (newIndex > lastIndex) newIndex-- // there is a issue when going down? better way to resolve this?
+ if (lastIndex === newIndex) return
+
+ const affectedIndexes = getAffectedIndexes(
+ lastIndex,
+ newIndex === -1 ? length! : newIndex
+ )
+
+ if (newIndex === -1) {
+ // if (newIndex === -1) remove it from the map because it will be deleted
+ affectedIndexes.delete(lastIndex)
+ // when collection deleted opended requests from that collection be affected
+ if (type === "remove") {
+ resetSaveContextForAffectedRequests(
+ folderPath ? `${folderPath}/${lastIndex}` : lastIndex.toString()
+ )
+ }
+ }
+
+ // add folder path as prefix to the affected indexes
+ const affectedPaths = new Map()
+ for (const [key, value] of affectedIndexes) {
+ if (folderPath) {
+ affectedPaths.set(`${folderPath}/${key}`, `${folderPath}/${value}`)
+ } else {
+ affectedPaths.set(key.toString(), value.toString())
+ }
+ }
+
+ const tabs = getTabsRefTo((tab) => {
+ return (
+ tab.document.saveContext?.originLocation === "user-collection" &&
+ affectedPaths.has(tab.document.saveContext.folderPath)
+ )
+ })
+
+ for (const tab of tabs) {
+ if (tab.value.document.saveContext?.originLocation === "user-collection") {
+ const newPath = affectedPaths.get(
+ tab.value.document.saveContext?.folderPath
+ )!
+ tab.value.document.saveContext.folderPath = newPath
+ }
+ }
+}
+
+/**
+ * Resolve save context for affected requests on drop folder from one to another
+ * @param oldFolderPath
+ * @param newFolderPath
+ * @returns
+ */
+
+export function updateSaveContextForAffectedRequests(
+ oldFolderPath: string,
+ newFolderPath: string
+) {
+ const tabs = getTabsRefTo((tab) => {
+ return (
+ tab.document.saveContext?.originLocation === "user-collection" &&
+ tab.document.saveContext.folderPath.startsWith(oldFolderPath)
+ )
+ })
+
+ for (const tab of tabs) {
+ if (tab.value.document.saveContext?.originLocation === "user-collection") {
+ tab.value.document.saveContext = {
+ ...tab.value.document.saveContext,
+ folderPath: tab.value.document.saveContext.folderPath.replace(
+ oldFolderPath,
+ newFolderPath
+ ),
+ }
+ }
+ }
+}
+
+function resetSaveContextForAffectedRequests(folderPath: string) {
+ const tabs = getTabsRefTo((tab) => {
+ return (
+ tab.document.saveContext?.originLocation === "user-collection" &&
+ tab.document.saveContext.folderPath.startsWith(folderPath)
+ )
+ })
+
+ for (const tab of tabs) {
+ tab.value.document.saveContext = null
+ tab.value.document.isDirty = true
+ }
+}
+
+export function getFoldersByPath(
+ collections: HoppCollection[],
+ path: string
+): HoppCollection[] {
+ if (!path) return collections
+
+ // path will be like this "0/0/1" these are the indexes of the folders
+ const pathArray = path.split("/").map((index) => parseInt(index))
+
+ console.log(pathArray, collections[pathArray[0]])
+
+ let currentCollection = collections[pathArray[0]]
+
+ if (pathArray.length === 1) {
+ return currentCollection.folders
+ } else {
+ for (let i = 1; i < pathArray.length; i++) {
+ const folder = currentCollection.folders[pathArray[i]]
+ if (folder) currentCollection = folder
+ }
+ }
+
+ return currentCollection.folders
+}
diff --git a/packages/hoppscotch-common/src/helpers/collection/request.ts b/packages/hoppscotch-common/src/helpers/collection/request.ts
new file mode 100644
index 000000000..a4889c9b4
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/collection/request.ts
@@ -0,0 +1,72 @@
+import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
+import { getTabsRefTo } from "../rest/tab"
+import { getAffectedIndexes } from "./affectedIndex"
+
+/**
+ * Resolve save context on reorder
+ * @param payload
+ * @param payload.lastIndex
+ * @param payload.newIndex
+ * @param payload.folderPath
+ * @param payload.length
+ * @returns
+ */
+
+export function resolveSaveContextOnRequestReorder(payload: {
+ lastIndex: number
+ folderPath: string
+ newIndex: number
+ length?: number // better way to do this? now it could be undefined
+}) {
+ const { lastIndex, folderPath, length } = payload
+ let { newIndex } = payload
+
+ if (newIndex > lastIndex) newIndex-- // there is a issue when going down? better way to resolve this?
+ if (lastIndex === newIndex) return
+
+ const affectedIndexes = getAffectedIndexes(
+ lastIndex,
+ newIndex === -1 ? length! : newIndex
+ )
+
+ // if (newIndex === -1) remove it from the map because it will be deleted
+ if (newIndex === -1) affectedIndexes.delete(lastIndex)
+
+ const tabs = getTabsRefTo((tab) => {
+ return (
+ tab.document.saveContext?.originLocation === "user-collection" &&
+ tab.document.saveContext.folderPath === folderPath &&
+ affectedIndexes.has(tab.document.saveContext.requestIndex)
+ )
+ })
+
+ for (const tab of tabs) {
+ if (tab.value.document.saveContext?.originLocation === "user-collection") {
+ const newIndex = affectedIndexes.get(
+ tab.value.document.saveContext?.requestIndex
+ )!
+ tab.value.document.saveContext.requestIndex = newIndex
+ }
+ }
+}
+
+export function getRequestsByPath(
+ collections: HoppCollection[],
+ path: string
+): HoppRESTRequest[] {
+ // path will be like this "0/0/1" these are the indexes of the folders
+ const pathArray = path.split("/").map((index) => parseInt(index))
+
+ let currentCollection = collections[pathArray[0]]
+
+ if (pathArray.length === 1) {
+ return currentCollection.requests
+ } else {
+ for (let i = 1; i < pathArray.length; i++) {
+ const folder = currentCollection.folders[pathArray[i]]
+ if (folder) currentCollection = folder
+ }
+ }
+
+ return currentCollection.requests
+}
diff --git a/packages/hoppscotch-common/src/helpers/curl/curlparser.ts b/packages/hoppscotch-common/src/helpers/curl/curlparser.ts
index b6985d7b9..155923eee 100644
--- a/packages/hoppscotch-common/src/helpers/curl/curlparser.ts
+++ b/packages/hoppscotch-common/src/helpers/curl/curlparser.ts
@@ -20,7 +20,7 @@ import { getMethod } from "./sub_helpers/method"
import { concatParams, getURLObject } from "./sub_helpers/url"
import { preProcessCurlCommand } from "./sub_helpers/preproc"
import { getBody, getFArgumentMultipartData } from "./sub_helpers/body"
-import { getDefaultRESTRequest } from "~/newstore/RESTSession"
+import { getDefaultRESTRequest } from "../rest/default"
import {
objHasProperty,
objHasArrayProperty,
diff --git a/packages/hoppscotch-common/src/helpers/curl/sub_helpers/auth.ts b/packages/hoppscotch-common/src/helpers/curl/sub_helpers/auth.ts
index 4b29da063..77a234221 100644
--- a/packages/hoppscotch-common/src/helpers/curl/sub_helpers/auth.ts
+++ b/packages/hoppscotch-common/src/helpers/curl/sub_helpers/auth.ts
@@ -3,7 +3,7 @@ import parser from "yargs-parser"
import * as O from "fp-ts/Option"
import * as S from "fp-ts/string"
import { pipe } from "fp-ts/function"
-import { getDefaultRESTRequest } from "~/newstore/RESTSession"
+import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { objHasProperty } from "~/helpers/functional/object"
const defaultRESTReq = getDefaultRESTRequest()
diff --git a/packages/hoppscotch-common/src/helpers/curl/sub_helpers/method.ts b/packages/hoppscotch-common/src/helpers/curl/sub_helpers/method.ts
index ba0e4f274..4b43a0e77 100644
--- a/packages/hoppscotch-common/src/helpers/curl/sub_helpers/method.ts
+++ b/packages/hoppscotch-common/src/helpers/curl/sub_helpers/method.ts
@@ -2,7 +2,7 @@ import parser from "yargs-parser"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as R from "fp-ts/Refinement"
-import { getDefaultRESTRequest } from "~/newstore/RESTSession"
+import { getDefaultRESTRequest } from "~/helpers/rest/default"
import {
objHasProperty,
objHasArrayProperty,
diff --git a/packages/hoppscotch-common/src/helpers/curl/sub_helpers/url.ts b/packages/hoppscotch-common/src/helpers/curl/sub_helpers/url.ts
index ef352280f..502072492 100644
--- a/packages/hoppscotch-common/src/helpers/curl/sub_helpers/url.ts
+++ b/packages/hoppscotch-common/src/helpers/curl/sub_helpers/url.ts
@@ -2,7 +2,7 @@ import parser from "yargs-parser"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
-import { getDefaultRESTRequest } from "~/newstore/RESTSession"
+import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { stringArrayJoin } from "~/helpers/functional/array"
const defaultRESTReq = getDefaultRESTRequest()
diff --git a/packages/hoppscotch-common/src/helpers/fb/request.ts b/packages/hoppscotch-common/src/helpers/fb/request.ts
deleted file mode 100644
index 7522d1b38..000000000
--- a/packages/hoppscotch-common/src/helpers/fb/request.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import {
- audit,
- combineLatest,
- distinctUntilChanged,
- EMPTY,
- from,
- map,
- Subscription,
-} from "rxjs"
-import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
-import { cloneDeep } from "lodash-es"
-import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
-import { platform } from "~/platform"
-import { HoppUser } from "~/platform/auth"
-import { restRequest$ } from "~/newstore/RESTSession"
-
-/**
- * Writes a request to a user's firestore sync
- *
- * @param user The user to write to
- * @param request The request to write to the request sync
- */
-function writeCurrentRequest(user: HoppUser, request: HoppRESTRequest) {
- const req = cloneDeep(request)
-
- // Remove FormData entries because those can't be stored on Firestore
- if (req.body.contentType === "multipart/form-data") {
- req.body.body = req.body.body.map((formData) => {
- if (!formData.isFile) return formData
-
- return {
- active: formData.active,
- isFile: false,
- key: formData.key,
- value: "",
- }
- })
- }
- return setDoc(doc(getFirestore(), "users", user.uid, "requests", "rest"), req)
-}
-
-/**
- * Loads the synced request from the firestore sync
- *
- * @returns Fetched request object if exists else null
- */
-export async function loadRequestFromSync(): Promise {
- const currentUser = platform.auth.getCurrentUser()
-
- if (!currentUser)
- throw new Error("Cannot load request from sync without login")
-
- const fbDoc = await getDoc(
- doc(getFirestore(), "users", currentUser.uid, "requests", "rest")
- )
-
- const data = fbDoc.data()
-
- if (!data) return null
- else return translateToNewRequest(data)
-}
-
-/**
- * Performs sync of the REST Request session with Firestore.
- *
- * @returns A subscription to the sync observable stream.
- * Unsubscribe to stop syncing.
- */
-export function startRequestSync(): Subscription {
- const currentUser$ = platform.auth.getCurrentUserStream()
-
- const sub = combineLatest([
- currentUser$,
- restRequest$.pipe(distinctUntilChanged()),
- ])
- .pipe(
- map(([user, request]) =>
- user ? from(writeCurrentRequest(user, request)) : EMPTY
- ),
- audit((x) => x)
- )
- .subscribe(() => {
- // NOTE: This subscription should be kept
- })
-
- return sub
-}
diff --git a/packages/hoppscotch-common/src/helpers/rest/default.ts b/packages/hoppscotch-common/src/helpers/rest/default.ts
new file mode 100644
index 000000000..873ee2d7d
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/rest/default.ts
@@ -0,0 +1,20 @@
+import { HoppRESTRequest, RESTReqSchemaVersion } from "@hoppscotch/data"
+
+export const getDefaultRESTRequest = (): HoppRESTRequest => ({
+ v: RESTReqSchemaVersion,
+ endpoint: "https://echo.hoppscotch.io",
+ name: "Untitled",
+ params: [],
+ headers: [],
+ method: "GET",
+ auth: {
+ authType: "none",
+ authActive: true,
+ },
+ preRequestScript: "",
+ testScript: "",
+ body: {
+ contentType: null,
+ body: null,
+ },
+})
diff --git a/packages/hoppscotch-common/src/helpers/rest/document.ts b/packages/hoppscotch-common/src/helpers/rest/document.ts
new file mode 100644
index 000000000..e80ef8e88
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/rest/document.ts
@@ -0,0 +1,58 @@
+import { HoppRESTRequest } from "@hoppscotch/data"
+
+export type HoppRESTSaveContext =
+ | {
+ /**
+ * The origin source of the request
+ */
+ originLocation: "user-collection"
+ /**
+ * Path to the request folder
+ */
+ folderPath: string
+ /**
+ * Index to the request
+ */
+ requestIndex: number
+ }
+ | {
+ /**
+ * The origin source of the request
+ */
+ originLocation: "team-collection"
+ /**
+ * ID of the request in the team
+ */
+ requestID: string
+ /**
+ * ID of the team
+ */
+ teamID?: string
+ /**
+ * ID of the collection loaded
+ */
+ collectionID?: string
+ }
+ | null
+
+/**
+ * Defines a live 'document' (something that is open and being edited) in the app
+ */
+export type HoppRESTDocument = {
+ /**
+ * The request as it is in the document
+ */
+ request: HoppRESTRequest
+
+ /**
+ * Whether the request has any unsaved changes
+ * (atleast as far as we can say)
+ */
+ isDirty: boolean
+
+ /**
+ * Info about where this request should be saved.
+ * This contains where the request is originated from basically.
+ */
+ saveContext?: HoppRESTSaveContext
+}
diff --git a/packages/hoppscotch-common/src/helpers/rest/labelColoring.ts b/packages/hoppscotch-common/src/helpers/rest/labelColoring.ts
new file mode 100644
index 000000000..6c4f65387
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/rest/labelColoring.ts
@@ -0,0 +1,25 @@
+import { pipe } from "fp-ts/function"
+import * as O from "fp-ts/Option"
+import * as RR from "fp-ts/ReadonlyRecord"
+import { HoppRESTRequest } from "@hoppscotch/data"
+
+export const REQUEST_METHOD_LABEL_COLORS = {
+ get: "text-green-500",
+ post: "text-yellow-500",
+ put: "text-blue-500",
+ delete: "text-red-500",
+ default: "text-gray-500",
+} as const
+
+/**
+ * Returns the label color tailwind class for a request
+ * @param request The HoppRESTRequest object to get the value for
+ * @returns The class value for the given HTTP VERB, if not, a generic verb class
+ */
+export function getMethodLabelColorClassOf(request: HoppRESTRequest) {
+ return pipe(
+ REQUEST_METHOD_LABEL_COLORS,
+ RR.lookup(request.method.toLowerCase()),
+ O.getOrElseW(() => REQUEST_METHOD_LABEL_COLORS.default)
+ )
+}
diff --git a/packages/hoppscotch-common/src/helpers/rest/tab.ts b/packages/hoppscotch-common/src/helpers/rest/tab.ts
new file mode 100644
index 000000000..31829ebb4
--- /dev/null
+++ b/packages/hoppscotch-common/src/helpers/rest/tab.ts
@@ -0,0 +1,199 @@
+import { v4 as uuidV4 } from "uuid"
+import { isEqual } from "lodash-es"
+import { reactive, watch, computed, ref, shallowReadonly } from "vue"
+import { HoppRESTDocument, HoppRESTSaveContext } from "./document"
+import { refWithControl } from "@vueuse/core"
+import { HoppRESTResponse } from "../types/HoppRESTResponse"
+import { getDefaultRESTRequest } from "./default"
+import { HoppTestResult } from "../types/HoppTestResult"
+
+export type HoppRESTTab = {
+ id: string
+ document: HoppRESTDocument
+ response?: HoppRESTResponse | null
+ testResults?: HoppTestResult | null
+}
+
+export type PersistableRESTTabState = {
+ lastActiveTabID: string
+ orderedDocs: Array<{
+ tabID: string
+ doc: HoppRESTDocument
+ }>
+}
+
+export const currentTabID = refWithControl("test", {
+ onBeforeChange(newTabID) {
+ if (!newTabID || !tabMap.has(newTabID)) {
+ console.warn(
+ `Tried to set current tab id to an invalid value. (value: ${newTabID})`
+ )
+
+ // Don't allow change
+ return false
+ }
+ },
+})
+
+const tabMap = reactive(
+ new Map([
+ [
+ "test",
+ {
+ id: "test",
+ document: {
+ request: getDefaultRESTRequest(),
+ isDirty: false,
+ },
+ },
+ ],
+ ])
+)
+const tabOrdering = ref(["test"])
+
+watch(
+ tabOrdering,
+ (newOrdering) => {
+ if (!currentTabID.value || !newOrdering.includes(currentTabID.value)) {
+ currentTabID.value = newOrdering[newOrdering.length - 1] // newOrdering should always be non-empty
+ }
+ },
+ { deep: true }
+)
+
+export const persistableTabState = computed(() => ({
+ lastActiveTabID: currentTabID.value,
+ orderedDocs: tabOrdering.value.map((tabID) => {
+ const tab = tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
+ return {
+ tabID: tab.id,
+ doc: tab.document,
+ }
+ }),
+}))
+
+export const currentActiveTab = computed(() => tabMap.get(currentTabID.value)!) // Guaranteed to not be undefined
+
+// TODO: Mark this unknown and do validations
+export function loadTabsFromPersistedState(data: PersistableRESTTabState) {
+ if (data) {
+ tabMap.clear()
+ tabOrdering.value = []
+
+ for (const doc of data.orderedDocs) {
+ tabMap.set(doc.tabID, {
+ id: doc.tabID,
+ document: doc.doc,
+ })
+
+ tabOrdering.value.push(doc.tabID)
+ }
+
+ currentTabID.value = data.lastActiveTabID
+ }
+}
+
+/**
+ * Returns all the active Tab IDs in order
+ */
+export function getActiveTabs() {
+ return shallowReadonly(
+ computed(() => tabOrdering.value.map((x) => tabMap.get(x)!))
+ )
+}
+
+export function getTabRef(tabID: string) {
+ return computed({
+ get() {
+ const result = tabMap.get(tabID)
+
+ if (result === undefined) throw new Error(`Invalid tab id: ${tabID}`)
+
+ return result
+ },
+ set(value) {
+ return tabMap.set(tabID, value)
+ },
+ })
+}
+
+function generateNewTabID() {
+ while (true) {
+ const id = uuidV4()
+
+ if (!tabMap.has(id)) return id
+ }
+}
+
+export function updateTab(tabUpdate: HoppRESTTab) {
+ if (!tabMap.has(tabUpdate.id)) {
+ console.warn(
+ `Cannot update tab as tab with that tab id does not exist (id: ${tabUpdate.id})`
+ )
+ }
+
+ tabMap.set(tabUpdate.id, tabUpdate)
+}
+
+export function createNewTab(document: HoppRESTDocument, switchToIt = true) {
+ const id = generateNewTabID()
+
+ const tab: HoppRESTTab = { id, document }
+
+ tabMap.set(id, tab)
+ tabOrdering.value.push(id)
+
+ if (switchToIt) {
+ currentTabID.value = id
+ }
+
+ return tab
+}
+
+export function updateTabOrdering(fromIndex: number, toIndex: number) {
+ tabOrdering.value.splice(
+ toIndex,
+ 0,
+ tabOrdering.value.splice(fromIndex, 1)[0]
+ )
+}
+
+export function closeTab(tabID: string) {
+ if (!tabMap.has(tabID)) {
+ console.warn(`Tried to close a tab which does not exist (tab id: ${tabID})`)
+ return
+ }
+
+ if (tabOrdering.value.length === 1) {
+ console.warn(
+ `Tried to close the only tab open, which is not allowed. (tab id: ${tabID})`
+ )
+ return
+ }
+
+ tabOrdering.value.splice(tabOrdering.value.indexOf(tabID), 1)
+
+ tabMap.delete(tabID)
+}
+
+export function getTabRefWithSaveContext(ctx: HoppRESTSaveContext) {
+ for (const tab of tabMap.values()) {
+ // For `team-collection` request id can be considered unique
+ if (ctx && ctx.originLocation === "team-collection") {
+ if (
+ tab.document.saveContext?.originLocation === "team-collection" &&
+ tab.document.saveContext.requestID === ctx.requestID
+ ) {
+ return getTabRef(tab.id)
+ }
+ } else if (isEqual(ctx, tab.document.saveContext)) return getTabRef(tab.id)
+ }
+
+ return null
+}
+
+export function getTabsRefTo(func: (tab: HoppRESTTab) => boolean) {
+ return Array.from(tabMap.values())
+ .filter(func)
+ .map((tab) => getTabRef(tab.id))
+}
diff --git a/packages/hoppscotch-common/src/helpers/types/HoppRequestSaveContext.ts b/packages/hoppscotch-common/src/helpers/types/HoppRequestSaveContext.ts
index 014b76b39..0eddaa9e3 100644
--- a/packages/hoppscotch-common/src/helpers/types/HoppRequestSaveContext.ts
+++ b/packages/hoppscotch-common/src/helpers/types/HoppRequestSaveContext.ts
@@ -4,7 +4,7 @@ import { HoppRESTRequest } from "@hoppscotch/data"
* We use the save context to figure out
* how a loaded request is to be saved.
* These will be set when the request is loaded
- * into the request session (RESTSession)
+ * into the request session
*/
export type HoppRequestSaveContext =
| {
diff --git a/packages/hoppscotch-common/src/newstore/RESTSession.ts b/packages/hoppscotch-common/src/newstore/RESTSession.ts
deleted file mode 100644
index f1e18cf29..000000000
--- a/packages/hoppscotch-common/src/newstore/RESTSession.ts
+++ /dev/null
@@ -1,710 +0,0 @@
-import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
-import { Ref } from "vue"
-import {
- FormDataKeyValue,
- HoppRESTHeader,
- HoppRESTParam,
- HoppRESTReqBody,
- HoppRESTRequest,
- RESTReqSchemaVersion,
- HoppRESTAuth,
- ValidContentTypes,
-} from "@hoppscotch/data"
-import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
-import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
-import { useStream } from "@composables/stream"
-import { HoppTestResult } from "~/helpers/types/HoppTestResult"
-import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
-import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
-
-type RESTSession = {
- request: HoppRESTRequest
- response: HoppRESTResponse | null
- testResults: HoppTestResult | null
- saveContext: HoppRequestSaveContext | null
-}
-
-export const getDefaultRESTRequest = (): HoppRESTRequest => ({
- v: RESTReqSchemaVersion,
- endpoint: "https://echo.hoppscotch.io",
- name: "Untitled request",
- params: [],
- headers: [],
- method: "GET",
- auth: {
- authType: "none",
- authActive: true,
- },
- preRequestScript: "",
- testScript: "",
- body: {
- contentType: null,
- body: null,
- },
-})
-
-const defaultRESTSession: RESTSession = {
- request: getDefaultRESTRequest(),
- response: null,
- testResults: null,
- saveContext: null,
-}
-
-const dispatchers = defineDispatchers({
- setRequest(_: RESTSession, { req }: { req: HoppRESTRequest }) {
- return {
- request: req,
- }
- },
- setRequestName(curr: RESTSession, { newName }: { newName: string }) {
- return {
- request: {
- ...curr.request,
- name: newName,
- },
- }
- },
- setEndpoint(curr: RESTSession, { newEndpoint }: { newEndpoint: string }) {
- return {
- request: {
- ...curr.request,
- endpoint: newEndpoint,
- },
- }
- },
- setParams(curr: RESTSession, { entries }: { entries: HoppRESTParam[] }) {
- return {
- request: {
- ...curr.request,
- params: entries,
- },
- }
- },
- addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
- return {
- request: {
- ...curr.request,
- params: [...curr.request.params, newParam],
- },
- }
- },
- updateParam(
- curr: RESTSession,
- { index, updatedParam }: { index: number; updatedParam: HoppRESTParam }
- ) {
- const newParams = curr.request.params.map((param, i) => {
- if (i === index) return updatedParam
- else return param
- })
-
- return {
- request: {
- ...curr.request,
- params: newParams,
- },
- }
- },
- deleteParam(curr: RESTSession, { index }: { index: number }) {
- const newParams = curr.request.params.filter((_x, i) => i !== index)
-
- return {
- request: {
- ...curr.request,
- params: newParams,
- },
- }
- },
- deleteAllParams(curr: RESTSession, {}) {
- return {
- request: {
- ...curr.request,
- params: [],
- },
- }
- },
- updateMethod(curr: RESTSession, { newMethod }: { newMethod: string }) {
- return {
- request: {
- ...curr.request,
- method: newMethod,
- },
- }
- },
- setHeaders(curr: RESTSession, { entries }: { entries: HoppRESTHeader[] }) {
- return {
- request: {
- ...curr.request,
- headers: entries,
- },
- }
- },
- addHeader(curr: RESTSession, { entry }: { entry: HoppRESTHeader }) {
- return {
- request: {
- ...curr.request,
- headers: [...curr.request.headers, entry],
- },
- }
- },
- updateHeader(
- curr: RESTSession,
- { index, updatedEntry }: { index: number; updatedEntry: HoppRESTHeader }
- ) {
- return {
- request: {
- ...curr.request,
- headers: curr.request.headers.map((header, i) => {
- if (i === index) return updatedEntry
- else return header
- }),
- },
- }
- },
- deleteHeader(curr: RESTSession, { index }: { index: number }) {
- return {
- request: {
- ...curr.request,
- headers: curr.request.headers.filter((_, i) => i !== index),
- },
- }
- },
- deleteAllHeaders(curr: RESTSession, {}) {
- return {
- request: {
- ...curr.request,
- headers: [],
- },
- }
- },
- setAuth(curr: RESTSession, { newAuth }: { newAuth: HoppRESTAuth }) {
- return {
- request: {
- ...curr.request,
- auth: newAuth,
- },
- }
- },
- setPreRequestScript(curr: RESTSession, { newScript }: { newScript: string }) {
- return {
- request: {
- ...curr.request,
- preRequestScript: newScript,
- },
- }
- },
- setTestScript(curr: RESTSession, { newScript }: { newScript: string }) {
- return {
- request: {
- ...curr.request,
- testScript: newScript,
- },
- }
- },
- setContentType(
- curr: RESTSession,
- { newContentType }: { newContentType: ValidContentTypes | null }
- ) {
- // TODO: persist body evenafter switching content typees
- return {
- request: {
- ...curr.request,
- body: applyBodyTransition(curr.request.body, newContentType),
- },
- }
- },
- addFormDataEntry(curr: RESTSession, { entry }: { entry: FormDataKeyValue }) {
- // Only perform update if the current content-type is formdata
- if (curr.request.body.contentType !== "multipart/form-data") return {}
-
- return {
- request: {
- ...curr.request,
- body: {
- contentType: "multipart/form-data",
- body: [...curr.request.body.body, entry],
- },
- },
- }
- },
- deleteFormDataEntry(curr: RESTSession, { index }: { index: number }) {
- // Only perform update if the current content-type is formdata
- if (curr.request.body.contentType !== "multipart/form-data") return {}
-
- return {
- request: {
- ...curr.request,
- body: {
- contentType: "multipart/form-data",
- body: curr.request.body.body.filter((_, i) => i !== index),
- },
- },
- }
- },
- updateFormDataEntry(
- curr: RESTSession,
- { index, entry }: { index: number; entry: FormDataKeyValue }
- ) {
- // Only perform update if the current content-type is formdata
- if (curr.request.body.contentType !== "multipart/form-data") return {}
-
- return {
- request: {
- ...curr.request,
- body: {
- contentType: "multipart/form-data",
- body: curr.request.body.body.map((x, i) => (i !== index ? x : entry)),
- },
- },
- }
- },
- deleteAllFormDataEntries(curr: RESTSession, {}) {
- // Only perform update if the current content-type is formdata
- if (curr.request.body.contentType !== "multipart/form-data") return {}
-
- return {
- request: {
- ...curr.request,
- body: {
- contentType: "multipart/form-data",
- body: [],
- },
- },
- }
- },
- setRequestBody(curr: RESTSession, { newBody }: { newBody: HoppRESTReqBody }) {
- return {
- request: {
- ...curr.request,
- body: newBody,
- },
- }
- },
- updateResponse(
- _curr: RESTSession,
- { updatedRes }: { updatedRes: HoppRESTResponse | null }
- ) {
- return {
- response: updatedRes,
- }
- },
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- clearResponse(_curr: RESTSession, {}) {
- return {
- response: null,
- }
- },
- setTestResults(
- _curr: RESTSession,
- { newResults }: { newResults: HoppTestResult | null }
- ) {
- return {
- testResults: newResults,
- }
- },
- setSaveContext(
- _,
- { newContext }: { newContext: HoppRequestSaveContext | null }
- ) {
- return {
- saveContext: newContext,
- }
- },
-})
-
-const restSessionStore = new DispatchingStore(defaultRESTSession, dispatchers)
-
-export function getRESTRequest() {
- return restSessionStore.subject$.value.request
-}
-
-export function setRESTRequest(
- req: HoppRESTRequest,
- saveContext?: HoppRequestSaveContext | null
-) {
- restSessionStore.dispatch({
- dispatcher: "setRequest",
- payload: {
- req,
- },
- })
-
- if (saveContext) setRESTSaveContext(saveContext)
-}
-
-export function setRESTSaveContext(saveContext: HoppRequestSaveContext | null) {
- restSessionStore.dispatch({
- dispatcher: "setSaveContext",
- payload: {
- newContext: saveContext,
- },
- })
-}
-
-export function getRESTSaveContext() {
- return restSessionStore.value.saveContext
-}
-
-export function resetRESTRequest() {
- setRESTRequest(getDefaultRESTRequest())
-}
-
-export function setRESTEndpoint(newEndpoint: string) {
- restSessionStore.dispatch({
- dispatcher: "setEndpoint",
- payload: {
- newEndpoint,
- },
- })
-}
-
-export function setRESTRequestName(newName: string) {
- restSessionStore.dispatch({
- dispatcher: "setRequestName",
- payload: {
- newName,
- },
- })
-}
-
-export function setRESTParams(entries: HoppRESTParam[]) {
- restSessionStore.dispatch({
- dispatcher: "setParams",
- payload: {
- entries,
- },
- })
-}
-
-export function addRESTParam(newParam: HoppRESTParam) {
- restSessionStore.dispatch({
- dispatcher: "addParam",
- payload: {
- newParam,
- },
- })
-}
-
-export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
- restSessionStore.dispatch({
- dispatcher: "updateParam",
- payload: {
- updatedParam,
- index,
- },
- })
-}
-
-export function deleteRESTParam(index: number) {
- restSessionStore.dispatch({
- dispatcher: "deleteParam",
- payload: {
- index,
- },
- })
-}
-
-export function deleteAllRESTParams() {
- restSessionStore.dispatch({
- dispatcher: "deleteAllParams",
- payload: {},
- })
-}
-
-export function updateRESTMethod(newMethod: string) {
- restSessionStore.dispatch({
- dispatcher: "updateMethod",
- payload: {
- newMethod,
- },
- })
-}
-
-export function setRESTHeaders(entries: HoppRESTHeader[]) {
- restSessionStore.dispatch({
- dispatcher: "setHeaders",
- payload: {
- entries,
- },
- })
-}
-
-export function addRESTHeader(entry: HoppRESTHeader) {
- restSessionStore.dispatch({
- dispatcher: "addHeader",
- payload: {
- entry,
- },
- })
-}
-
-export function updateRESTHeader(index: number, updatedEntry: HoppRESTHeader) {
- restSessionStore.dispatch({
- dispatcher: "updateHeader",
- payload: {
- index,
- updatedEntry,
- },
- })
-}
-
-export function deleteRESTHeader(index: number) {
- restSessionStore.dispatch({
- dispatcher: "deleteHeader",
- payload: {
- index,
- },
- })
-}
-
-export function deleteAllRESTHeaders() {
- restSessionStore.dispatch({
- dispatcher: "deleteAllHeaders",
- payload: {},
- })
-}
-
-export function setRESTAuth(newAuth: HoppRESTAuth) {
- restSessionStore.dispatch({
- dispatcher: "setAuth",
- payload: {
- newAuth,
- },
- })
-}
-
-export function setRESTPreRequestScript(newScript: string) {
- restSessionStore.dispatch({
- dispatcher: "setPreRequestScript",
- payload: {
- newScript,
- },
- })
-}
-
-export function setRESTTestScript(newScript: string) {
- restSessionStore.dispatch({
- dispatcher: "setTestScript",
- payload: {
- newScript,
- },
- })
-}
-
-export function setRESTReqBody(newBody: HoppRESTReqBody) {
- restSessionStore.dispatch({
- dispatcher: "setRequestBody",
- payload: {
- newBody,
- },
- })
-}
-
-export function updateRESTResponse(updatedRes: HoppRESTResponse | null) {
- restSessionStore.dispatch({
- dispatcher: "updateResponse",
- payload: {
- updatedRes,
- },
- })
-}
-
-export function clearRESTResponse() {
- restSessionStore.dispatch({
- dispatcher: "clearResponse",
- payload: {},
- })
-}
-
-export function setRESTTestResults(newResults: HoppTestResult | null) {
- restSessionStore.dispatch({
- dispatcher: "setTestResults",
- payload: {
- newResults,
- },
- })
-}
-
-export function addFormDataEntry(entry: FormDataKeyValue) {
- restSessionStore.dispatch({
- dispatcher: "addFormDataEntry",
- payload: {
- entry,
- },
- })
-}
-
-export function deleteFormDataEntry(index: number) {
- restSessionStore.dispatch({
- dispatcher: "deleteFormDataEntry",
- payload: {
- index,
- },
- })
-}
-
-export function updateFormDataEntry(index: number, entry: FormDataKeyValue) {
- restSessionStore.dispatch({
- dispatcher: "updateFormDataEntry",
- payload: {
- index,
- entry,
- },
- })
-}
-
-export function setRESTContentType(newContentType: ValidContentTypes | null) {
- restSessionStore.dispatch({
- dispatcher: "setContentType",
- payload: {
- newContentType,
- },
- })
-}
-
-export function deleteAllFormDataEntries() {
- restSessionStore.dispatch({
- dispatcher: "deleteAllFormDataEntries",
- payload: {},
- })
-}
-
-export const restSaveContext$ = restSessionStore.subject$.pipe(
- pluck("saveContext"),
- distinctUntilChanged()
-)
-
-export const restRequest$ = restSessionStore.subject$.pipe(
- pluck("request"),
- distinctUntilChanged()
-)
-
-export const restRequestName$ = restRequest$.pipe(
- pluck("name"),
- distinctUntilChanged()
-)
-
-export const restEndpoint$ = restSessionStore.subject$.pipe(
- pluck("request", "endpoint"),
- distinctUntilChanged()
-)
-
-export const restParams$ = restSessionStore.subject$.pipe(
- pluck("request", "params"),
- distinctUntilChanged()
-)
-
-export const restActiveParamsCount$ = restParams$.pipe(
- map(
- (params) =>
- params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
- )
-)
-
-export const restMethod$ = restSessionStore.subject$.pipe(
- pluck("request", "method"),
- distinctUntilChanged()
-)
-
-export const restHeaders$ = restSessionStore.subject$.pipe(
- pluck("request", "headers"),
- distinctUntilChanged()
-)
-
-export const restActiveHeadersCount$ = restHeaders$.pipe(
- map(
- (params) =>
- params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
- )
-)
-
-export const restAuth$ = restRequest$.pipe(pluck("auth"))
-
-export const restPreRequestScript$ = restSessionStore.subject$.pipe(
- pluck("request", "preRequestScript"),
- distinctUntilChanged()
-)
-
-export const restContentType$ = restRequest$.pipe(
- pluck("body", "contentType"),
- distinctUntilChanged()
-)
-
-export const restTestScript$ = restSessionStore.subject$.pipe(
- pluck("request", "testScript"),
- distinctUntilChanged()
-)
-
-export const restReqBody$ = restSessionStore.subject$.pipe(
- pluck("request", "body"),
- distinctUntilChanged()
-)
-
-export const restResponse$ = restSessionStore.subject$.pipe(
- pluck("response"),
- distinctUntilChanged()
-)
-
-export const completedRESTResponse$ = restResponse$.pipe(
- filter(
- (res) =>
- res !== null &&
- res.type !== "loading" &&
- res.type !== "network_fail" &&
- res.type !== "script_fail"
- )
-)
-
-export const restTestResults$ = restSessionStore.subject$.pipe(
- pluck("testResults"),
- distinctUntilChanged()
-)
-
-/**
- * A Vue 3 composable function that gives access to a ref
- * which is updated to the preRequestScript value in the store.
- * The ref value is kept in sync with the store and all writes
- * to the ref are dispatched to the store as `setPreRequestScript`
- * dispatches.
- */
-export function usePreRequestScript(): Ref {
- return useStream(
- restPreRequestScript$,
- restSessionStore.value.request.preRequestScript,
- (value) => {
- setRESTPreRequestScript(value)
- }
- )
-}
-
-/**
- * A Vue 3 composable function that gives access to a ref
- * which is updated to the testScript value in the store.
- * The ref value is kept in sync with the store and all writes
- * to the ref are dispatched to the store as `setTestScript`
- * dispatches.
- */
-export function useTestScript(): Ref {
- return useStream(
- restTestScript$,
- restSessionStore.value.request.testScript,
- (value) => {
- setRESTTestScript(value)
- }
- )
-}
-
-export function useRESTRequestBody(): Ref {
- return useStream(
- restReqBody$,
- restSessionStore.value.request.body,
- setRESTReqBody
- )
-}
-
-export function useRESTRequestName(): Ref {
- return useStream(
- restRequestName$,
- restSessionStore.value.request.name,
- setRESTRequestName
- )
-}
diff --git a/packages/hoppscotch-common/src/newstore/collections.ts b/packages/hoppscotch-common/src/newstore/collections.ts
index db4b23cc5..63870627d 100644
--- a/packages/hoppscotch-common/src/newstore/collections.ts
+++ b/packages/hoppscotch-common/src/newstore/collections.ts
@@ -6,8 +6,8 @@ import {
makeCollection,
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
-import { getRESTSaveContext, setRESTSaveContext } from "./RESTSession"
import { cloneDeep } from "lodash-es"
+import { getTabRefWithSaveContext } from "~/helpers/rest/tab"
const defaultRESTCollectionState = {
state: [
@@ -400,14 +400,17 @@ const restCollectionDispatchers = defineDispatchers({
targetLocation.requests.splice(requestIndex, 1)
- // If the save context is set and is set to the same source, we invalidate it
- const saveCtx = getRESTSaveContext()
- if (
- saveCtx?.originLocation === "user-collection" &&
- saveCtx.folderPath === path &&
- saveCtx.requestIndex === requestIndex
- ) {
- setRESTSaveContext(null)
+ // Deal with situations where a tab with the given thing is deleted
+ // We are just going to dissociate the save context of the tab and mark it dirty
+ const tab = getTabRefWithSaveContext({
+ originLocation: "user-collection",
+ folderPath: path,
+ requestIndex: requestIndex,
+ })
+
+ if (tab) {
+ tab.value.document.saveContext = undefined
+ tab.value.document.isDirty = true
}
return {
@@ -457,6 +460,20 @@ const restCollectionDispatchers = defineDispatchers({
destLocation.requests.push(req)
targetLocation.requests.splice(requestIndex, 1)
+ const possibleTab = getTabRefWithSaveContext({
+ originLocation: "user-collection",
+ folderPath: path,
+ requestIndex,
+ })
+
+ if (possibleTab) {
+ possibleTab.value.document.saveContext = {
+ originLocation: "user-collection",
+ folderPath: destinationPath,
+ requestIndex: destLocation.requests.length - 1,
+ }
+ }
+
return {
state: newState,
}
@@ -719,16 +736,6 @@ const gqlCollectionDispatchers = defineDispatchers({
targetLocation.requests.splice(requestIndex, 1)
- // If the save context is set and is set to the same source, we invalidate it
- const saveCtx = getRESTSaveContext()
- if (
- saveCtx?.originLocation === "user-collection" &&
- saveCtx.folderPath === path &&
- saveCtx.requestIndex === requestIndex
- ) {
- setRESTSaveContext(null)
- }
-
return {
state: newState,
}
diff --git a/packages/hoppscotch-common/src/newstore/history.ts b/packages/hoppscotch-common/src/newstore/history.ts
index e4e5d53d9..1f5518221 100644
--- a/packages/hoppscotch-common/src/newstore/history.ts
+++ b/packages/hoppscotch-common/src/newstore/history.ts
@@ -8,7 +8,7 @@ import {
GQL_REQ_SCHEMA_VERSION,
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
-import { completedRESTResponse$ } from "./RESTSession"
+import { executedResponses$ } from "~/helpers/RequestRunner"
export type RESTHistoryEntry = {
v: number
@@ -340,36 +340,27 @@ export function removeDuplicateGraphqlHistoryEntry(id: string) {
}
// Listen to completed responses to add to history
-completedRESTResponse$.subscribe((res) => {
- if (res !== null) {
- if (
- res.type === "loading" ||
- res.type === "network_fail" ||
- res.type === "script_fail"
- )
- return
-
- addRESTHistoryEntry(
- makeRESTHistoryEntry({
- request: {
- auth: res.req.auth,
- body: res.req.body,
- endpoint: res.req.endpoint,
- headers: res.req.headers,
- method: res.req.method,
- name: res.req.name,
- params: res.req.params,
- preRequestScript: res.req.preRequestScript,
- testScript: res.req.testScript,
- v: res.req.v,
- },
- responseMeta: {
- duration: res.meta.responseDuration,
- statusCode: res.statusCode,
- },
- star: false,
- updatedOn: new Date(),
- })
- )
- }
+executedResponses$.subscribe((res) => {
+ addRESTHistoryEntry(
+ makeRESTHistoryEntry({
+ request: {
+ auth: res.req.auth,
+ body: res.req.body,
+ endpoint: res.req.endpoint,
+ headers: res.req.headers,
+ method: res.req.method,
+ name: res.req.name,
+ params: res.req.params,
+ preRequestScript: res.req.preRequestScript,
+ testScript: res.req.testScript,
+ v: res.req.v,
+ },
+ responseMeta: {
+ duration: res.meta.responseDuration,
+ statusCode: res.statusCode,
+ },
+ star: false,
+ updatedOn: new Date(),
+ })
+ )
})
diff --git a/packages/hoppscotch-common/src/newstore/localpersistence.ts b/packages/hoppscotch-common/src/newstore/localpersistence.ts
index cfde49425..40eec65cd 100644
--- a/packages/hoppscotch-common/src/newstore/localpersistence.ts
+++ b/packages/hoppscotch-common/src/newstore/localpersistence.ts
@@ -1,11 +1,9 @@
/* eslint-disable no-restricted-globals, no-restricted-syntax */
-import { clone, cloneDeep, assign, isEmpty } from "lodash-es"
+import { clone, assign, isEmpty } from "lodash-es"
import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/function"
import {
- safelyExtractRESTRequest,
- translateToNewRequest,
translateToNewRESTCollection,
translateToNewGQLCollection,
Environment,
@@ -41,17 +39,16 @@ import {
setSelectedEnvironmentIndex,
selectedEnvironmentIndex$,
} from "./environments"
-import {
- getDefaultRESTRequest,
- restRequest$,
- setRESTRequest,
-} from "./RESTSession"
import { WSRequest$, setWSRequest } from "./WebSocketSession"
import { SIORequest$, setSIORequest } from "./SocketIOSession"
import { SSERequest$, setSSERequest } from "./SSESession"
import { MQTTRequest$, setMQTTRequest } from "./MQTTSession"
import { bulkApplyLocalState, localStateStore } from "./localstate"
-import { StorageLike } from "@vueuse/core"
+import { StorageLike, watchDebounced } from "@vueuse/core"
+import {
+ loadTabsFromPersistedState,
+ persistableTabState,
+} from "~/helpers/rest/tab"
function checkAndMigrateOldSettings() {
const vuexData = JSON.parse(window.localStorage.getItem("vuex") || "{}")
@@ -305,33 +302,28 @@ function setupGlobalEnvsPersistence() {
})
}
-function setupRequestPersistence() {
- const localRequest = JSON.parse(
- window.localStorage.getItem("restRequest") || "null"
- )
-
- if (localRequest) {
- const parsedLocal = translateToNewRequest(localRequest)
- setRESTRequest(
- safelyExtractRESTRequest(parsedLocal, getDefaultRESTRequest())
+// TODO: Graceful error handling ?
+export function setupRESTTabsPersistence() {
+ try {
+ const state = window.localStorage.getItem("restTabState")
+ if (state) {
+ const data = JSON.parse(state)
+ loadTabsFromPersistedState(data)
+ }
+ } catch (e) {
+ console.error(
+ `Failed parsing persisted tab state, state:`,
+ window.localStorage.getItem("restTabState")
)
}
- restRequest$.subscribe((req) => {
- const reqClone = cloneDeep(req)
- if (reqClone.body.contentType === "multipart/form-data") {
- reqClone.body.body = reqClone.body.body.map((x) => {
- if (x.isFile)
- return {
- ...x,
- isFile: false,
- value: "",
- }
- else return x
- })
- }
- window.localStorage.setItem("restRequest", JSON.stringify(reqClone))
- })
+ watchDebounced(
+ persistableTabState,
+ (state) => {
+ window.localStorage.setItem("restTabState", JSON.stringify(state))
+ },
+ { debounce: 500, deep: true }
+ )
}
export function setupLocalPersistence() {
@@ -339,7 +331,7 @@ export function setupLocalPersistence() {
setupLocalStatePersistence()
setupSettingsPersistence()
- setupRequestPersistence()
+ setupRESTTabsPersistence()
setupHistoryPersistence()
setupCollectionsPersistence()
setupGlobalEnvsPersistence()
diff --git a/packages/hoppscotch-common/src/pages/index.vue b/packages/hoppscotch-common/src/pages/index.vue
index 87ec68713..71c40add7 100644
--- a/packages/hoppscotch-common/src/pages/index.vue
+++ b/packages/hoppscotch-common/src/pages/index.vue
@@ -1,51 +1,109 @@
-
-
-
-
-
+
+
+
+
+ {{ tab.document.request.method }}
+
+
+ •
+
+
+ {{ tab.document.request.name }}
+
+
+
+
+
+
+
+
-
diff --git a/packages/hoppscotch-common/src/pages/r/_id.vue b/packages/hoppscotch-common/src/pages/r/_id.vue
index d1faa769f..a810409bb 100644
--- a/packages/hoppscotch-common/src/pages/r/_id.vue
+++ b/packages/hoppscotch-common/src/pages/r/_id.vue
@@ -71,10 +71,11 @@ import {
ResolveShortcodeQuery,
ResolveShortcodeQueryVariables,
} from "~/helpers/backend/graphql"
-import { getDefaultRESTRequest, setRESTRequest } from "~/newstore/RESTSession"
import IconHome from "~icons/lucide/home"
import IconRefreshCW from "~icons/lucide/refresh-cw"
+import { createNewTab } from "~/helpers/rest/tab"
+import { getDefaultRESTRequest } from "~/helpers/rest/default"
export default defineComponent({
setup() {
@@ -106,9 +107,10 @@ export default defineComponent({
data.right.shortcode?.request as string
)
- setRESTRequest(
- safelyExtractRESTRequest(request, getDefaultRESTRequest())
- )
+ createNewTab({
+ request: safelyExtractRESTRequest(request, getDefaultRESTRequest()),
+ isDirty: false,
+ })
router.push({ path: "/" })
}
diff --git a/packages/hoppscotch-common/src/platform/index.ts b/packages/hoppscotch-common/src/platform/index.ts
index 5894d39c8..9c8a1ff08 100644
--- a/packages/hoppscotch-common/src/platform/index.ts
+++ b/packages/hoppscotch-common/src/platform/index.ts
@@ -4,6 +4,7 @@ import { EnvironmentsPlatformDef } from "./environments"
import { CollectionsPlatformDef } from "./collections"
import { SettingsPlatformDef } from "./settings"
import { HistoryPlatformDef } from "./history"
+import { TabStatePlatformDef } from "./tab"
export type PlatformDef = {
ui?: UIPlatformDef
@@ -13,6 +14,7 @@ export type PlatformDef = {
collections: CollectionsPlatformDef
settings: SettingsPlatformDef
history: HistoryPlatformDef
+ tabState: TabStatePlatformDef
}
}
diff --git a/packages/hoppscotch-common/src/platform/tab.ts b/packages/hoppscotch-common/src/platform/tab.ts
new file mode 100644
index 000000000..ea8472fcc
--- /dev/null
+++ b/packages/hoppscotch-common/src/platform/tab.ts
@@ -0,0 +1,10 @@
+import { PersistableRESTTabState } from "~/helpers/rest/tab"
+import { HoppUser } from "./auth"
+
+export type TabStatePlatformDef = {
+ loadTabStateFromSync: () => Promise
+ writeCurrentTabState: (
+ user: HoppUser,
+ persistableTabState: PersistableRESTTabState
+ ) => Promise
+}
diff --git a/packages/hoppscotch-ui/src/components/smart/Window.vue b/packages/hoppscotch-ui/src/components/smart/Window.vue
index ff6a82a63..0e28df2e1 100644
--- a/packages/hoppscotch-ui/src/components/smart/Window.vue
+++ b/packages/hoppscotch-ui/src/components/smart/Window.vue
@@ -1,5 +1,9 @@
-
+
@@ -27,16 +31,33 @@ const props = defineProps({
default: false,
},
})
+
const tabMeta = computed
(() => ({
info: props.info,
label: props.label,
isRemovable: props.isRemovable,
icon: slots.icon,
+ tabhead: slots.tabhead
}))
-const { activeTabID, addTabEntry, updateTabEntry, removeTabEntry } =
- inject("tabs-system")!
+
+const {
+ activeTabID,
+ renderInactive,
+ addTabEntry,
+ updateTabEntry,
+ removeTabEntry,
+} = inject("tabs-system")!
+
const active = computed(() => activeTabID.value === props.id)
+const shouldRender = computed(() => {
+ // If render inactive is true, then it should be rendered nonetheless
+ if (renderInactive.value) return true
+
+ // Else, return whatever is the active state
+ return active.value
+})
+
onMounted(() => {
addTabEntry(props.id, tabMeta.value)
})
diff --git a/packages/hoppscotch-ui/src/components/smart/Windows.vue b/packages/hoppscotch-ui/src/components/smart/Windows.vue
index 15e4cb549..d15c4bbef 100644
--- a/packages/hoppscotch-ui/src/components/smart/Windows.vue
+++ b/packages/hoppscotch-ui/src/components/smart/Windows.vue
@@ -1,8 +1,8 @@
-
-
+
+
@@ -10,16 +10,22 @@