feat: rest revamp (#2918)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com> Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com> Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -138,6 +138,7 @@
|
|||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"exit_team": "Are you sure you want to leave this team?",
|
"exit_team": "Are you sure you want to leave this team?",
|
||||||
|
"save_unsaved_tab": "Do you want to save changes made in this tab ?",
|
||||||
"logout": "Are you sure you want to logout?",
|
"logout": "Are you sure you want to logout?",
|
||||||
"remove_collection": "Are you sure you want to permanently delete this collection?",
|
"remove_collection": "Are you sure you want to permanently delete this collection?",
|
||||||
"remove_environment": "Are you sure you want to permanently delete this environment?",
|
"remove_environment": "Are you sure you want to permanently delete this environment?",
|
||||||
@@ -317,6 +318,7 @@
|
|||||||
"modal": {
|
"modal": {
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
|
"close_unsaved_tab": "Close Unsaved Tab ?",
|
||||||
"edit_request": "Edit Request",
|
"edit_request": "Edit Request",
|
||||||
"import_export": "Import / Export"
|
"import_export": "Import / Export"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,7 +89,11 @@ declare module '@vue/runtime-core' {
|
|||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||||
|
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||||
|
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||||
|
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||||
|
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
||||||
HttpBody: typeof import('./components/http/Body.vue')['default']
|
HttpBody: typeof import('./components/http/Body.vue')['default']
|
||||||
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
|
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
|
||||||
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
|
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
|
||||||
@@ -102,6 +106,7 @@ declare module '@vue/runtime-core' {
|
|||||||
HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
|
HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
|
||||||
HttpRequest: typeof import('./components/http/Request.vue')['default']
|
HttpRequest: typeof import('./components/http/Request.vue')['default']
|
||||||
HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
|
HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
|
||||||
|
HttpRequestTab: typeof import('./components/http/RequestTab.vue')['default']
|
||||||
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
||||||
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
||||||
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<slot name="primary" />
|
<slot name="primary" />
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane
|
<Pane
|
||||||
|
v-if="hasSecondary"
|
||||||
:size="PANE_MAIN_BOTTOM_SIZE"
|
:size="PANE_MAIN_BOTTOM_SIZE"
|
||||||
class="flex flex-col !overflow-auto"
|
class="flex flex-col !overflow-auto"
|
||||||
>
|
>
|
||||||
@@ -62,6 +63,7 @@ const SIDEBAR = useSetting("SIDEBAR")
|
|||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
const hasSidebar = computed(() => !!slots.sidebar)
|
const hasSidebar = computed(() => !!slots.sidebar)
|
||||||
|
const hasSecondary = computed(() => !!slots.secondary)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
layoutId: {
|
layoutId: {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
import { ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { getRESTRequest } from "~/newstore/RESTSession"
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -70,7 +70,7 @@ watch(
|
|||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
name.value = getRESTRequest().name
|
name.value = currentActiveTab.value.document.request.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ const props = defineProps({
|
|||||||
parentID: {
|
parentID: {
|
||||||
type: String as PropType<string | null>,
|
type: String as PropType<string | null>,
|
||||||
default: null,
|
default: null,
|
||||||
required: true,
|
required: false,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,
|
type: Object as PropType<HoppCollection<HoppRESTRequest> | TeamCollection>,
|
||||||
|
|||||||
@@ -298,11 +298,10 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
|||||||
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { useReadonlyStream } from "~/composables/stream"
|
|
||||||
import { restSaveContext$ } from "~/newstore/RESTSession"
|
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked.js"
|
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
export type Collection = {
|
export type Collection = {
|
||||||
type: "collections"
|
type: "collections"
|
||||||
@@ -508,10 +507,9 @@ const isSelected = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const active = useReadonlyStream(restSaveContext$, null)
|
const active = computed(() => currentActiveTab.value.document.saveContext)
|
||||||
|
|
||||||
const isActiveRequest = computed(() => {
|
const isActiveRequest = (folderPath: string, requestIndex: number) => {
|
||||||
return (folderPath: string, requestIndex: number) => {
|
|
||||||
return pipe(
|
return pipe(
|
||||||
active.value,
|
active.value,
|
||||||
O.fromNullable,
|
O.fromNullable,
|
||||||
@@ -523,8 +521,7 @@ const isActiveRequest = computed(() => {
|
|||||||
),
|
),
|
||||||
O.isSome
|
O.isSome
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const selectRequest = (data: {
|
const selectRequest = (data: {
|
||||||
request: HoppRESTRequest
|
request: HoppRESTRequest
|
||||||
@@ -532,6 +529,7 @@ const selectRequest = (data: {
|
|||||||
requestIndex: string
|
requestIndex: string
|
||||||
}) => {
|
}) => {
|
||||||
const { request, folderPath, requestIndex } = data
|
const { request, folderPath, requestIndex } = data
|
||||||
|
|
||||||
if (props.saveRequest) {
|
if (props.saveRequest) {
|
||||||
emit("select", {
|
emit("select", {
|
||||||
pickedType: "my-request",
|
pickedType: "my-request",
|
||||||
@@ -543,7 +541,7 @@ const selectRequest = (data: {
|
|||||||
request,
|
request,
|
||||||
folderPath,
|
folderPath,
|
||||||
requestIndex,
|
requestIndex,
|
||||||
isActive: isActiveRequest.value(folderPath, parseInt(requestIndex)),
|
isActive: isActiveRequest(folderPath, parseInt(requestIndex)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,14 +152,12 @@ import { ref, PropType, watch, computed } from "vue"
|
|||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as RR from "fp-ts/ReadonlyRecord"
|
|
||||||
import * as O from "fp-ts/Option"
|
|
||||||
import {
|
import {
|
||||||
changeCurrentReorderStatus,
|
changeCurrentReorderStatus,
|
||||||
currentReorderingStatus$,
|
currentReorderingStatus$,
|
||||||
} from "~/newstore/reordering"
|
} from "~/newstore/reordering"
|
||||||
import { useReadonlyStream } from "~/composables/stream"
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||||
|
|
||||||
type CollectionType = "my-collections" | "team-collections"
|
type CollectionType = "my-collections" | "team-collections"
|
||||||
|
|
||||||
@@ -242,20 +240,8 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
|||||||
parentID: "",
|
parentID: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestMethodLabels = {
|
|
||||||
get: "text-green-500",
|
|
||||||
post: "text-yellow-500",
|
|
||||||
put: "text-blue-500",
|
|
||||||
delete: "text-red-500",
|
|
||||||
default: "text-gray-500",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const requestLabelColor = computed(() =>
|
const requestLabelColor = computed(() =>
|
||||||
pipe(
|
getMethodLabelColorClassOf(props.request)
|
||||||
requestMethodLabels,
|
|
||||||
RR.lookup(props.request.method.toLowerCase()),
|
|
||||||
O.getOrElseW(() => requestMethodLabels.default)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable prettier/prettier -->
|
||||||
<template>
|
<template>
|
||||||
<HoppSmartModal
|
<HoppSmartModal
|
||||||
v-if="show"
|
v-if="show"
|
||||||
@@ -61,8 +62,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from "@composables/i18n"
|
import { reactive, ref, watch } from "vue"
|
||||||
import { useToast } from "@composables/toast"
|
import { cloneDeep } from "lodash-es"
|
||||||
import {
|
import {
|
||||||
HoppGQLRequest,
|
HoppGQLRequest,
|
||||||
HoppRESTRequest,
|
HoppRESTRequest,
|
||||||
@@ -70,8 +71,6 @@ import {
|
|||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
import { cloneDeep } from "lodash-es"
|
|
||||||
import { reactive, ref, watch } from "vue"
|
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
import {
|
import {
|
||||||
createRequestInCollection,
|
createRequestInCollection,
|
||||||
@@ -79,11 +78,8 @@ import {
|
|||||||
} from "~/helpers/backend/mutations/TeamRequest"
|
} from "~/helpers/backend/mutations/TeamRequest"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
|
||||||
import {
|
import { useI18n } from "@composables/i18n"
|
||||||
getRESTRequest,
|
import { useToast } from "@composables/toast"
|
||||||
setRESTSaveContext,
|
|
||||||
useRESTRequestName,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import {
|
import {
|
||||||
editGraphqlRequest,
|
editGraphqlRequest,
|
||||||
editRESTRequest,
|
editRESTRequest,
|
||||||
@@ -91,6 +87,8 @@ import {
|
|||||||
saveRESTRequestAs,
|
saveRESTRequestAs,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
|
import { computedWithControl } from "@vueuse/core"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -127,8 +125,13 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const requestName = ref(
|
const gqlRequestName = useGQLRequestName()
|
||||||
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName()
|
const requestName = computedWithControl(
|
||||||
|
() => [currentActiveTab.value, gqlRequestName.value],
|
||||||
|
() =>
|
||||||
|
props.mode === "rest"
|
||||||
|
? currentActiveTab.value.document.request.name
|
||||||
|
: gqlRequestName.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const requestData = reactive({
|
const requestData = reactive({
|
||||||
@@ -186,7 +189,7 @@ const saveRequestAs = async () => {
|
|||||||
|
|
||||||
const requestUpdated =
|
const requestUpdated =
|
||||||
props.mode === "rest"
|
props.mode === "rest"
|
||||||
? cloneDeep(getRESTRequest())
|
? cloneDeep(currentActiveTab.value.document.request)
|
||||||
: cloneDeep(getGQLSession().request)
|
: cloneDeep(getGQLSession().request)
|
||||||
|
|
||||||
if (picked.value.pickedType === "my-collection") {
|
if (picked.value.pickedType === "my-collection") {
|
||||||
@@ -198,12 +201,15 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
setRESTSaveContext({
|
currentActiveTab.value.document = {
|
||||||
|
request: requestUpdated,
|
||||||
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: `${picked.value.collectionIndex}`,
|
folderPath: `${picked.value.collectionIndex}`,
|
||||||
requestIndex: insertionIndex,
|
requestIndex: insertionIndex,
|
||||||
req: requestUpdated,
|
},
|
||||||
})
|
}
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "my-folder") {
|
} else if (picked.value.pickedType === "my-folder") {
|
||||||
@@ -215,12 +221,15 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
setRESTSaveContext({
|
currentActiveTab.value.document = {
|
||||||
|
request: requestUpdated,
|
||||||
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: picked.value.folderPath,
|
folderPath: picked.value.folderPath,
|
||||||
requestIndex: insertionIndex,
|
requestIndex: insertionIndex,
|
||||||
req: requestUpdated,
|
},
|
||||||
})
|
}
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "my-request") {
|
} else if (picked.value.pickedType === "my-request") {
|
||||||
@@ -233,12 +242,15 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated
|
requestUpdated
|
||||||
)
|
)
|
||||||
|
|
||||||
setRESTSaveContext({
|
currentActiveTab.value.document = {
|
||||||
|
request: requestUpdated,
|
||||||
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: picked.value.folderPath,
|
folderPath: picked.value.folderPath,
|
||||||
requestIndex: picked.value.requestIndex,
|
requestIndex: picked.value.requestIndex,
|
||||||
req: requestUpdated,
|
},
|
||||||
})
|
}
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "teams-collection") {
|
} else if (picked.value.pickedType === "teams-collection") {
|
||||||
@@ -341,13 +353,17 @@ const updateTeamCollectionOrFolder = (
|
|||||||
(result) => {
|
(result) => {
|
||||||
const { createRequestInCollection } = result
|
const { createRequestInCollection } = result
|
||||||
|
|
||||||
setRESTSaveContext({
|
currentActiveTab.value.document = {
|
||||||
|
request: requestUpdated,
|
||||||
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
originLocation: "team-collection",
|
originLocation: "team-collection",
|
||||||
requestID: createRequestInCollection.id,
|
requestID: createRequestInCollection.id,
|
||||||
collectionID: createRequestInCollection.collection.id,
|
collectionID: createRequestInCollection.collection.id,
|
||||||
teamID: createRequestInCollection.collection.team.id,
|
teamID: createRequestInCollection.collection.team.id,
|
||||||
req: requestUpdated,
|
},
|
||||||
})
|
}
|
||||||
|
|
||||||
modalLoadingState.value = false
|
modalLoadingState.value = false
|
||||||
requestSaved()
|
requestSaved()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,11 +316,10 @@ import { TeamRequest } from "~/helpers/teams/TeamRequest"
|
|||||||
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useReadonlyStream } from "~/composables/stream"
|
|
||||||
import { restSaveContext$ } from "~/newstore/RESTSession"
|
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked.js"
|
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
@@ -520,7 +519,7 @@ const isSelected = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const active = useReadonlyStream(restSaveContext$, null)
|
const active = computed(() => currentActiveTab.value.document.saveContext)
|
||||||
|
|
||||||
const isActiveRequest = computed(() => {
|
const isActiveRequest = computed(() => {
|
||||||
return (requestID: string) => {
|
return (requestID: string) => {
|
||||||
|
|||||||
@@ -146,18 +146,6 @@
|
|||||||
@import-to-teams="importToTeams"
|
@import-to-teams="importToTeams"
|
||||||
@hide-modal="displayModalImportExport(false)"
|
@hide-modal="displayModalImportExport(false)"
|
||||||
/>
|
/>
|
||||||
<HttpReqChangeConfirmModal
|
|
||||||
:show="confirmChangeToRequest"
|
|
||||||
:loading="modalLoadingState"
|
|
||||||
@hide-modal="confirmChangeToRequest = false"
|
|
||||||
@save-change="saveRequestChange"
|
|
||||||
@discard-change="discardRequestChange"
|
|
||||||
/>
|
|
||||||
<CollectionsSaveRequest
|
|
||||||
mode="rest"
|
|
||||||
:show="showSaveRequestModal"
|
|
||||||
@hide-modal="showSaveRequestModal = false"
|
|
||||||
/>
|
|
||||||
<TeamsAdd
|
<TeamsAdd
|
||||||
:show="showTeamModalAdd"
|
:show="showTeamModalAdd"
|
||||||
@hide-modal="displayTeamModalAdd(false)"
|
@hide-modal="displayTeamModalAdd(false)"
|
||||||
@@ -166,7 +154,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, PropType, reactive, ref, watch } from "vue"
|
import { computed, PropType, ref, watch } from "vue"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
@@ -183,7 +171,6 @@ import {
|
|||||||
editRESTCollection,
|
editRESTCollection,
|
||||||
editRESTFolder,
|
editRESTFolder,
|
||||||
editRESTRequest,
|
editRESTRequest,
|
||||||
moveRESTFolder,
|
|
||||||
moveRESTRequest,
|
moveRESTRequest,
|
||||||
removeRESTCollection,
|
removeRESTCollection,
|
||||||
removeRESTFolder,
|
removeRESTFolder,
|
||||||
@@ -192,23 +179,14 @@ import {
|
|||||||
saveRESTRequestAs,
|
saveRESTRequestAs,
|
||||||
updateRESTRequestOrder,
|
updateRESTRequestOrder,
|
||||||
updateRESTCollectionOrder,
|
updateRESTCollectionOrder,
|
||||||
|
moveRESTFolder,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
|
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
|
||||||
import {
|
import {
|
||||||
HoppCollection,
|
HoppCollection,
|
||||||
HoppRESTRequest,
|
HoppRESTRequest,
|
||||||
isEqualHoppRESTRequest,
|
|
||||||
makeCollection,
|
makeCollection,
|
||||||
safelyExtractRESTRequest,
|
|
||||||
translateToNewRequest,
|
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import {
|
|
||||||
getDefaultRESTRequest,
|
|
||||||
getRESTRequest,
|
|
||||||
getRESTSaveContext,
|
|
||||||
setRESTRequest,
|
|
||||||
setRESTSaveContext,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import { cloneDeep, isEqual } from "lodash-es"
|
import { cloneDeep, isEqual } from "lodash-es"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
import {
|
import {
|
||||||
@@ -234,12 +212,26 @@ import {
|
|||||||
getTeamCollectionJSON,
|
getTeamCollectionJSON,
|
||||||
teamCollToHoppRESTColl,
|
teamCollToHoppRESTColl,
|
||||||
} from "~/helpers/backend/helpers"
|
} from "~/helpers/backend/helpers"
|
||||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { createCollectionGists } from "~/helpers/gist"
|
import { createCollectionGists } from "~/helpers/gist"
|
||||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||||
import IconListEnd from "~icons/lucide/list-end"
|
import IconListEnd from "~icons/lucide/list-end"
|
||||||
|
import {
|
||||||
|
createNewTab,
|
||||||
|
currentActiveTab,
|
||||||
|
currentTabID,
|
||||||
|
getTabRefWithSaveContext,
|
||||||
|
} from "~/helpers/rest/tab"
|
||||||
|
import {
|
||||||
|
getRequestsByPath,
|
||||||
|
resolveSaveContextOnRequestReorder,
|
||||||
|
} from "~/helpers/collection/request"
|
||||||
|
import {
|
||||||
|
getFoldersByPath,
|
||||||
|
resolveSaveContextOnCollectionReorder,
|
||||||
|
updateSaveContextForAffectedRequests,
|
||||||
|
} from "~/helpers/collection/collection"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -314,15 +306,6 @@ const exportingTeamCollections = ref(false)
|
|||||||
const creatingGistCollection = ref(false)
|
const creatingGistCollection = ref(false)
|
||||||
const importingMyCollections = ref(false)
|
const importingMyCollections = ref(false)
|
||||||
|
|
||||||
// Confirm Change to request modal
|
|
||||||
const confirmChangeToRequest = ref(false)
|
|
||||||
const showSaveRequestModal = ref(false)
|
|
||||||
const clickedRequest = reactive({
|
|
||||||
folderPath: "" as string | undefined,
|
|
||||||
requestIndex: null as string | null,
|
|
||||||
request: null as HoppRESTRequest | null,
|
|
||||||
})
|
|
||||||
|
|
||||||
// TeamList-Adapter
|
// TeamList-Adapter
|
||||||
const teamListAdapter = new TeamListAdapter(true)
|
const teamListAdapter = new TeamListAdapter(true)
|
||||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||||
@@ -637,7 +620,7 @@ const addRequest = (payload: {
|
|||||||
|
|
||||||
const onAddRequest = (requestName: string) => {
|
const onAddRequest = (requestName: string) => {
|
||||||
const newRequest = {
|
const newRequest = {
|
||||||
...cloneDeep(getRESTRequest()),
|
...cloneDeep(currentActiveTab.value.document.request),
|
||||||
name: requestName,
|
name: requestName,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,10 +629,14 @@ const onAddRequest = (requestName: string) => {
|
|||||||
if (!path) return
|
if (!path) return
|
||||||
const insertionIndex = saveRESTRequestAs(path, newRequest)
|
const insertionIndex = saveRESTRequestAs(path, newRequest)
|
||||||
|
|
||||||
setRESTRequest(newRequest, {
|
createNewTab({
|
||||||
|
request: newRequest,
|
||||||
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
originLocation: "user-collection",
|
originLocation: "user-collection",
|
||||||
folderPath: path,
|
folderPath: path,
|
||||||
requestIndex: insertionIndex,
|
requestIndex: insertionIndex,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
displayModalAddRequest(false)
|
displayModalAddRequest(false)
|
||||||
@@ -677,12 +664,17 @@ const onAddRequest = (requestName: string) => {
|
|||||||
(result) => {
|
(result) => {
|
||||||
const { createRequestInCollection } = result
|
const { createRequestInCollection } = result
|
||||||
|
|
||||||
setRESTRequest(newRequest, {
|
createNewTab({
|
||||||
|
request: newRequest,
|
||||||
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
originLocation: "team-collection",
|
originLocation: "team-collection",
|
||||||
requestID: createRequestInCollection.id,
|
requestID: createRequestInCollection.id,
|
||||||
collectionID: createRequestInCollection.collection.id,
|
collectionID: createRequestInCollection.collection.id,
|
||||||
teamID: createRequestInCollection.collection.team.id,
|
teamID: createRequestInCollection.collection.team.id,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
modalLoadingState.value = false
|
modalLoadingState.value = false
|
||||||
displayModalAddRequest(false)
|
displayModalAddRequest(false)
|
||||||
}
|
}
|
||||||
@@ -873,27 +865,22 @@ const updateEditingRequest = (newName: string) => {
|
|||||||
...request,
|
...request,
|
||||||
name: newName || request.name,
|
name: newName || request.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveCtx = getRESTSaveContext()
|
|
||||||
|
|
||||||
if (collectionsType.value.type === "my-collections") {
|
if (collectionsType.value.type === "my-collections") {
|
||||||
const folderPath = editingFolderPath.value
|
const folderPath = editingFolderPath.value
|
||||||
const requestIndex = editingRequestIndex.value
|
const requestIndex = editingRequestIndex.value
|
||||||
|
|
||||||
if (folderPath === null || requestIndex === null) return
|
if (folderPath === null || requestIndex === null) return
|
||||||
|
|
||||||
|
const possibleActiveTab = getTabRefWithSaveContext({
|
||||||
|
originLocation: "user-collection",
|
||||||
|
requestIndex,
|
||||||
|
folderPath,
|
||||||
|
})
|
||||||
|
|
||||||
editRESTRequest(folderPath, requestIndex, requestUpdated)
|
editRESTRequest(folderPath, requestIndex, requestUpdated)
|
||||||
|
|
||||||
if (
|
if (possibleActiveTab) {
|
||||||
saveCtx &&
|
possibleActiveTab.value.document.request.name = requestUpdated.name
|
||||||
saveCtx.originLocation === "user-collection" &&
|
|
||||||
saveCtx.requestIndex === editingRequestIndex.value &&
|
|
||||||
saveCtx.folderPath === editingFolderPath.value
|
|
||||||
) {
|
|
||||||
setRESTRequest({
|
|
||||||
...getRESTRequest(),
|
|
||||||
name: requestUpdated.name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
displayModalEditRequest(false)
|
displayModalEditRequest(false)
|
||||||
@@ -925,15 +912,13 @@ const updateEditingRequest = (newName: string) => {
|
|||||||
)
|
)
|
||||||
)()
|
)()
|
||||||
|
|
||||||
if (
|
const possibleTab = getTabRefWithSaveContext({
|
||||||
saveCtx &&
|
originLocation: "team-collection",
|
||||||
saveCtx.originLocation === "team-collection" &&
|
requestID,
|
||||||
saveCtx.requestID === editingRequestID.value
|
|
||||||
) {
|
|
||||||
setRESTRequest({
|
|
||||||
...getRESTRequest(),
|
|
||||||
name: requestName,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (possibleTab) {
|
||||||
|
possibleTab.value.document.request.name = requestName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1030,6 +1015,13 @@ const onRemoveCollection = () => {
|
|||||||
|
|
||||||
removeRESTCollection(collectionIndex)
|
removeRESTCollection(collectionIndex)
|
||||||
|
|
||||||
|
resolveSaveContextOnCollectionReorder({
|
||||||
|
lastIndex: collectionIndex,
|
||||||
|
newIndex: -1,
|
||||||
|
folderPath: "", // root folder
|
||||||
|
length: myCollections.value.length,
|
||||||
|
})
|
||||||
|
|
||||||
toast.success(t("state.deleted"))
|
toast.success(t("state.deleted"))
|
||||||
displayConfirmModal(false)
|
displayConfirmModal(false)
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
@@ -1074,6 +1066,14 @@ const onRemoveFolder = () => {
|
|||||||
|
|
||||||
removeRESTFolder(folderPath)
|
removeRESTFolder(folderPath)
|
||||||
|
|
||||||
|
const parentFolder = folderPath.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
|
||||||
|
resolveSaveContextOnCollectionReorder({
|
||||||
|
lastIndex: pathToLastIndex(folderPath),
|
||||||
|
newIndex: -1,
|
||||||
|
folderPath: parentFolder,
|
||||||
|
length: getFoldersByPath(myCollections.value, parentFolder).length,
|
||||||
|
})
|
||||||
|
|
||||||
toast.success(t("state.deleted"))
|
toast.success(t("state.deleted"))
|
||||||
displayConfirmModal(false)
|
displayConfirmModal(false)
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
@@ -1124,8 +1124,28 @@ const onRemoveRequest = () => {
|
|||||||
emit("select", null)
|
emit("select", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const possibleTab = getTabRefWithSaveContext({
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath,
|
||||||
|
requestIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
// If there is a tab attached to this request, dissociate its state and mark it dirty
|
||||||
|
if (possibleTab) {
|
||||||
|
possibleTab.value.document.saveContext = null
|
||||||
|
possibleTab.value.document.isDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
removeRESTRequest(folderPath, requestIndex)
|
removeRESTRequest(folderPath, requestIndex)
|
||||||
|
|
||||||
|
// the same function is used to reorder requests since after removing, it's basically doing reorder
|
||||||
|
resolveSaveContextOnRequestReorder({
|
||||||
|
lastIndex: requestIndex,
|
||||||
|
newIndex: -1,
|
||||||
|
folderPath,
|
||||||
|
length: getRequestsByPath(myCollections.value, folderPath).length,
|
||||||
|
})
|
||||||
|
|
||||||
toast.success(t("state.deleted"))
|
toast.success(t("state.deleted"))
|
||||||
displayConfirmModal(false)
|
displayConfirmModal(false)
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
@@ -1157,6 +1177,17 @@ const onRemoveRequest = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)()
|
)()
|
||||||
|
|
||||||
|
// If there is a tab attached to this request, dissociate its state and mark it dirty
|
||||||
|
const possibleTab = getTabRefWithSaveContext({
|
||||||
|
originLocation: "team-collection",
|
||||||
|
requestID,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (possibleTab) {
|
||||||
|
possibleTab.value.document.saveContext = undefined
|
||||||
|
possibleTab.value.document.isDirty = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1165,41 +1196,6 @@ const selectPicked = (payload: Picked | null) => {
|
|||||||
emit("select", payload)
|
emit("select", payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// select request change modal functions
|
|
||||||
const noChangeSetRESTRequest = () => {
|
|
||||||
const folderPath = clickedRequest.folderPath
|
|
||||||
const requestIndex = clickedRequest.requestIndex
|
|
||||||
const request = clickedRequest.request
|
|
||||||
|
|
||||||
let newContext: HoppRequestSaveContext | null = null
|
|
||||||
if (collectionsType.value.type === "my-collections") {
|
|
||||||
if (!folderPath || !requestIndex || !request) return
|
|
||||||
|
|
||||||
newContext = {
|
|
||||||
originLocation: "user-collection",
|
|
||||||
requestIndex: parseInt(requestIndex),
|
|
||||||
folderPath,
|
|
||||||
req: cloneDeep(request),
|
|
||||||
}
|
|
||||||
} else if (collectionsType.value.type === "team-collections") {
|
|
||||||
if (!requestIndex || !request) return
|
|
||||||
newContext = {
|
|
||||||
originLocation: "team-collection",
|
|
||||||
requestID: requestIndex,
|
|
||||||
req: cloneDeep(request),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setRESTRequest(
|
|
||||||
cloneDeep(
|
|
||||||
safelyExtractRESTRequest(
|
|
||||||
translateToNewRequest(request),
|
|
||||||
getDefaultRESTRequest()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
newContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is called when the user clicks on a request
|
* This function is called when the user clicks on a request
|
||||||
* @param selectedRequest The request that the user clicked on emited from the collection tree
|
* @param selectedRequest The request that the user clicked on emited from the collection tree
|
||||||
@@ -1210,129 +1206,49 @@ const selectRequest = (selectedRequest: {
|
|||||||
requestIndex: string
|
requestIndex: string
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { request, folderPath, requestIndex, isActive } = selectedRequest
|
const { request, folderPath, requestIndex } = selectedRequest
|
||||||
// If the request is already active, then we reset the save context
|
|
||||||
if (isActive) {
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentRESTRequest = getRESTRequest()
|
// If there is a request with this save context, switch into it
|
||||||
|
let possibleTab = null
|
||||||
|
|
||||||
const currentRESTSaveContext = getRESTSaveContext()
|
if (collectionsType.value.type === "team-collections") {
|
||||||
|
possibleTab = getTabRefWithSaveContext({
|
||||||
clickedRequest.folderPath = folderPath
|
originLocation: "team-collection",
|
||||||
clickedRequest.requestIndex = requestIndex
|
requestID: requestIndex,
|
||||||
clickedRequest.request = request
|
})
|
||||||
|
if (possibleTab) {
|
||||||
// If there is no active context,
|
currentTabID.value = possibleTab.value.id
|
||||||
if (!currentRESTSaveContext) {
|
|
||||||
// Check if the use is clicking on the same request
|
|
||||||
if (isEqualHoppRESTRequest(currentRESTRequest, request)) {
|
|
||||||
noChangeSetRESTRequest()
|
|
||||||
} else {
|
} else {
|
||||||
// can show the save change modal here since there is change in the request
|
createNewTab({
|
||||||
// and the user is clicking on the different request
|
request: cloneDeep(request),
|
||||||
// and currently we dont have any active context
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
confirmChangeToRequest.value = true
|
originLocation: "team-collection",
|
||||||
}
|
requestID: requestIndex,
|
||||||
} else {
|
|
||||||
if (isEqualHoppRESTRequest(currentRESTRequest, request)) {
|
|
||||||
noChangeSetRESTRequest()
|
|
||||||
} else {
|
|
||||||
const currentReqWithNoChange = currentRESTSaveContext.req
|
|
||||||
// now we compare the current request
|
|
||||||
// with the request inside the active context
|
|
||||||
if (
|
|
||||||
currentReqWithNoChange &&
|
|
||||||
isEqualHoppRESTRequest(currentReqWithNoChange, currentRESTRequest)
|
|
||||||
) {
|
|
||||||
noChangeSetRESTRequest()
|
|
||||||
} else {
|
|
||||||
// there is change in the request
|
|
||||||
// so we can show the save change modal here
|
|
||||||
confirmChangeToRequest.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called when the user clicks on the save button in the confirm change modal
|
|
||||||
* There are two cases
|
|
||||||
* 1. There is no active context
|
|
||||||
* 2. There is active context
|
|
||||||
* In the first case, we can show the save request as modal and user can select the location to save the request
|
|
||||||
* In the second case, we can save the request in the same location and update the request
|
|
||||||
*/
|
|
||||||
const saveRequestChange = () => {
|
|
||||||
const currentRESTSaveContext = getRESTSaveContext()
|
|
||||||
|
|
||||||
if (!currentRESTSaveContext) {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
confirmChangeToRequest.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentRESTRequest = getRESTRequest()
|
|
||||||
|
|
||||||
if (currentRESTSaveContext.originLocation === "user-collection") {
|
|
||||||
const folderPath = currentRESTSaveContext.folderPath
|
|
||||||
const requestIndex = currentRESTSaveContext.requestIndex
|
|
||||||
|
|
||||||
editRESTRequest(folderPath, requestIndex, currentRESTRequest)
|
|
||||||
|
|
||||||
// after saving the request, we need to change the context
|
|
||||||
// to the new request (clicked request)
|
|
||||||
noChangeSetRESTRequest()
|
|
||||||
|
|
||||||
toast.success(`${t("request.saved")}`)
|
|
||||||
confirmChangeToRequest.value = false
|
|
||||||
} else {
|
|
||||||
modalLoadingState.value = true
|
|
||||||
|
|
||||||
const requestID = currentRESTSaveContext.requestID
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
request: JSON.stringify(currentRESTRequest),
|
|
||||||
title: currentRESTRequest.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
pipe(
|
|
||||||
updateTeamRequest(requestID, data),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
confirmChangeToRequest.value = false
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
modalLoadingState.value = false
|
|
||||||
},
|
},
|
||||||
() => {
|
})
|
||||||
toast.success(`${t("request.saved")}`)
|
}
|
||||||
modalLoadingState.value = false
|
} else {
|
||||||
confirmChangeToRequest.value = false
|
possibleTab = getTabRefWithSaveContext({
|
||||||
|
originLocation: "user-collection",
|
||||||
const clickedRequestID = clickedRequest.requestIndex
|
requestIndex: parseInt(requestIndex),
|
||||||
|
folderPath: folderPath!,
|
||||||
if (!clickedRequestID) return
|
})
|
||||||
|
if (possibleTab) {
|
||||||
noChangeSetRESTRequest()
|
currentTabID.value = possibleTab.value.id
|
||||||
|
} else {
|
||||||
|
// If not, open the request in a new tab
|
||||||
|
createNewTab({
|
||||||
|
request: cloneDeep(request),
|
||||||
|
isDirty: false,
|
||||||
|
saveContext: {
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath: folderPath!,
|
||||||
|
requestIndex: parseInt(requestIndex),
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called when the user clicks on the
|
|
||||||
* don't save button in the confirm change modal
|
|
||||||
* This function will change the request to the clicked request
|
|
||||||
* without saving the changes
|
|
||||||
*/
|
|
||||||
const discardRequestChange = () => {
|
|
||||||
noChangeSetRESTRequest()
|
|
||||||
confirmChangeToRequest.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1355,13 +1271,42 @@ const dropRequest = (payload: {
|
|||||||
destinationCollectionIndex: string
|
destinationCollectionIndex: string
|
||||||
}) => {
|
}) => {
|
||||||
const { folderPath, requestIndex, destinationCollectionIndex } = payload
|
const { folderPath, requestIndex, destinationCollectionIndex } = payload
|
||||||
|
|
||||||
if (!requestIndex || !destinationCollectionIndex) return
|
if (!requestIndex || !destinationCollectionIndex) return
|
||||||
|
|
||||||
if (collectionsType.value.type === "my-collections" && folderPath) {
|
if (collectionsType.value.type === "my-collections" && folderPath) {
|
||||||
moveRESTRequest(
|
moveRESTRequest(
|
||||||
folderPath,
|
folderPath,
|
||||||
pathToLastIndex(requestIndex),
|
pathToLastIndex(requestIndex),
|
||||||
destinationCollectionIndex
|
destinationCollectionIndex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const possibleTab = getTabRefWithSaveContext({
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath,
|
||||||
|
requestIndex: pathToLastIndex(requestIndex),
|
||||||
|
})
|
||||||
|
|
||||||
|
// If there is a tab attached to this request, change save its save context
|
||||||
|
if (possibleTab) {
|
||||||
|
possibleTab.value.document.saveContext = {
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath: destinationCollectionIndex,
|
||||||
|
requestIndex: getRequestsByPath(
|
||||||
|
myCollections.value,
|
||||||
|
destinationCollectionIndex
|
||||||
|
).length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When it's drop it's basically getting deleted from last folder. reordering last folder accordingly
|
||||||
|
resolveSaveContextOnRequestReorder({
|
||||||
|
lastIndex: pathToLastIndex(requestIndex),
|
||||||
|
newIndex: -1, // being deleted from last folder
|
||||||
|
folderPath,
|
||||||
|
length: getRequestsByPath(myCollections.value, folderPath).length,
|
||||||
|
})
|
||||||
|
|
||||||
toast.success(`${t("request.moved")}`)
|
toast.success(`${t("request.moved")}`)
|
||||||
draggingToRoot.value = false
|
draggingToRoot.value = false
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
@@ -1384,6 +1329,18 @@ const dropRequest = (payload: {
|
|||||||
requestMoveLoading.value.indexOf(requestIndex),
|
requestMoveLoading.value.indexOf(requestIndex),
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const possibleTab = getTabRefWithSaveContext({
|
||||||
|
originLocation: "team-collection",
|
||||||
|
requestID: requestIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (possibleTab) {
|
||||||
|
possibleTab.value.document.saveContext = {
|
||||||
|
originLocation: "team-collection",
|
||||||
|
requestID: requestIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
toast.success(`${t("request.moved")}`)
|
toast.success(`${t("request.moved")}`)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1440,6 +1397,7 @@ const dropCollection = (payload: {
|
|||||||
const { collectionIndexDragged, destinationCollectionIndex } = payload
|
const { collectionIndexDragged, destinationCollectionIndex } = payload
|
||||||
if (!collectionIndexDragged || !destinationCollectionIndex) return
|
if (!collectionIndexDragged || !destinationCollectionIndex) return
|
||||||
if (collectionIndexDragged === destinationCollectionIndex) return
|
if (collectionIndexDragged === destinationCollectionIndex) return
|
||||||
|
|
||||||
if (collectionsType.value.type === "my-collections") {
|
if (collectionsType.value.type === "my-collections") {
|
||||||
if (
|
if (
|
||||||
checkIfCollectionIsAParentOfTheChildren(
|
checkIfCollectionIsAParentOfTheChildren(
|
||||||
@@ -1450,7 +1408,32 @@ const dropCollection = (payload: {
|
|||||||
toast.error(`${t("team.parent_coll_move")}`)
|
toast.error(`${t("team.parent_coll_move")}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parentFolder = collectionIndexDragged
|
||||||
|
.split("/")
|
||||||
|
.slice(0, -1)
|
||||||
|
.join("/") // remove last folder to get parent folder
|
||||||
|
const totalFoldersOfDestinationCollection =
|
||||||
|
getFoldersByPath(myCollections.value, destinationCollectionIndex).length -
|
||||||
|
(parentFolder === destinationCollectionIndex ? 1 : 0)
|
||||||
|
|
||||||
moveRESTFolder(collectionIndexDragged, destinationCollectionIndex)
|
moveRESTFolder(collectionIndexDragged, destinationCollectionIndex)
|
||||||
|
|
||||||
|
resolveSaveContextOnCollectionReorder(
|
||||||
|
{
|
||||||
|
lastIndex: pathToLastIndex(collectionIndexDragged),
|
||||||
|
newIndex: -1,
|
||||||
|
folderPath: parentFolder,
|
||||||
|
length: getFoldersByPath(myCollections.value, parentFolder).length,
|
||||||
|
},
|
||||||
|
"drop"
|
||||||
|
)
|
||||||
|
|
||||||
|
updateSaveContextForAffectedRequests(
|
||||||
|
collectionIndexDragged,
|
||||||
|
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
|
||||||
|
)
|
||||||
|
|
||||||
draggingToRoot.value = false
|
draggingToRoot.value = false
|
||||||
toast.success(`${t("collection.moved")}`)
|
toast.success(`${t("collection.moved")}`)
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
@@ -1600,6 +1583,11 @@ const updateRequestOrder = (payload: {
|
|||||||
pathToLastIndex(destinationRequestIndex),
|
pathToLastIndex(destinationRequestIndex),
|
||||||
destinationCollectionIndex
|
destinationCollectionIndex
|
||||||
)
|
)
|
||||||
|
resolveSaveContextOnRequestReorder({
|
||||||
|
lastIndex: pathToLastIndex(dragedRequestIndex),
|
||||||
|
newIndex: pathToLastIndex(destinationRequestIndex),
|
||||||
|
folderPath: destinationCollectionIndex,
|
||||||
|
})
|
||||||
toast.success(`${t("request.order_changed")}`)
|
toast.success(`${t("request.order_changed")}`)
|
||||||
}
|
}
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
@@ -1654,6 +1642,11 @@ const updateCollectionOrder = (payload: {
|
|||||||
dragedCollectionIndex,
|
dragedCollectionIndex,
|
||||||
destinationCollectionIndex
|
destinationCollectionIndex
|
||||||
)
|
)
|
||||||
|
resolveSaveContextOnCollectionReorder({
|
||||||
|
lastIndex: pathToLastIndex(dragedCollectionIndex),
|
||||||
|
newIndex: pathToLastIndex(destinationCollectionIndex),
|
||||||
|
folderPath: dragedCollectionIndex.split("/").slice(0, -1).join("/"),
|
||||||
|
})
|
||||||
toast.success(`${t("collection.order_changed")}`)
|
toast.success(`${t("collection.order_changed")}`)
|
||||||
}
|
}
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
|
|||||||
@@ -148,17 +148,6 @@
|
|||||||
@hide-modal="confirmRemove = false"
|
@hide-modal="confirmRemove = false"
|
||||||
@resolve="clearHistory"
|
@resolve="clearHistory"
|
||||||
/>
|
/>
|
||||||
<HttpReqChangeConfirmModal
|
|
||||||
:show="confirmChange"
|
|
||||||
@hide-modal="confirmChange = false"
|
|
||||||
@save-change="saveRequestChange"
|
|
||||||
@discard-change="discardRequestChange"
|
|
||||||
/>
|
|
||||||
<CollectionsSaveRequest
|
|
||||||
mode="rest"
|
|
||||||
:show="showSaveRequestModal"
|
|
||||||
@hide-modal="showSaveRequestModal = false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -169,17 +158,11 @@ import IconTrash from "~icons/lucide/trash"
|
|||||||
import IconFilter from "~icons/lucide/filter"
|
import IconFilter from "~icons/lucide/filter"
|
||||||
import { computed, ref, Ref, toRaw } from "vue"
|
import { computed, ref, Ref, toRaw } from "vue"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import {
|
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
HoppGQLRequest,
|
|
||||||
HoppRESTRequest,
|
|
||||||
isEqualHoppRESTRequest,
|
|
||||||
safelyExtractRESTRequest,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import { groupBy, escapeRegExp, filter } from "lodash-es"
|
import { groupBy, escapeRegExp, filter } from "lodash-es"
|
||||||
import { useTimeAgo } from "@vueuse/core"
|
import { useTimeAgo } from "@vueuse/core"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
@@ -195,20 +178,10 @@ import {
|
|||||||
RESTHistoryEntry,
|
RESTHistoryEntry,
|
||||||
GQLHistoryEntry,
|
GQLHistoryEntry,
|
||||||
} from "~/newstore/history"
|
} from "~/newstore/history"
|
||||||
import {
|
|
||||||
getDefaultRESTRequest,
|
|
||||||
getRESTRequest,
|
|
||||||
getRESTSaveContext,
|
|
||||||
setRESTRequest,
|
|
||||||
setRESTSaveContext,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import { editRESTRequest } from "~/newstore/collections"
|
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
|
||||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
|
||||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
|
||||||
|
|
||||||
import HistoryRestCard from "./rest/Card.vue"
|
import HistoryRestCard from "./rest/Card.vue"
|
||||||
import HistoryGraphqlCard from "./graphql/Card.vue"
|
import HistoryGraphqlCard from "./graphql/Card.vue"
|
||||||
|
import { createNewTab } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
||||||
|
|
||||||
@@ -229,10 +202,6 @@ const filterText = ref("")
|
|||||||
const showMore = ref(false)
|
const showMore = ref(false)
|
||||||
const confirmRemove = ref(false)
|
const confirmRemove = ref(false)
|
||||||
|
|
||||||
const clickedHistory = ref<HistoryEntry | null>(null)
|
|
||||||
const confirmChange = ref(false)
|
|
||||||
const showSaveRequestModal = ref(false)
|
|
||||||
|
|
||||||
const history = useReadonlyStream<RESTHistoryEntry[] | GQLHistoryEntry[]>(
|
const history = useReadonlyStream<RESTHistoryEntry[] | GQLHistoryEntry[]>(
|
||||||
props.page === "rest" ? restHistory$ : graphqlHistory$,
|
props.page === "rest" ? restHistory$ : graphqlHistory$,
|
||||||
[]
|
[]
|
||||||
@@ -326,111 +295,13 @@ const clearHistory = () => {
|
|||||||
toast.success(`${t("state.history_deleted")}`)
|
toast.success(`${t("state.history_deleted")}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setRestReq = (request: HoppRESTRequest | null | undefined) => {
|
|
||||||
setRESTRequest(safelyExtractRESTRequest(request, getDefaultRESTRequest()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry
|
// NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry
|
||||||
// (That is not a really good behaviour tho ¯\_(ツ)_/¯)
|
// (That is not a really good behaviour tho ¯\_(ツ)_/¯)
|
||||||
const useHistory = (entry: RESTHistoryEntry) => {
|
const useHistory = (entry: RESTHistoryEntry) => {
|
||||||
const currentFullReq = getRESTRequest()
|
createNewTab({
|
||||||
|
request: entry.request,
|
||||||
const currentReqWithNoChange = getRESTSaveContext()?.req
|
isDirty: false,
|
||||||
|
|
||||||
// checks if the current request is the same as the save context request if present
|
|
||||||
if (
|
|
||||||
currentReqWithNoChange &&
|
|
||||||
isEqualHoppRESTRequest(currentReqWithNoChange, currentFullReq)
|
|
||||||
) {
|
|
||||||
props.page === "rest" && setRestReq(entry.request)
|
|
||||||
clickedHistory.value = entry
|
|
||||||
}
|
|
||||||
// Initial state trigers a popup
|
|
||||||
else if (!clickedHistory.value) {
|
|
||||||
clickedHistory.value = entry
|
|
||||||
confirmChange.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Checks if there are any change done in current request and the history request
|
|
||||||
else if (
|
|
||||||
!isEqualHoppRESTRequest(
|
|
||||||
currentFullReq,
|
|
||||||
clickedHistory.value.request as HoppRESTRequest
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
clickedHistory.value = entry
|
|
||||||
confirmChange.value = true
|
|
||||||
} else {
|
|
||||||
props.page === "rest" && setRestReq(entry.request)
|
|
||||||
clickedHistory.value = entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save current request to the collection */
|
|
||||||
const saveRequestChange = () => {
|
|
||||||
const saveCtx = getRESTSaveContext()
|
|
||||||
saveCurrentRequest(saveCtx)
|
|
||||||
confirmChange.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Discard changes and change the current request and remove the collection context */
|
|
||||||
const discardRequestChange = () => {
|
|
||||||
const saveCtx = getRESTSaveContext()
|
|
||||||
if (saveCtx) {
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
}
|
|
||||||
clickedHistory.value &&
|
|
||||||
setRestReq(clickedHistory.value.request as HoppRESTRequest)
|
|
||||||
confirmChange.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveCurrentRequest = (saveCtx: HoppRequestSaveContext | null) => {
|
|
||||||
if (!saveCtx) {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (saveCtx.originLocation === "user-collection") {
|
|
||||||
try {
|
|
||||||
editRESTRequest(
|
|
||||||
saveCtx.folderPath,
|
|
||||||
saveCtx.requestIndex,
|
|
||||||
getRESTRequest()
|
|
||||||
)
|
|
||||||
clickedHistory.value &&
|
|
||||||
setRestReq(clickedHistory.value.request as HoppRESTRequest)
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
toast.success(`${t("request.saved")}`)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
saveCurrentRequest(null)
|
|
||||||
}
|
|
||||||
} else if (saveCtx.originLocation === "team-collection") {
|
|
||||||
const req = getRESTRequest()
|
|
||||||
try {
|
|
||||||
runMutation(UpdateRequestDocument, {
|
|
||||||
requestID: saveCtx.requestID,
|
|
||||||
data: {
|
|
||||||
title: req.name,
|
|
||||||
request: JSON.stringify(req),
|
|
||||||
},
|
|
||||||
})().then((result) => {
|
|
||||||
if (E.isLeft(result)) {
|
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
|
||||||
} else {
|
|
||||||
toast.success(`${t("request.saved")}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
clickedHistory.value &&
|
|
||||||
setRestReq(clickedHistory.value.request as HoppRESTRequest)
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
} catch (error) {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
toast.error(`${t("error.something_went_wrong")}`)
|
|
||||||
console.error(error)
|
|
||||||
setRESTSaveContext(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRESTHistoryEntry = (
|
const isRESTHistoryEntry = (
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
:active="authName === 'None'"
|
:active="authName === 'None'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
authType = 'none'
|
auth.authType = 'none'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
:active="authName === 'Basic Auth'"
|
:active="authName === 'Basic Auth'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
authType = 'basic'
|
auth.authType = 'basic'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
:active="authName === 'Bearer'"
|
:active="authName === 'Bearer'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
authType = 'bearer'
|
auth.authType = 'bearer'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
:active="authName === 'OAuth 2.0'"
|
:active="authName === 'OAuth 2.0'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
authType = 'oauth-2'
|
auth.authType = 'oauth-2'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
:active="authName === 'API key'"
|
:active="authName === 'API key'"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
authType = 'api-key'
|
auth.authType = 'api-key'
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="authType === 'none'"
|
v-if="auth.authType === 'none'"
|
||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -136,91 +136,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
<div v-if="authType === 'basic'">
|
<div v-if="auth.authType === 'basic'">
|
||||||
|
<HttpAuthorizationBasic v-model="auth" />
|
||||||
|
</div>
|
||||||
|
<div v-if="auth.authType === 'bearer'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
||||||
v-model="basicUsername"
|
|
||||||
:placeholder="t('authorization.username')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="auth.authType === 'oauth-2'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
||||||
v-model="basicPassword"
|
|
||||||
:placeholder="t('authorization.password')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<HttpOAuth2Authorization v-model="auth" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="authType === 'bearer'">
|
<div v-if="auth.authType === 'api-key'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<HttpAuthorizationApiKey v-model="auth" />
|
||||||
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="authType === 'oauth-2'">
|
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
|
||||||
<SmartEnvInput v-model="oauth2Token" placeholder="Token" />
|
|
||||||
</div>
|
|
||||||
<HttpOAuth2Authorization />
|
|
||||||
</div>
|
|
||||||
<div v-if="authType === 'api-key'">
|
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
|
||||||
<SmartEnvInput v-model="apiKey" placeholder="Key" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
|
||||||
<SmartEnvInput v-model="apiValue" placeholder="Value" />
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center border-b border-dividerLight">
|
|
||||||
<span class="flex items-center">
|
|
||||||
<label class="ml-4 text-secondaryLight">
|
|
||||||
{{ t("authorization.pass_key_by") }}
|
|
||||||
</label>
|
|
||||||
<tippy
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => authTippyActions.focus()"
|
|
||||||
>
|
|
||||||
<span class="select-wrapper">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="addTo || t('state.none')"
|
|
||||||
class="pr-8 ml-2 rounded-none"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<template #content="{ hide }">
|
|
||||||
<div
|
|
||||||
ref="authTippyActions"
|
|
||||||
class="flex flex-col focus:outline-none"
|
|
||||||
tabindex="0"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
|
|
||||||
:active="addTo === 'Headers'"
|
|
||||||
:label="'Headers'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
addTo = 'Headers'
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="
|
|
||||||
addTo === 'Query params' ? IconCircleDot : IconCircle
|
|
||||||
"
|
|
||||||
:active="addTo === 'Query params'"
|
|
||||||
:label="'Query params'"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
addTo = 'Query params'
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -248,49 +179,40 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
|||||||
import IconExternalLink from "~icons/lucide/external-link"
|
import IconExternalLink from "~icons/lucide/external-link"
|
||||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||||
import IconCircle from "~icons/lucide/circle"
|
import IconCircle from "~icons/lucide/circle"
|
||||||
import { computed, ref, Ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import {
|
import { HoppRESTAuth } from "@hoppscotch/data"
|
||||||
HoppRESTAuthBasic,
|
|
||||||
HoppRESTAuthBearer,
|
|
||||||
HoppRESTAuthOAuth2,
|
|
||||||
HoppRESTAuthAPIKey,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import { pluckRef } from "@composables/ref"
|
import { pluckRef } from "@composables/ref"
|
||||||
import { useStream } from "@composables/stream"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
const auth = useStream(
|
const props = defineProps<{
|
||||||
restAuth$,
|
modelValue: HoppRESTAuth
|
||||||
{ authType: "none", authActive: true },
|
}>()
|
||||||
setRESTAuth
|
|
||||||
)
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: HoppRESTAuth): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const auth = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
|
const AUTH_KEY_NAME = {
|
||||||
|
basic: "Basic Auth",
|
||||||
|
bearer: "Bearer",
|
||||||
|
"oauth-2": "OAuth 2.0",
|
||||||
|
"api-key": "API key",
|
||||||
|
none: "None",
|
||||||
|
} as const
|
||||||
|
|
||||||
const authType = pluckRef(auth, "authType")
|
const authType = pluckRef(auth, "authType")
|
||||||
const authName = computed(() => {
|
const authName = computed(() =>
|
||||||
if (authType.value === "basic") return "Basic Auth"
|
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
|
||||||
else if (authType.value === "bearer") return "Bearer"
|
)
|
||||||
else if (authType.value === "oauth-2") return "OAuth 2.0"
|
|
||||||
else if (authType.value === "api-key") return "API key"
|
|
||||||
else return "None"
|
|
||||||
})
|
|
||||||
const authActive = pluckRef(auth, "authActive")
|
const authActive = pluckRef(auth, "authActive")
|
||||||
const basicUsername = pluckRef(auth as Ref<HoppRESTAuthBasic>, "username")
|
|
||||||
const basicPassword = pluckRef(auth as Ref<HoppRESTAuthBasic>, "password")
|
|
||||||
const bearerToken = pluckRef(auth as Ref<HoppRESTAuthBearer>, "token")
|
|
||||||
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
|
||||||
const apiKey = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "key")
|
|
||||||
const apiValue = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "value")
|
|
||||||
const addTo = pluckRef(auth as Ref<HoppRESTAuthAPIKey>, "addTo")
|
|
||||||
if (typeof addTo.value === "undefined") {
|
|
||||||
addTo.value = "Headers"
|
|
||||||
apiKey.value = ""
|
|
||||||
apiValue.value = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
auth.value = {
|
auth.value = {
|
||||||
@@ -301,5 +223,4 @@ const clearContent = () => {
|
|||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
const authTippyActions = ref<any | null>(null)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
>
|
>
|
||||||
<span class="select-wrapper">
|
<span class="select-wrapper">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="contentType || t('state.none')"
|
:label="body.contentType || t('state.none')"
|
||||||
class="pr-8 ml-2 rounded-none"
|
class="pr-8 ml-2 rounded-none"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -28,11 +28,11 @@
|
|||||||
>
|
>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
:label="t('state.none')"
|
:label="t('state.none')"
|
||||||
:info-icon="contentType === null ? IconDone : null"
|
:info-icon="(body.contentType === null ? IconDone : null) as any"
|
||||||
:active-info-icon="contentType === null"
|
:active-info-icon="body.contentType === null"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
contentType = null
|
body.contentType = null
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -57,12 +57,12 @@
|
|||||||
:key="`contentTypeItem-${contentTypeIndex}`"
|
:key="`contentTypeItem-${contentTypeIndex}`"
|
||||||
:label="contentTypeItem"
|
:label="contentTypeItem"
|
||||||
:info-icon="
|
:info-icon="
|
||||||
contentTypeItem === contentType ? IconDone : null
|
contentTypeItem === body.contentType ? IconDone : null
|
||||||
"
|
"
|
||||||
:active-info-icon="contentTypeItem === contentType"
|
:active-info-icon="contentTypeItem === body.contentType"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
contentType = contentTypeItem
|
body.contentType = contentTypeItem
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -93,13 +93,17 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<HttpBodyParameters v-if="contentType === 'multipart/form-data'" />
|
<HttpBodyParameters
|
||||||
<HttpURLEncodedParams
|
v-if="body.contentType === 'multipart/form-data'"
|
||||||
v-else-if="contentType === 'application/x-www-form-urlencoded'"
|
v-model="body"
|
||||||
/>
|
/>
|
||||||
<HttpRawBody v-else-if="contentType !== null" :content-type="contentType" />
|
<HttpURLEncodedParams
|
||||||
|
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
|
||||||
|
v-model="body"
|
||||||
|
/>
|
||||||
|
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
||||||
<div
|
<div
|
||||||
v-if="contentType == null"
|
v-if="body.contentType == null"
|
||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -123,38 +127,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconDone from "~icons/lucide/check"
|
|
||||||
import IconInfo from "~icons/lucide/info"
|
|
||||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
|
||||||
import IconExternalLink from "~icons/lucide/external-link"
|
|
||||||
import { computed, ref } from "vue"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as A from "fp-ts/Array"
|
|
||||||
import * as O from "fp-ts/Option"
|
|
||||||
import { RequestOptionTabs } from "./RequestOptions.vue"
|
|
||||||
import { useStream } from "@composables/stream"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
|
import { HoppRESTHeader, HoppRESTReqBody } from "@hoppscotch/data"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import * as A from "fp-ts/Array"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
|
import { computed, ref } from "vue"
|
||||||
import { segmentedContentTypes } from "~/helpers/utils/contenttypes"
|
import { segmentedContentTypes } from "~/helpers/utils/contenttypes"
|
||||||
import {
|
import IconDone from "~icons/lucide/check"
|
||||||
restContentType$,
|
import IconExternalLink from "~icons/lucide/external-link"
|
||||||
restHeaders$,
|
import IconInfo from "~icons/lucide/info"
|
||||||
setRESTContentType,
|
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||||
setRESTHeaders,
|
import { RequestOptionTabs } from "./RequestOptions.vue"
|
||||||
addRESTHeader,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const props = defineProps<{
|
||||||
(e: "change-tab", value: string): void
|
body: HoppRESTReqBody
|
||||||
|
headers: HoppRESTHeader[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const contentType = useStream(restContentType$, null, setRESTContentType)
|
const emit = defineEmits<{
|
||||||
|
(e: "change-tab", value: RequestOptionTabs): void
|
||||||
|
(e: "update:headers", value: HoppRESTHeader[]): void
|
||||||
|
(e: "update:body", value: HoppRESTReqBody): void
|
||||||
|
}>()
|
||||||
|
|
||||||
// The functional headers list (the headers actually in the system)
|
const headers = useVModel(props, "headers", emit)
|
||||||
const headers = useStream(restHeaders$, [], setRESTHeaders)
|
const body = useVModel(props, "body", emit)
|
||||||
|
|
||||||
const overridenContentType = computed(() =>
|
const overridenContentType = computed(() =>
|
||||||
pipe(
|
pipe(
|
||||||
@@ -168,7 +171,9 @@ const overridenContentType = computed(() =>
|
|||||||
const contentTypeOverride = (tab: RequestOptionTabs) => {
|
const contentTypeOverride = (tab: RequestOptionTabs) => {
|
||||||
emit("change-tab", tab)
|
emit("change-tab", tab)
|
||||||
if (!isContentTypeAlreadyExist()) {
|
if (!isContentTypeAlreadyExist()) {
|
||||||
addRESTHeader({
|
// TODO: Fix this
|
||||||
|
|
||||||
|
headers.value.push({
|
||||||
key: "Content-Type",
|
key: "Content-Type",
|
||||||
value: "",
|
value: "",
|
||||||
active: true,
|
active: true,
|
||||||
|
|||||||
@@ -186,14 +186,26 @@ import { ref, watch } from "vue"
|
|||||||
import { flow, pipe } from "fp-ts/function"
|
import { flow, pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import { FormDataKeyValue } from "@hoppscotch/data"
|
import { FormDataKeyValue, HoppRESTReqBody } from "@hoppscotch/data"
|
||||||
import { isEqual, clone } from "lodash-es"
|
import { isEqual, clone } from "lodash-es"
|
||||||
import draggable from "vuedraggable-es"
|
import draggable from "vuedraggable-es"
|
||||||
import { pluckRef } from "@composables/ref"
|
import { pluckRef } from "@composables/ref"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
|
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Body
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", val: Body): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const body = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
type WorkingFormDataKeyValue = { id: number; entry: FormDataKeyValue }
|
type WorkingFormDataKeyValue = { id: number; entry: FormDataKeyValue }
|
||||||
|
|
||||||
@@ -206,7 +218,7 @@ const idTicker = ref(0)
|
|||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
const bodyParams = pluckRef<any, any>(useRESTRequestBody(), "body")
|
const bodyParams = pluckRef(body, "body")
|
||||||
|
|
||||||
// The UI representation of the parameters list (has the empty end param)
|
// The UI representation of the parameters list (has the empty end param)
|
||||||
const workingParams = ref<WorkingFormDataKeyValue[]>([
|
const workingParams = ref<WorkingFormDataKeyValue[]>([
|
||||||
@@ -355,7 +367,7 @@ const clearContent = () => {
|
|||||||
const setRequestAttachment = (
|
const setRequestAttachment = (
|
||||||
index: number,
|
index: number,
|
||||||
entry: FormDataKeyValue,
|
entry: FormDataKeyValue,
|
||||||
event: InputEvent
|
event: InputEvent | Event
|
||||||
) => {
|
) => {
|
||||||
// check if file exists or not
|
// check if file exists or not
|
||||||
if ((event.target as HTMLInputElement).files?.length === 0) {
|
if ((event.target as HTMLInputElement).files?.length === 0) {
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ import {
|
|||||||
resolvesEnvsInBody,
|
resolvesEnvsInBody,
|
||||||
} from "~/helpers/utils/EffectiveURL"
|
} from "~/helpers/utils/EffectiveURL"
|
||||||
import { getAggregateEnvs } from "~/newstore/environments"
|
import { getAggregateEnvs } from "~/newstore/environments"
|
||||||
import { getRESTRequest } from "~/newstore/RESTSession"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import {
|
import {
|
||||||
@@ -164,6 +163,8 @@ import {
|
|||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import IconCheck from "~icons/lucide/check"
|
import IconCheck from "~icons/lucide/check"
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
|
import cloneDeep from "lodash-es/cloneDeep"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -177,7 +178,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const request = ref(getRESTRequest())
|
const request = ref(cloneDeep(currentActiveTab.value.document.request))
|
||||||
const codegenType = ref<CodegenName>("shell-curl")
|
const codegenType = ref<CodegenName>("shell-curl")
|
||||||
const errorState = ref(false)
|
const errorState = ref(false)
|
||||||
|
|
||||||
@@ -246,7 +247,7 @@ watch(
|
|||||||
() => props.show,
|
() => props.show,
|
||||||
(goingToShow) => {
|
(goingToShow) => {
|
||||||
if (goingToShow) {
|
if (goingToShow) {
|
||||||
request.value = getRESTRequest()
|
request.value = cloneDeep(currentActiveTab.value.document.request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="workingHeaders"
|
v-model="workingHeaders"
|
||||||
:item-key="(header) => `header-${header.id}`"
|
:item-key="(header: WorkingHeader) => `header-${header.id}`"
|
||||||
animation="250"
|
animation="250"
|
||||||
handle=".draggable-handle"
|
handle=".draggable-handle"
|
||||||
draggable=".draggable-content"
|
draggable=".draggable-content"
|
||||||
@@ -240,10 +240,11 @@ import IconEyeOff from "~icons/lucide/eye-off"
|
|||||||
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
|
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { computed, reactive, Ref, ref, watch } from "vue"
|
import { computed, reactive, ref, watch } from "vue"
|
||||||
import { isEqual, cloneDeep } from "lodash-es"
|
import { isEqual, cloneDeep } from "lodash-es"
|
||||||
import {
|
import {
|
||||||
HoppRESTHeader,
|
HoppRESTHeader,
|
||||||
|
HoppRESTRequest,
|
||||||
parseRawKeyValueEntriesE,
|
parseRawKeyValueEntriesE,
|
||||||
rawKeyValueEntriesToString,
|
rawKeyValueEntriesToString,
|
||||||
RawKeyValueEntry,
|
RawKeyValueEntry,
|
||||||
@@ -256,15 +257,9 @@ import * as A from "fp-ts/Array"
|
|||||||
import draggable from "vuedraggable-es"
|
import draggable from "vuedraggable-es"
|
||||||
import { RequestOptionTabs } from "./RequestOptions.vue"
|
import { RequestOptionTabs } from "./RequestOptions.vue"
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import {
|
|
||||||
getRESTRequest,
|
|
||||||
restHeaders$,
|
|
||||||
restRequest$,
|
|
||||||
setRESTHeaders,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import { commonHeaders } from "~/helpers/headers"
|
import { commonHeaders } from "~/helpers/headers"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import linter from "~/helpers/editor/linting/rawKeyValue"
|
import linter from "~/helpers/editor/linting/rawKeyValue"
|
||||||
import { throwError } from "~/helpers/functional/error"
|
import { throwError } from "~/helpers/functional/error"
|
||||||
@@ -274,6 +269,7 @@ import {
|
|||||||
getComputedHeaders,
|
getComputedHeaders,
|
||||||
} from "~/helpers/utils/EffectiveURL"
|
} from "~/helpers/utils/EffectiveURL"
|
||||||
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -288,10 +284,16 @@ const linewrapEnabled = ref(true)
|
|||||||
|
|
||||||
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
|
// v-model integration with props and emit
|
||||||
|
const props = defineProps<{ modelValue: HoppRESTRequest }>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "change-tab", value: RequestOptionTabs): void
|
(e: "change-tab", value: RequestOptionTabs): void
|
||||||
|
(e: "update:modelValue", value: HoppRESTRequest): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const request = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
bulkEditor,
|
bulkEditor,
|
||||||
bulkHeaders,
|
bulkHeaders,
|
||||||
@@ -307,13 +309,10 @@ useCodemirror(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// The functional headers list (the headers actually in the system)
|
type WorkingHeader = HoppRESTHeader & { id: number }
|
||||||
const headers = useStream(restHeaders$, [], setRESTHeaders) as Ref<
|
|
||||||
HoppRESTHeader[]
|
|
||||||
>
|
|
||||||
|
|
||||||
// The UI representation of the headers list (has the empty end headers)
|
// The UI representation of the headers list (has the empty end headers)
|
||||||
const workingHeaders = ref<Array<HoppRESTHeader & { id: number }>>([
|
const workingHeaders = ref<Array<WorkingHeader>>([
|
||||||
{
|
{
|
||||||
id: idTicker.value++,
|
id: idTicker.value++,
|
||||||
key: "",
|
key: "",
|
||||||
@@ -339,7 +338,7 @@ watch(workingHeaders, (headersList) => {
|
|||||||
|
|
||||||
// Sync logic between headers and working/bulk headers
|
// Sync logic between headers and working/bulk headers
|
||||||
watch(
|
watch(
|
||||||
headers,
|
request.value.headers,
|
||||||
(newHeadersList) => {
|
(newHeadersList) => {
|
||||||
// Sync should overwrite working headers
|
// Sync should overwrite working headers
|
||||||
const filteredWorkingHeaders = pipe(
|
const filteredWorkingHeaders = pipe(
|
||||||
@@ -388,8 +387,8 @@ watch(workingHeaders, (newWorkingHeaders) => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isEqual(headers.value, fixedHeaders)) {
|
if (!isEqual(request.value.headers, fixedHeaders)) {
|
||||||
headers.value = cloneDeep(fixedHeaders)
|
request.value.headers = cloneDeep(fixedHeaders)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -405,8 +404,8 @@ watch(bulkHeaders, (newBulkHeaders) => {
|
|||||||
E.getOrElse(() => [] as RawKeyValueEntry[])
|
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isEqual(headers.value, filteredBulkHeaders)) {
|
if (!isEqual(props.modelValue, filteredBulkHeaders)) {
|
||||||
headers.value = filteredBulkHeaders
|
request.value.headers = filteredBulkHeaders
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -481,11 +480,10 @@ const clearContent = () => {
|
|||||||
bulkHeaders.value = ""
|
bulkHeaders.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const restRequest = useReadonlyStream(restRequest$, getRESTRequest())
|
|
||||||
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs())
|
||||||
|
|
||||||
const computedHeaders = computed(() =>
|
const computedHeaders = computed(() =>
|
||||||
getComputedHeaders(restRequest.value, aggregateEnvs.value).map(
|
getComputedHeaders(request.value, aggregateEnvs.value).map(
|
||||||
(header, index) => ({
|
(header, index) => ({
|
||||||
id: `header-${index}`,
|
id: `header-${index}`,
|
||||||
...header,
|
...header,
|
||||||
|
|||||||
@@ -81,7 +81,6 @@
|
|||||||
import { reactive, ref, watch } from "vue"
|
import { reactive, ref, watch } from "vue"
|
||||||
import { refAutoReset } from "@vueuse/core"
|
import { refAutoReset } from "@vueuse/core"
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import { setRESTRequest } from "~/newstore/RESTSession"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { parseCurlToHoppRESTReq } from "~/helpers/curl"
|
import { parseCurlToHoppRESTReq } from "~/helpers/curl"
|
||||||
@@ -94,6 +93,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
|||||||
import IconClipboard from "~icons/lucide/clipboard"
|
import IconClipboard from "~icons/lucide/clipboard"
|
||||||
import IconCheck from "~icons/lucide/check"
|
import IconCheck from "~icons/lucide/check"
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ const handleImport = () => {
|
|||||||
try {
|
try {
|
||||||
const req = parseCurlToHoppRESTReq(text)
|
const req = parseCurlToHoppRESTReq(text)
|
||||||
|
|
||||||
setRESTRequest(req)
|
currentActiveTab.value.document.request = req
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.error(`${t("error.curl_invalid_format")}`)
|
toast.error(`${t("error.curl_invalid_format")}`)
|
||||||
|
|||||||
@@ -31,50 +31,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, defineComponent } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
|
import { HoppRESTAuthOAuth2, parseTemplateString } from "@hoppscotch/data"
|
||||||
import { pluckRef } from "@composables/ref"
|
import { pluckRef } from "@composables/ref"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useStream } from "@composables/stream"
|
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { restAuth$, setRESTAuth } from "~/newstore/RESTSession"
|
|
||||||
import { tokenRequest } from "~/helpers/oauth"
|
import { tokenRequest } from "~/helpers/oauth"
|
||||||
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
import { getCombinedEnvVariables } from "~/helpers/preRequest"
|
||||||
|
|
||||||
export default defineComponent({
|
const t = useI18n()
|
||||||
setup() {
|
const toast = useToast()
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const auth = useStream(
|
const props = defineProps<{
|
||||||
restAuth$,
|
modelValue: HoppRESTAuthOAuth2
|
||||||
{ authType: "none", authActive: true },
|
}>()
|
||||||
setRESTAuth
|
|
||||||
)
|
|
||||||
|
|
||||||
const oidcDiscoveryURL = pluckRef(
|
const emit = defineEmits<{
|
||||||
auth as Ref<HoppRESTAuthOAuth2>,
|
(e: "update:modelValue", value: HoppRESTAuthOAuth2): void
|
||||||
"oidcDiscoveryURL"
|
}>()
|
||||||
)
|
|
||||||
|
|
||||||
const authURL = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "authURL")
|
const auth = ref(props.modelValue)
|
||||||
|
|
||||||
const accessTokenURL = pluckRef(
|
watch(
|
||||||
auth as Ref<HoppRESTAuthOAuth2>,
|
() => auth.value,
|
||||||
"accessTokenURL"
|
(val) => {
|
||||||
)
|
emit("update:modelValue", val)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const clientID = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "clientID")
|
const oidcDiscoveryURL = pluckRef(auth, "oidcDiscoveryURL")
|
||||||
|
|
||||||
const clientSecret = pluckRef(
|
const authURL = pluckRef(auth, "authURL")
|
||||||
auth as Ref<HoppRESTAuthOAuth2>,
|
|
||||||
"clientSecret"
|
|
||||||
)
|
|
||||||
|
|
||||||
const scope = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "scope")
|
const accessTokenURL = pluckRef(auth, "accessTokenURL")
|
||||||
|
|
||||||
const handleAccessTokenRequest = async () => {
|
const clientID = pluckRef(auth, "clientID")
|
||||||
|
|
||||||
|
// TODO: Fix this type error. currently there is no type for clientSecret
|
||||||
|
const clientSecret = pluckRef(auth, "clientSecret" as any)
|
||||||
|
|
||||||
|
const scope = pluckRef(auth, "scope")
|
||||||
|
|
||||||
|
const handleAccessTokenRequest = async () => {
|
||||||
if (
|
if (
|
||||||
oidcDiscoveryURL.value === "" &&
|
oidcDiscoveryURL.value === "" &&
|
||||||
(authURL.value === "" || accessTokenURL.value === "")
|
(authURL.value === "" || accessTokenURL.value === "")
|
||||||
@@ -88,10 +87,7 @@ export default defineComponent({
|
|||||||
try {
|
try {
|
||||||
const tokenReqParams = {
|
const tokenReqParams = {
|
||||||
grantType: "code",
|
grantType: "code",
|
||||||
oidcDiscoveryUrl: parseTemplateString(
|
oidcDiscoveryUrl: parseTemplateString(oidcDiscoveryURL.value, envVars),
|
||||||
oidcDiscoveryURL.value,
|
|
||||||
envVars
|
|
||||||
),
|
|
||||||
authUrl: parseTemplateString(authURL.value, envVars),
|
authUrl: parseTemplateString(authURL.value, envVars),
|
||||||
accessTokenUrl: parseTemplateString(accessTokenURL.value, envVars),
|
accessTokenUrl: parseTemplateString(accessTokenURL.value, envVars),
|
||||||
clientId: parseTemplateString(clientID.value, envVars),
|
clientId: parseTemplateString(clientID.value, envVars),
|
||||||
@@ -102,18 +98,5 @@ export default defineComponent({
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(`${e}`)
|
toast.error(`${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
oidcDiscoveryURL,
|
|
||||||
authURL,
|
|
||||||
accessTokenURL,
|
|
||||||
clientID,
|
|
||||||
clientSecret,
|
|
||||||
scope,
|
|
||||||
handleAccessTokenRequest,
|
|
||||||
t,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
|
|||||||
import IconCircle from "~icons/lucide/circle"
|
import IconCircle from "~icons/lucide/circle"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
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 { flow, pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
@@ -198,10 +198,9 @@ import { useCodemirror } from "@composables/codemirror"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useStream } from "@composables/stream"
|
|
||||||
import { restParams$, setRESTParams } from "~/newstore/RESTSession"
|
|
||||||
import { throwError } from "@functional/error"
|
import { throwError } from "@functional/error"
|
||||||
import { objRemoveKey } from "@functional/object"
|
import { objRemoveKey } from "@functional/object"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
@@ -232,8 +231,16 @@ useCodemirror(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: HoppRESTParam[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: Array<HoppRESTParam>): void
|
||||||
|
}>()
|
||||||
|
|
||||||
// The functional parameters list (the parameters actually applied to the session)
|
// The functional parameters list (the parameters actually applied to the session)
|
||||||
const params = useStream(restParams$, [], setRESTParams) as Ref<HoppRESTParam[]>
|
const params = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
// The UI representation of the parameters list (has the empty end param)
|
// The UI representation of the parameters list (has the empty end param)
|
||||||
const workingParams = ref<Array<HoppRESTParam & { id: number }>>([
|
const workingParams = ref<Array<HoppRESTParam & { id: number }>>([
|
||||||
|
|||||||
@@ -66,16 +66,23 @@ import IconHelpCircle from "~icons/lucide/help-circle"
|
|||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import { reactive, ref } from "vue"
|
import { reactive, ref } from "vue"
|
||||||
import { usePreRequestScript } from "~/newstore/RESTSession"
|
|
||||||
import snippets from "@helpers/preRequestScriptSnippets"
|
import snippets from "@helpers/preRequestScriptSnippets"
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import linter from "~/helpers/editor/linting/preRequest"
|
import linter from "~/helpers/editor/linting/preRequest"
|
||||||
import completer from "~/helpers/editor/completion/preRequest"
|
import completer from "~/helpers/editor/completion/preRequest"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
const t = useI18n()
|
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<any | null>(null)
|
const preRequestEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const linewrapEnabled = ref(true)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
'application/hal+json',
|
'application/hal+json',
|
||||||
'application/vnd.api+json',
|
'application/vnd.api+json',
|
||||||
'application/xml',
|
'application/xml',
|
||||||
].includes(contentType)
|
].includes(body.contentType)
|
||||||
"
|
"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('action.prettify')"
|
:title="t('action.prettify')"
|
||||||
@@ -74,16 +74,14 @@ import IconInfo from "~icons/lucide/info"
|
|||||||
import { computed, reactive, Ref, ref, watch } from "vue"
|
import { computed, reactive, Ref, ref, watch } from "vue"
|
||||||
import * as TO from "fp-ts/TaskOption"
|
import * as TO from "fp-ts/TaskOption"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import { ValidContentTypes } from "@hoppscotch/data"
|
import { HoppRESTReqBody, ValidContentTypes } from "@hoppscotch/data"
|
||||||
import { refAutoReset } from "@vueuse/core"
|
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import { getEditorLangForMimeType } from "@helpers/editorutils"
|
import { getEditorLangForMimeType } from "@helpers/editorutils"
|
||||||
import { pluckRef } from "@composables/ref"
|
import { pluckRef } from "@composables/ref"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
|
||||||
|
|
||||||
import jsonLinter from "~/helpers/editor/linting/json"
|
import jsonLinter from "~/helpers/editor/linting/json"
|
||||||
import { readFileAsText } from "~/helpers/functional/files"
|
import { readFileAsText } from "~/helpers/functional/files"
|
||||||
|
|
||||||
@@ -92,27 +90,35 @@ type PossibleContentTypes = Exclude<
|
|||||||
"multipart/form-data" | "application/x-www-form-urlencoded"
|
"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 t = useI18n()
|
||||||
|
|
||||||
const payload = ref<HTMLInputElement | null>(null)
|
const payload = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
contentType: PossibleContentTypes
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const rawParamsBody = pluckRef(useRESTRequestBody(), "body")
|
const rawParamsBody = pluckRef(body, "body")
|
||||||
|
|
||||||
const prettifyIcon = refAutoReset<
|
const prettifyIcon = refAutoReset<
|
||||||
typeof IconWand2 | typeof IconCheck | typeof IconInfo
|
typeof IconWand2 | typeof IconCheck | typeof IconInfo
|
||||||
>(IconWand2, 1000)
|
>(IconWand2, 1000)
|
||||||
|
|
||||||
const rawInputEditorLang = computed(() =>
|
const rawInputEditorLang = computed(() =>
|
||||||
getEditorLangForMimeType(props.contentType)
|
getEditorLangForMimeType(body.value.contentType)
|
||||||
)
|
)
|
||||||
const langLinter = computed(() =>
|
const langLinter = computed(() =>
|
||||||
isJSONContentType(props.contentType) ? jsonLinter : null
|
isJSONContentType(body.value.contentType) ? jsonLinter : null
|
||||||
)
|
)
|
||||||
|
|
||||||
const linewrapEnabled = ref(true)
|
const linewrapEnabled = ref(true)
|
||||||
@@ -175,10 +181,10 @@ const uploadPayload = async (e: Event) => {
|
|||||||
const prettifyRequestBody = () => {
|
const prettifyRequestBody = () => {
|
||||||
let prettifyBody = ""
|
let prettifyBody = ""
|
||||||
try {
|
try {
|
||||||
if (props.contentType.endsWith("json")) {
|
if (body.value.contentType.endsWith("json")) {
|
||||||
const jsonObj = JSON.parse(rawParamsBody.value as string)
|
const jsonObj = JSON.parse(rawParamsBody.value as string)
|
||||||
prettifyBody = JSON.stringify(jsonObj, null, 2)
|
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)
|
prettifyBody = prettifyXML(rawParamsBody.value as string)
|
||||||
}
|
}
|
||||||
rawParamsBody.value = prettifyBody
|
rawParamsBody.value = prettifyBody
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
<input
|
<input
|
||||||
id="method"
|
id="method"
|
||||||
class="flex px-4 py-2 font-semibold transition rounded-l cursor-pointer text-secondaryDark w-26 bg-primaryLight"
|
class="flex px-4 py-2 font-semibold transition rounded-l cursor-pointer text-secondaryDark w-26 bg-primaryLight"
|
||||||
:value="newMethod"
|
:value="tab.document.request.method"
|
||||||
:readonly="!isCustomMethod"
|
:readonly="!isCustomMethod"
|
||||||
:placeholder="`${t('request.method')}`"
|
:placeholder="`${t('request.method')}`"
|
||||||
@input="onSelectMethod($event.target.value)"
|
@input="onSelectMethod($event)"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
:label="method"
|
:label="method"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
onSelectMethod(method)
|
updateMethod(method)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
class="flex flex-1 overflow-auto transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
|
class="flex flex-1 overflow-auto transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="newEndpoint"
|
v-model="tab.document.request.endpoint"
|
||||||
:placeholder="`${t('request.url')}`"
|
:placeholder="`${t('request.url')}`"
|
||||||
@enter="newSendRequest()"
|
@enter="newSendRequest()"
|
||||||
@paste="onPasteUrl($event)"
|
@paste="onPasteUrl($event)"
|
||||||
@@ -161,12 +161,11 @@
|
|||||||
ref="saveTippyActions"
|
ref="saveTippyActions"
|
||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.s="saveRequestAction.$el.click()"
|
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="request-name"
|
id="request-name"
|
||||||
v-model="requestName"
|
v-model="tab.document.request.name"
|
||||||
:placeholder="`${t('request.name')}`"
|
:placeholder="`${t('request.name')}`"
|
||||||
name="request-name"
|
name="request-name"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -195,7 +194,6 @@
|
|||||||
ref="saveRequestAction"
|
ref="saveRequestAction"
|
||||||
:label="`${t('request.save_as')}`"
|
:label="`${t('request.save_as')}`"
|
||||||
:icon="IconFolderPlus"
|
:icon="IconFolderPlus"
|
||||||
:shortcut="['S']"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
showSaveRequestModal = true
|
showSaveRequestModal = true
|
||||||
@@ -227,55 +225,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconShare2 from "~icons/lucide/share-2"
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import IconCheck from "~icons/lucide/check"
|
|
||||||
import IconFileCode from "~icons/lucide/file-code"
|
|
||||||
import IconCode2 from "~icons/lucide/code-2"
|
|
||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
|
||||||
import IconSave from "~icons/lucide/save"
|
|
||||||
import IconChevronDown from "~icons/lucide/chevron-down"
|
|
||||||
import IconLink2 from "~icons/lucide/link-2"
|
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
|
||||||
import { computed, ref, watch } from "vue"
|
|
||||||
import { isLeft, isRight } from "fp-ts/lib/Either"
|
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import { cloneDeep } from "lodash-es"
|
|
||||||
import { refAutoReset } from "@vueuse/core"
|
|
||||||
import {
|
|
||||||
updateRESTResponse,
|
|
||||||
restEndpoint$,
|
|
||||||
setRESTEndpoint,
|
|
||||||
restMethod$,
|
|
||||||
updateRESTMethod,
|
|
||||||
resetRESTRequest,
|
|
||||||
useRESTRequestName,
|
|
||||||
getRESTSaveContext,
|
|
||||||
getRESTRequest,
|
|
||||||
restRequest$,
|
|
||||||
setRESTSaveContext,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import { editRESTRequest } from "~/newstore/collections"
|
|
||||||
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
|
||||||
import {
|
|
||||||
useStream,
|
|
||||||
useStreamSubscriber,
|
|
||||||
useReadonlyStream,
|
|
||||||
} from "@composables/stream"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
import { useStreamSubscriber } from "@composables/stream"
|
||||||
|
import { useToast } from "@composables/toast"
|
||||||
|
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
|
||||||
|
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import { isLeft, isRight } from "fp-ts/lib/Either"
|
||||||
|
import { computed, ref, watch } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
|
||||||
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
|
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||||
|
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
import {
|
import {
|
||||||
cancelRunningExtensionRequest,
|
cancelRunningExtensionRequest,
|
||||||
hasExtensionInstalled,
|
hasExtensionInstalled,
|
||||||
} from "~/helpers/strategies/ExtensionStrategy"
|
} from "~/helpers/strategies/ExtensionStrategy"
|
||||||
|
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import { editRESTRequest } from "~/newstore/collections"
|
||||||
|
import IconCheck from "~icons/lucide/check"
|
||||||
|
import IconChevronDown from "~icons/lucide/chevron-down"
|
||||||
|
import IconCode2 from "~icons/lucide/code-2"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconFileCode from "~icons/lucide/file-code"
|
||||||
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
|
import IconLink2 from "~icons/lucide/link-2"
|
||||||
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
|
import IconSave from "~icons/lucide/save"
|
||||||
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
|
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -296,9 +279,19 @@ const toast = useToast()
|
|||||||
|
|
||||||
const { subscribeToStream } = useStreamSubscriber()
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|
||||||
const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
|
const props = defineProps<{ modelValue: HoppRESTTab }>()
|
||||||
|
const emit = defineEmits(["update:modelValue"])
|
||||||
|
|
||||||
|
const tab = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
|
const newEndpoint = computed(() => {
|
||||||
|
return tab.value.document.request.endpoint
|
||||||
|
})
|
||||||
|
const newMethod = computed(() => {
|
||||||
|
return tab.value.document.request.method
|
||||||
|
})
|
||||||
|
|
||||||
const curlText = ref("")
|
const curlText = ref("")
|
||||||
const newMethod = useStream(restMethod$, "", updateRESTMethod)
|
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@@ -327,6 +320,30 @@ watch(loading, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: make this oAuthURL() work
|
||||||
|
|
||||||
|
// function oAuthURL() {
|
||||||
|
// const auth = useReadonlyStream(props.request.auth$, {
|
||||||
|
// authType: "none",
|
||||||
|
// authActive: true,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||||
|
|
||||||
|
// onBeforeMount(async () => {
|
||||||
|
// try {
|
||||||
|
// const tokenInfo = await oauthRedirect()
|
||||||
|
// if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
|
||||||
|
// if (typeof tokenInfo === "object") {
|
||||||
|
// oauth2Token.value = tokenInfo.access_token
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // eslint-disable-next-line no-empty
|
||||||
|
// } catch (_) {}
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
const newSendRequest = async () => {
|
const newSendRequest = async () => {
|
||||||
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||||
toast.error(`${t("empty.endpoint")}`)
|
toast.error(`${t("empty.endpoint")}`)
|
||||||
@@ -335,10 +352,14 @@ const newSendRequest = async () => {
|
|||||||
|
|
||||||
ensureMethodInEndpoint()
|
ensureMethodInEndpoint()
|
||||||
|
|
||||||
|
console.log("Sending request", newEndpoint.value)
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
// Double calling is because the function returns a TaskEither than should be executed
|
// Double calling is because the function returns a TaskEither than should be executed
|
||||||
const streamResult = await runRESTRequest$()()
|
const streamResult = await runRESTRequest$(tab)()
|
||||||
|
|
||||||
|
console.log("Stream result", streamResult)
|
||||||
|
|
||||||
if (isRight(streamResult)) {
|
if (isRight(streamResult)) {
|
||||||
subscribeToStream(
|
subscribeToStream(
|
||||||
@@ -380,9 +401,11 @@ const ensureMethodInEndpoint = () => {
|
|||||||
) {
|
) {
|
||||||
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)) {
|
||||||
setRESTEndpoint("http://" + newEndpoint.value)
|
tab.value.document.request.endpoint =
|
||||||
|
"http://" + tab.value.document.request.endpoint
|
||||||
} else {
|
} else {
|
||||||
setRESTEndpoint("https://" + newEndpoint.value)
|
tab.value.document.request.endpoint =
|
||||||
|
"https://" + tab.value.document.request.endpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,7 +418,7 @@ const onPasteUrl = (e: { pastedValue: string; prevValue: string }) => {
|
|||||||
if (isCURL(pastedData)) {
|
if (isCURL(pastedData)) {
|
||||||
showCurlImportModal.value = true
|
showCurlImportModal.value = true
|
||||||
curlText.value = pastedData
|
curlText.value = pastedData
|
||||||
newEndpoint.value = e.prevValue
|
tab.value.document.request.endpoint = e.prevValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,15 +435,21 @@ const cancelRequest = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateMethod = (method: string) => {
|
const updateMethod = (method: string) => {
|
||||||
updateRESTMethod(method)
|
tab.value.document.request.method = method
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSelectMethod = (method: string) => {
|
const onSelectMethod = (e: Event | any) => {
|
||||||
updateMethod(method)
|
// type any because of value property not being recognized by TS in the event.target object. It is a valid property though.
|
||||||
|
updateMethod(e.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
resetRESTRequest()
|
tab.value.document.request = getDefaultRESTRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRESTResponse = (response: HoppRESTResponse | null) => {
|
||||||
|
tab.value.response = response
|
||||||
|
console.log("Updating response", response)
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyLinkIcon = refAutoReset<
|
const copyLinkIcon = refAutoReset<
|
||||||
@@ -440,20 +469,13 @@ const shareButtonText = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const request = useReadonlyStream(restRequest$, getRESTRequest())
|
|
||||||
|
|
||||||
watch(request, () => {
|
|
||||||
shareLink.value = null
|
|
||||||
})
|
|
||||||
|
|
||||||
const copyRequest = async () => {
|
const copyRequest = async () => {
|
||||||
if (shareLink.value) {
|
if (shareLink.value) {
|
||||||
copyShareLink(shareLink.value)
|
copyShareLink(shareLink.value)
|
||||||
} else {
|
} else {
|
||||||
shareLink.value = ""
|
shareLink.value = ""
|
||||||
fetchingShareLink.value = true
|
fetchingShareLink.value = true
|
||||||
const request = getRESTRequest()
|
const shortcodeResult = await createShortcode(tab.value.document.request)()
|
||||||
const shortcodeResult = await createShortcode(request)()
|
|
||||||
if (E.isLeft(shortcodeResult)) {
|
if (E.isLeft(shortcodeResult)) {
|
||||||
toast.error(`${shortcodeResult.left.error}`)
|
toast.error(`${shortcodeResult.left.error}`)
|
||||||
shareLink.value = `${t("error.something_went_wrong")}`
|
shareLink.value = `${t("error.something_went_wrong")}`
|
||||||
@@ -511,33 +533,26 @@ const cycleDownMethod = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveRequest = () => {
|
const saveRequest = () => {
|
||||||
const saveCtx = getRESTSaveContext()
|
const saveCtx = tab.value.document.saveContext
|
||||||
|
|
||||||
if (!saveCtx) {
|
if (!saveCtx) {
|
||||||
showSaveRequestModal.value = true
|
showSaveRequestModal.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (saveCtx.originLocation === "user-collection") {
|
if (saveCtx.originLocation === "user-collection") {
|
||||||
const req = getRESTRequest()
|
const req = tab.value.document.request
|
||||||
|
|
||||||
try {
|
try {
|
||||||
editRESTRequest(
|
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
|
||||||
saveCtx.folderPath,
|
|
||||||
saveCtx.requestIndex,
|
tab.value.document.isDirty = false
|
||||||
getRESTRequest()
|
|
||||||
)
|
|
||||||
setRESTSaveContext({
|
|
||||||
originLocation: "user-collection",
|
|
||||||
folderPath: saveCtx.folderPath,
|
|
||||||
requestIndex: saveCtx.requestIndex,
|
|
||||||
req: cloneDeep(req),
|
|
||||||
})
|
|
||||||
toast.success(`${t("request.saved")}`)
|
toast.success(`${t("request.saved")}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setRESTSaveContext(null)
|
tab.value.document.saveContext = undefined
|
||||||
saveRequest()
|
saveRequest()
|
||||||
}
|
}
|
||||||
} else if (saveCtx.originLocation === "team-collection") {
|
} else if (saveCtx.originLocation === "team-collection") {
|
||||||
const req = getRESTRequest()
|
const req = tab.value.document.request
|
||||||
|
|
||||||
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
|
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
|
||||||
try {
|
try {
|
||||||
@@ -551,11 +566,8 @@ const saveRequest = () => {
|
|||||||
if (E.isLeft(result)) {
|
if (E.isLeft(result)) {
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
toast.error(`${t("profile.no_permission")}`)
|
||||||
} else {
|
} else {
|
||||||
setRESTSaveContext({
|
tab.value.document.isDirty = false
|
||||||
originLocation: "team-collection",
|
|
||||||
requestID: saveCtx.requestID,
|
|
||||||
req: cloneDeep(req),
|
|
||||||
})
|
|
||||||
toast.success(`${t("request.saved")}`)
|
toast.success(`${t("request.saved")}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -587,10 +599,11 @@ defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
|
|||||||
defineActionHandler("request.method.head", () => updateMethod("HEAD"))
|
defineActionHandler("request.method.head", () => updateMethod("HEAD"))
|
||||||
|
|
||||||
const isCustomMethod = computed(() => {
|
const isCustomMethod = computed(() => {
|
||||||
return newMethod.value === "CUSTOM" || !methods.includes(newMethod.value)
|
return (
|
||||||
|
tab.value.document.request.method === "CUSTOM" ||
|
||||||
|
!methods.includes(newMethod.value)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestName = useRESTRequestName()
|
|
||||||
|
|
||||||
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,51 +9,52 @@
|
|||||||
:label="`${t('tab.parameters')}`"
|
:label="`${t('tab.parameters')}`"
|
||||||
:info="`${newActiveParamsCount$}`"
|
:info="`${newActiveParamsCount$}`"
|
||||||
>
|
>
|
||||||
<HttpParameters />
|
<HttpParameters v-model="request.params" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
|
<HoppSmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
|
||||||
<HttpBody @change-tab="changeTab" />
|
<HttpBody
|
||||||
|
v-model:headers="request.headers"
|
||||||
|
v-model:body="request.body"
|
||||||
|
@change-tab="changeTab"
|
||||||
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'headers'"
|
:id="'headers'"
|
||||||
:label="`${t('tab.headers')}`"
|
:label="`${t('tab.headers')}`"
|
||||||
:info="`${newActiveHeadersCount$}`"
|
:info="`${newActiveHeadersCount$}`"
|
||||||
>
|
>
|
||||||
<HttpHeaders @change-tab="changeTab" />
|
<HttpHeaders v-model="request" @change-tab="changeTab" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||||
<HttpAuthorization />
|
<HttpAuthorization v-model="request.auth" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'preRequestScript'"
|
:id="'preRequestScript'"
|
||||||
:label="`${t('tab.pre_request_script')}`"
|
:label="`${t('tab.pre_request_script')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
preRequestScript && preRequestScript.length > 0 ? true : false
|
request.preRequestScript && request.preRequestScript.length > 0
|
||||||
|
? true
|
||||||
|
: false
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<HttpPreRequestScript />
|
<HttpPreRequestScript v-model="request.preRequestScript" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'tests'"
|
:id="'tests'"
|
||||||
:label="`${t('tab.tests')}`"
|
:label="`${t('tab.tests')}`"
|
||||||
:indicator="testScript && testScript.length > 0 ? true : false"
|
:indicator="
|
||||||
|
request.testScript && request.testScript.length > 0 ? true : false
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<HttpTests />
|
<HttpTests v-model="request.testScript" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue"
|
|
||||||
import { map } from "rxjs/operators"
|
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import {
|
|
||||||
restActiveHeadersCount$,
|
|
||||||
restActiveParamsCount$,
|
|
||||||
usePreRequestScript,
|
|
||||||
useTestScript,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { computed, ref, watch } from "vue"
|
||||||
|
|
||||||
export type RequestOptionTabs =
|
export type RequestOptionTabs =
|
||||||
| "params"
|
| "params"
|
||||||
@@ -63,33 +64,43 @@ export type RequestOptionTabs =
|
|||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
|
// v-model integration with props and emit
|
||||||
|
const props = defineProps<{ modelValue: HoppRESTRequest }>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: HoppRESTRequest): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const request = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => request.value,
|
||||||
|
(newVal) => {
|
||||||
|
emit("update:modelValue", newVal)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
||||||
|
|
||||||
const changeTab = (e: RequestOptionTabs) => {
|
const changeTab = (e: RequestOptionTabs) => {
|
||||||
selectedRealtimeTab.value = e
|
selectedRealtimeTab.value = e
|
||||||
}
|
}
|
||||||
|
|
||||||
const newActiveParamsCount$ = useReadonlyStream(
|
const newActiveParamsCount$ = computed(() => {
|
||||||
restActiveParamsCount$.pipe(
|
const e = request.value.params.filter(
|
||||||
map((e) => {
|
(x) => x.active && (x.key !== "" || x.value !== "")
|
||||||
|
).length
|
||||||
|
|
||||||
if (e === 0) return null
|
if (e === 0) return null
|
||||||
return `${e}`
|
return `${e}`
|
||||||
})
|
})
|
||||||
),
|
|
||||||
null
|
const newActiveHeadersCount$ = computed(() => {
|
||||||
)
|
const e = request.value.headers.filter(
|
||||||
|
(x) => x.active && (x.key !== "" || x.value !== "")
|
||||||
|
).length
|
||||||
|
|
||||||
const newActiveHeadersCount$ = useReadonlyStream(
|
|
||||||
restActiveHeadersCount$.pipe(
|
|
||||||
map((e) => {
|
|
||||||
if (e === 0) return null
|
if (e === 0) return null
|
||||||
return `${e}`
|
return `${e}`
|
||||||
})
|
})
|
||||||
),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
const preRequestScript = usePreRequestScript()
|
|
||||||
|
|
||||||
const testScript = useTestScript()
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<AppPaneLayout layout-id="rest-primary">
|
||||||
|
<template #primary>
|
||||||
|
<HttpRequest v-model="tab" />
|
||||||
|
<HttpRequestOptions v-model="tab.document.request" />
|
||||||
|
</template>
|
||||||
|
<template #secondary>
|
||||||
|
<HttpResponse v-model:tab="tab" />
|
||||||
|
</template>
|
||||||
|
</AppPaneLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from "vue"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||||
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { isEqualHoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
|
||||||
|
// TODO: Move Response and Request execution code to over here
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: HoppRESTTab }>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", val: HoppRESTTab): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tab = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
|
// TODO: Come up with a better dirty check
|
||||||
|
let oldRequest = cloneDeep(tab.value.document.request)
|
||||||
|
watch(
|
||||||
|
() => tab.value.document.request,
|
||||||
|
(updatedValue) => {
|
||||||
|
if (
|
||||||
|
!tab.value.document.isDirty &&
|
||||||
|
!isEqualHoppRESTRequest(oldRequest, updatedValue)
|
||||||
|
) {
|
||||||
|
tab.value.document.isDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
oldRequest = cloneDeep(updatedValue)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -1,34 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<HttpResponseMeta :response="response" />
|
<HttpResponseMeta :response="tab.response" />
|
||||||
<LensesResponseBodyRenderer
|
<LensesResponseBodyRenderer
|
||||||
v-if="!loading && hasResponse"
|
v-if="!loading && hasResponse"
|
||||||
v-model:selected-tab-preference="selectedTabPreference"
|
v-model:selected-tab-preference="selectedTabPreference"
|
||||||
:response="response"
|
v-model:tab="tab"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||||
import { restResponse$ } from "~/newstore/RESTSession"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tab: HoppRESTTab
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:tab", val: HoppRESTTab): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tab = useVModel(props, "tab", emit)
|
||||||
|
|
||||||
const selectedTabPreference = ref<string | null>(null)
|
const selectedTabPreference = ref<string | null>(null)
|
||||||
|
|
||||||
const response = useReadonlyStream(restResponse$, null)
|
|
||||||
|
|
||||||
const hasResponse = computed(
|
const hasResponse = computed(
|
||||||
() => response.value?.type === "success" || response.value?.type === "fail"
|
() =>
|
||||||
|
tab.value.response?.type === "success" ||
|
||||||
|
tab.value.response?.type === "fail"
|
||||||
)
|
)
|
||||||
|
|
||||||
const loading = computed(
|
const loading = computed(() => tab.value.response?.type === "loading")
|
||||||
() => response.value === null || response.value.type === "loading"
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(response, () => {
|
watch(loading, (isLoading) => {
|
||||||
if (response.value?.type === "loading") startPageProgress()
|
if (isLoading) startPageProgress()
|
||||||
else completePageProgress()
|
else completePageProgress()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const t = useI18n()
|
|||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
response: HoppRESTResponse | null
|
response: HoppRESTResponse | null | undefined
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,6 +119,7 @@ const props = defineProps<{
|
|||||||
const readableResponseSize = computed(() => {
|
const readableResponseSize = computed(() => {
|
||||||
if (
|
if (
|
||||||
props.response === null ||
|
props.response === null ||
|
||||||
|
props.response === undefined ||
|
||||||
props.response.type === "loading" ||
|
props.response.type === "loading" ||
|
||||||
props.response.type === "network_fail" ||
|
props.response.type === "network_fail" ||
|
||||||
props.response.type === "script_fail" ||
|
props.response.type === "script_fail" ||
|
||||||
@@ -137,6 +138,7 @@ const readableResponseSize = computed(() => {
|
|||||||
const statusCategory = computed(() => {
|
const statusCategory = computed(() => {
|
||||||
if (
|
if (
|
||||||
props.response === null ||
|
props.response === null ||
|
||||||
|
props.response === undefined ||
|
||||||
props.response.type === "loading" ||
|
props.response.type === "loading" ||
|
||||||
props.response.type === "network_fail" ||
|
props.response.type === "network_fail" ||
|
||||||
props.response.type === "script_fail" ||
|
props.response.type === "script_fail" ||
|
||||||
|
|||||||
@@ -5,13 +5,6 @@
|
|||||||
vertical
|
vertical
|
||||||
render-inactive-tabs
|
render-inactive-tabs
|
||||||
>
|
>
|
||||||
<HoppSmartTab
|
|
||||||
:id="'history'"
|
|
||||||
:icon="IconClock"
|
|
||||||
:label="`${t('tab.history')}`"
|
|
||||||
>
|
|
||||||
<History :page="'rest'" />
|
|
||||||
</HoppSmartTab>
|
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'collections'"
|
:id="'collections'"
|
||||||
:icon="IconFolder"
|
:icon="IconFolder"
|
||||||
@@ -26,6 +19,13 @@
|
|||||||
>
|
>
|
||||||
<Environments />
|
<Environments />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
|
<HoppSmartTab
|
||||||
|
:id="'history'"
|
||||||
|
:icon="IconClock"
|
||||||
|
:label="`${t('tab.history')}`"
|
||||||
|
>
|
||||||
|
<History :page="'rest'" />
|
||||||
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -40,5 +40,5 @@ const t = useI18n()
|
|||||||
|
|
||||||
type RequestOptionTabs = "history" | "collections" | "env"
|
type RequestOptionTabs = "history" | "collections" | "env"
|
||||||
|
|
||||||
const selectedNavigationTab = ref<RequestOptionTabs>("history")
|
const selectedNavigationTab = ref<RequestOptionTabs>("collections")
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -216,7 +216,6 @@ import {
|
|||||||
setGlobalEnvVariables,
|
setGlobalEnvVariables,
|
||||||
setSelectedEnvironmentIndex,
|
setSelectedEnvironmentIndex,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
import { restTestResults$, setRESTTestResults } from "~/newstore/RESTSession"
|
|
||||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||||
|
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
@@ -226,6 +225,17 @@ import IconCheck from "~icons/lucide/check"
|
|||||||
import IconClose from "~icons/lucide/x"
|
import IconClose from "~icons/lucide/x"
|
||||||
|
|
||||||
import { useColorMode } from "~/composables/theming"
|
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 t = useI18n()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
@@ -236,11 +246,6 @@ const displayModalAdd = (shouldDisplay: boolean) => {
|
|||||||
showModalDetails.value = shouldDisplay
|
showModalDetails.value = shouldDisplay
|
||||||
}
|
}
|
||||||
|
|
||||||
const testResults = useReadonlyStream(
|
|
||||||
restTestResults$,
|
|
||||||
null
|
|
||||||
) as Ref<HoppTestResult | null>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the "addition" environment variables
|
* Get the "addition" environment variables
|
||||||
* @returns Array of objects with key-value pairs of arguments
|
* @returns Array of objects with key-value pairs of arguments
|
||||||
@@ -250,7 +255,9 @@ const getAdditionVars = () =>
|
|||||||
? testResults.value.envDiff.selected.additions
|
? testResults.value.envDiff.selected.additions
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const clearContent = () => setRESTTestResults(null)
|
const clearContent = () => {
|
||||||
|
testResults.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const haveEnvVariables = computed(() => {
|
const haveEnvVariables = computed(() => {
|
||||||
if (!testResults.value) return false
|
if (!testResults.value) return false
|
||||||
|
|||||||
@@ -66,17 +66,20 @@ import IconHelpCircle from "~icons/lucide/help-circle"
|
|||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import { reactive, ref } from "vue"
|
import { reactive, ref } from "vue"
|
||||||
import { useTestScript } from "~/newstore/RESTSession"
|
|
||||||
import testSnippets from "~/helpers/testSnippets"
|
import testSnippets from "~/helpers/testSnippets"
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import linter from "~/helpers/editor/linting/testScript"
|
import linter from "~/helpers/editor/linting/testScript"
|
||||||
import completer from "~/helpers/editor/completion/testScript"
|
import completer from "~/helpers/editor/completion/testScript"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
const t = useI18n()
|
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<any | null>(null)
|
const testScriptEditor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const linewrapEnabled = ref(true)
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
|||||||
import { computed, reactive, ref, watch } from "vue"
|
import { computed, reactive, ref, watch } from "vue"
|
||||||
import { isEqual, cloneDeep } from "lodash-es"
|
import { isEqual, cloneDeep } from "lodash-es"
|
||||||
import {
|
import {
|
||||||
|
HoppRESTReqBody,
|
||||||
parseRawKeyValueEntries,
|
parseRawKeyValueEntries,
|
||||||
parseRawKeyValueEntriesE,
|
parseRawKeyValueEntriesE,
|
||||||
rawKeyValueEntriesToString,
|
rawKeyValueEntriesToString,
|
||||||
@@ -194,13 +195,27 @@ import * as E from "fp-ts/Either"
|
|||||||
import draggable from "vuedraggable-es"
|
import draggable from "vuedraggable-es"
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import linter from "~/helpers/editor/linting/rawKeyValue"
|
import linter from "~/helpers/editor/linting/rawKeyValue"
|
||||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
|
||||||
import { pluckRef } from "@composables/ref"
|
import { pluckRef } from "@composables/ref"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { objRemoveKey } from "~/helpers/functional/object"
|
import { objRemoveKey } from "~/helpers/functional/object"
|
||||||
import { throwError } from "~/helpers/functional/error"
|
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 t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -231,7 +246,7 @@ useCodemirror(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// The functional urlEncodedParams list (the urlEncodedParams actually in the system)
|
// The functional urlEncodedParams list (the urlEncodedParams actually in the system)
|
||||||
const urlEncodedParamsRaw = pluckRef(useRESTRequestBody(), "body")
|
const urlEncodedParamsRaw = pluckRef(body, "body")
|
||||||
|
|
||||||
const urlEncodedParams = computed<RawKeyValueEntry[]>({
|
const urlEncodedParams = computed<RawKeyValueEntry[]>({
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput v-model="auth.key" placeholder="Key" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput v-model="auth.value" placeholder="Value" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center border-b border-dividerLight">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<label class="ml-4 text-secondaryLight">
|
||||||
|
{{ t("authorization.pass_key_by") }}
|
||||||
|
</label>
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => authTippyActions.focus()"
|
||||||
|
>
|
||||||
|
<span class="select-wrapper">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="auth.addTo || t('state.none')"
|
||||||
|
class="pr-8 ml-2 rounded-none"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="authTippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
:icon="auth.addTo === 'Headers' ? IconCircleDot : IconCircle"
|
||||||
|
:active="auth.addTo === 'Headers'"
|
||||||
|
:label="'Headers'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
auth.addTo = 'Headers'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
:icon="auth.addTo === 'Query params' ? IconCircleDot : IconCircle"
|
||||||
|
:active="auth.addTo === 'Query params'"
|
||||||
|
:label="'Query params'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
auth.addTo = 'Query params'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconCircle from "~icons/lucide/circle"
|
||||||
|
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { HoppRESTAuthAPIKey } from "@hoppscotch/data"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: HoppRESTAuthAPIKey
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: HoppRESTAuthAPIKey): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const auth = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
|
const authTippyActions = ref<any | null>(null)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="auth.username"
|
||||||
|
:placeholder="t('authorization.username')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="auth.password"
|
||||||
|
:placeholder="t('authorization.password')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { HoppRESTAuthBasic } from "@hoppscotch/data"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: HoppRESTAuthBasic
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: HoppRESTAuthBasic): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const auth = useVModel(props, "modelValue", emit)
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoppSmartTabs
|
<HoppSmartTabs
|
||||||
v-if="response"
|
v-if="tab.response"
|
||||||
v-model="selectedLensTab"
|
v-model="selectedLensTab"
|
||||||
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary top-lowerPrimaryStickyFold"
|
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary top-lowerPrimaryStickyFold"
|
||||||
>
|
>
|
||||||
@@ -11,7 +11,10 @@
|
|||||||
:label="t(lens.lensName)"
|
:label="t(lens.lensName)"
|
||||||
class="flex flex-col flex-1 w-full h-full"
|
class="flex flex-col flex-1 w-full h-full"
|
||||||
>
|
>
|
||||||
<component :is="lensRendererFor(lens.renderer)" :response="response" />
|
<component
|
||||||
|
:is="lensRendererFor(lens.renderer)"
|
||||||
|
:response="tab.response"
|
||||||
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="maybeHeaders"
|
v-if="maybeHeaders"
|
||||||
@@ -26,18 +29,18 @@
|
|||||||
id="results"
|
id="results"
|
||||||
:label="t('test.results')"
|
:label="t('test.results')"
|
||||||
:indicator="
|
:indicator="
|
||||||
testResults &&
|
tab.testResults &&
|
||||||
(testResults.expectResults.length ||
|
(tab.testResults.expectResults.length ||
|
||||||
testResults.tests.length ||
|
tab.testResults.tests.length ||
|
||||||
testResults.envDiff.selected.additions.length ||
|
tab.testResults.envDiff.selected.additions.length ||
|
||||||
testResults.envDiff.selected.updations.length ||
|
tab.testResults.envDiff.selected.updations.length ||
|
||||||
testResults.envDiff.global.updations.length)
|
tab.testResults.envDiff.global.updations.length)
|
||||||
? true
|
? true
|
||||||
: false
|
: false
|
||||||
"
|
"
|
||||||
class="flex flex-col flex-1"
|
class="flex flex-col flex-1"
|
||||||
>
|
>
|
||||||
<HttpTestResult />
|
<HttpTestResult v-model="tab.testResults" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
@@ -49,44 +52,48 @@ import {
|
|||||||
getLensRenderers,
|
getLensRenderers,
|
||||||
Lens,
|
Lens,
|
||||||
} from "~/helpers/lenses/lenses"
|
} from "~/helpers/lenses/lenses"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { restTestResults$ } from "~/newstore/RESTSession"
|
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
response: HoppRESTResponse | null
|
tab: HoppRESTTab
|
||||||
selectedTabPreference: string | null
|
selectedTabPreference: string | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
(e: "update:tab", val: HoppRESTTab): void
|
||||||
(e: "update:selectedTabPreference", newTab: string): void
|
(e: "update:selectedTabPreference", newTab: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const tab = useVModel(props, "tab", emit)
|
||||||
|
const selectedTabPreference = useVModel(props, "selectedTabPreference", emit)
|
||||||
|
|
||||||
const allLensRenderers = getLensRenderers()
|
const allLensRenderers = getLensRenderers()
|
||||||
|
|
||||||
function lensRendererFor(name: string) {
|
function lensRendererFor(name: string) {
|
||||||
return allLensRenderers[name]
|
return allLensRenderers[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
const testResults = useReadonlyStream(restTestResults$, null)
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const selectedLensTab = ref("")
|
const selectedLensTab = ref("")
|
||||||
|
|
||||||
const maybeHeaders = computed(() => {
|
const maybeHeaders = computed(() => {
|
||||||
if (
|
if (
|
||||||
!props.response ||
|
!tab.value.response ||
|
||||||
!(props.response.type === "success" || props.response.type === "fail")
|
!(
|
||||||
|
tab.value.response.type === "success" ||
|
||||||
|
tab.value.response.type === "fail"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
return props.response.headers
|
return tab.value.response.headers
|
||||||
})
|
})
|
||||||
|
|
||||||
const validLenses = computed(() => {
|
const validLenses = computed(() => {
|
||||||
if (!props.response) return []
|
if (!tab.value.response) return []
|
||||||
return getSuitableLenses(props.response)
|
return getSuitableLenses(tab.value.response)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -101,10 +108,10 @@ watch(
|
|||||||
]
|
]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
props.selectedTabPreference &&
|
selectedTabPreference.value &&
|
||||||
validRenderers.includes(props.selectedTabPreference)
|
validRenderers.includes(selectedTabPreference.value)
|
||||||
) {
|
) {
|
||||||
selectedLensTab.value = props.selectedTabPreference
|
selectedLensTab.value = selectedTabPreference.value
|
||||||
} else {
|
} else {
|
||||||
selectedLensTab.value = newLenses[0].renderer
|
selectedLensTab.value = newLenses[0].renderer
|
||||||
}
|
}
|
||||||
@@ -113,6 +120,6 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(selectedLensTab, (newLensID) => {
|
watch(selectedLensTab, (newLensID) => {
|
||||||
emit("update:selectedTabPreference", newLensID)
|
selectedTabPreference.value = newLensID
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
v-if="!loading && myTeams.length === 0"
|
v-if="!loading && myTeams.length === 0"
|
||||||
class="flex flex-col items-center justify-center flex-1 p-4 text-secondaryLight"
|
class="flex flex-col items-center justify-center flex-1 p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-8"
|
||||||
|
:alt="`${t('empty.teams')}`"
|
||||||
|
/>
|
||||||
<span class="mb-4 text-center">
|
<span class="mb-4 text-center">
|
||||||
{{ t("empty.teams") }}
|
{{ t("empty.teams") }}
|
||||||
</span>
|
</span>
|
||||||
@@ -78,12 +84,14 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import IconUser from "~icons/lucide/user"
|
import IconUser from "~icons/lucide/user"
|
||||||
import IconUsers from "~icons/lucide/users"
|
import IconUsers from "~icons/lucide/users"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import { useColorMode } from "@composables/theming"
|
||||||
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
|
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
import IconDone from "~icons/lucide/check"
|
import IconDone from "~icons/lucide/check"
|
||||||
import { useLocalState } from "~/newstore/localstate"
|
import { useLocalState } from "~/newstore/localstate"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
const showModalAdd = ref(false)
|
const showModalAdd = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FormDataKeyValue, HoppRESTRequest } from "@hoppscotch/data"
|
import { FormDataKeyValue, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { getDefaultRESTRequest } from "./rest/default"
|
||||||
import { isJSONContentType } from "./utils/contenttypes"
|
import { isJSONContentType } from "./utils/contenttypes"
|
||||||
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles translations for all the hopp.io REST Shareable URL params
|
* Handles translations for all the hopp.io REST Shareable URL params
|
||||||
|
|||||||
293
packages/hoppscotch-common/src/helpers/RESTRequest.ts
Normal file
293
packages/hoppscotch-common/src/helpers/RESTRequest.ts
Normal file
@@ -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<typeof RESTReqSchemaVersion>(
|
||||||
|
RESTReqSchemaVersion
|
||||||
|
)
|
||||||
|
public name$ = new BehaviorSubject("Untitled")
|
||||||
|
public endpoint$ = new BehaviorSubject("https://echo.hoppscotch.io/")
|
||||||
|
public params$ = new BehaviorSubject<HoppRESTParam[]>([])
|
||||||
|
public headers$ = new BehaviorSubject<HoppRESTHeader[]>([])
|
||||||
|
public method$ = new BehaviorSubject("GET")
|
||||||
|
public auth$ = new BehaviorSubject<HoppRESTAuth>({
|
||||||
|
authType: "none",
|
||||||
|
authActive: true,
|
||||||
|
})
|
||||||
|
public preRequestScript$ = new BehaviorSubject("")
|
||||||
|
public testScript$ = new BehaviorSubject("")
|
||||||
|
public body$ = new BehaviorSubject<HoppRESTReqBody>({
|
||||||
|
contentType: null,
|
||||||
|
body: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
public response$ = new BehaviorSubject<HoppRESTResponse | null>(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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Observable } from "rxjs"
|
import { Observable, Subject } from "rxjs"
|
||||||
import { filter } from "rxjs/operators"
|
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 { flow, pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
@@ -22,7 +22,6 @@ import { createRESTNetworkRequestStream } from "./network"
|
|||||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||||
import { isJSONContentType } from "./utils/contenttypes"
|
import { isJSONContentType } from "./utils/contenttypes"
|
||||||
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
|
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
|
||||||
import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
|
|
||||||
import {
|
import {
|
||||||
environmentsStore,
|
environmentsStore,
|
||||||
getCurrentEnvironment,
|
getCurrentEnvironment,
|
||||||
@@ -31,6 +30,8 @@ import {
|
|||||||
setGlobalEnvVariables,
|
setGlobalEnvVariables,
|
||||||
updateEnvironment,
|
updateEnvironment,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
|
import { HoppRESTTab } from "./rest/tab"
|
||||||
|
import { Ref } from "vue"
|
||||||
|
|
||||||
const getTestableBody = (
|
const getTestableBody = (
|
||||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||||
@@ -64,20 +65,26 @@ const combineEnvVariables = (env: {
|
|||||||
selected: Environment["variables"]
|
selected: Environment["variables"]
|
||||||
}) => [...env.selected, ...env.global]
|
}) => [...env.selected, ...env.global]
|
||||||
|
|
||||||
export const runRESTRequest$ = (): TaskEither<
|
export const executedResponses$ = new Subject<
|
||||||
string | Error,
|
HoppRESTResponse & { type: "success" | "fail " }
|
||||||
Observable<HoppRESTResponse>
|
>()
|
||||||
> =>
|
|
||||||
|
export const runRESTRequest$ = (
|
||||||
|
tab: Ref<HoppRESTTab>
|
||||||
|
): TE.TaskEither<string | Error, Observable<HoppRESTResponse>> =>
|
||||||
pipe(
|
pipe(
|
||||||
getFinalEnvsFromPreRequest(
|
getFinalEnvsFromPreRequest(
|
||||||
getRESTRequest().preRequestScript,
|
tab.value.document.request.preRequestScript,
|
||||||
getCombinedEnvVariables()
|
getCombinedEnvVariables()
|
||||||
),
|
),
|
||||||
chain((envs) => {
|
TE.chain((envs) => {
|
||||||
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
|
const effectiveRequest = getEffectiveRESTRequest(
|
||||||
|
tab.value.document.request,
|
||||||
|
{
|
||||||
name: "Env",
|
name: "Env",
|
||||||
variables: combineEnvVariables(envs),
|
variables: combineEnvVariables(envs),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const stream = createRESTNetworkRequestStream(effectiveRequest)
|
const stream = createRESTNetworkRequestStream(effectiveRequest)
|
||||||
|
|
||||||
@@ -86,6 +93,11 @@ export const runRESTRequest$ = (): TaskEither<
|
|||||||
.pipe(filter((res) => res.type === "success" || res.type === "fail"))
|
.pipe(filter((res) => res.type === "success" || res.type === "fail"))
|
||||||
.subscribe(async (res) => {
|
.subscribe(async (res) => {
|
||||||
if (res.type === "success" || res.type === "fail") {
|
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, {
|
const runResult = await runTestScript(res.req.testScript, envs, {
|
||||||
status: res.statusCode,
|
status: res.statusCode,
|
||||||
body: getTestableBody(res),
|
body: getTestableBody(res),
|
||||||
@@ -93,7 +105,9 @@ export const runRESTRequest$ = (): TaskEither<
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
if (isRight(runResult)) {
|
if (isRight(runResult)) {
|
||||||
setRESTTestResults(translateToSandboxTestResults(runResult.right))
|
tab.value.testResults = translateToSandboxTestResults(
|
||||||
|
runResult.right
|
||||||
|
)
|
||||||
|
|
||||||
setGlobalEnvVariables(runResult.right.envs.global)
|
setGlobalEnvVariables(runResult.right.envs.global)
|
||||||
|
|
||||||
@@ -128,7 +142,7 @@ export const runRESTRequest$ = (): TaskEither<
|
|||||||
)()
|
)()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setRESTTestResults({
|
tab.value.testResults = {
|
||||||
description: "",
|
description: "",
|
||||||
expectResults: [],
|
expectResults: [],
|
||||||
tests: [],
|
tests: [],
|
||||||
@@ -145,14 +159,14 @@ export const runRESTRequest$ = (): TaskEither<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
scriptError: true,
|
scriptError: true,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return right(stream)
|
return TE.right(stream)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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<number, number>()
|
||||||
|
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
|
||||||
|
}
|
||||||
141
packages/hoppscotch-common/src/helpers/collection/collection.ts
Normal file
141
packages/hoppscotch-common/src/helpers/collection/collection.ts
Normal file
@@ -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<string, string>()
|
||||||
|
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<HoppRESTRequest>[],
|
||||||
|
path: string
|
||||||
|
): HoppCollection<HoppRESTRequest>[] {
|
||||||
|
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
|
||||||
|
}
|
||||||
72
packages/hoppscotch-common/src/helpers/collection/request.ts
Normal file
72
packages/hoppscotch-common/src/helpers/collection/request.ts
Normal file
@@ -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<HoppRESTRequest>[],
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import { getMethod } from "./sub_helpers/method"
|
|||||||
import { concatParams, getURLObject } from "./sub_helpers/url"
|
import { concatParams, getURLObject } from "./sub_helpers/url"
|
||||||
import { preProcessCurlCommand } from "./sub_helpers/preproc"
|
import { preProcessCurlCommand } from "./sub_helpers/preproc"
|
||||||
import { getBody, getFArgumentMultipartData } from "./sub_helpers/body"
|
import { getBody, getFArgumentMultipartData } from "./sub_helpers/body"
|
||||||
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
import { getDefaultRESTRequest } from "../rest/default"
|
||||||
import {
|
import {
|
||||||
objHasProperty,
|
objHasProperty,
|
||||||
objHasArrayProperty,
|
objHasArrayProperty,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import parser from "yargs-parser"
|
|||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as S from "fp-ts/string"
|
import * as S from "fp-ts/string"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { objHasProperty } from "~/helpers/functional/object"
|
import { objHasProperty } from "~/helpers/functional/object"
|
||||||
|
|
||||||
const defaultRESTReq = getDefaultRESTRequest()
|
const defaultRESTReq = getDefaultRESTRequest()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import parser from "yargs-parser"
|
|||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as R from "fp-ts/Refinement"
|
import * as R from "fp-ts/Refinement"
|
||||||
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import {
|
import {
|
||||||
objHasProperty,
|
objHasProperty,
|
||||||
objHasArrayProperty,
|
objHasArrayProperty,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import parser from "yargs-parser"
|
|||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import { getDefaultRESTRequest } from "~/newstore/RESTSession"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { stringArrayJoin } from "~/helpers/functional/array"
|
import { stringArrayJoin } from "~/helpers/functional/array"
|
||||||
|
|
||||||
const defaultRESTReq = getDefaultRESTRequest()
|
const defaultRESTReq = getDefaultRESTRequest()
|
||||||
|
|||||||
@@ -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<HoppRESTRequest | null> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
20
packages/hoppscotch-common/src/helpers/rest/default.ts
Normal file
20
packages/hoppscotch-common/src/helpers/rest/default.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
58
packages/hoppscotch-common/src/helpers/rest/document.ts
Normal file
58
packages/hoppscotch-common/src/helpers/rest/document.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
25
packages/hoppscotch-common/src/helpers/rest/labelColoring.ts
Normal file
25
packages/hoppscotch-common/src/helpers/rest/labelColoring.ts
Normal file
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
199
packages/hoppscotch-common/src/helpers/rest/tab.ts
Normal file
199
packages/hoppscotch-common/src/helpers/rest/tab.ts
Normal file
@@ -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<string, HoppRESTTab>([
|
||||||
|
[
|
||||||
|
"test",
|
||||||
|
{
|
||||||
|
id: "test",
|
||||||
|
document: {
|
||||||
|
request: getDefaultRESTRequest(),
|
||||||
|
isDirty: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
)
|
||||||
|
const tabOrdering = ref<string[]>(["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<PersistableRESTTabState>(() => ({
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { HoppRESTRequest } from "@hoppscotch/data"
|
|||||||
* We use the save context to figure out
|
* We use the save context to figure out
|
||||||
* how a loaded request is to be saved.
|
* how a loaded request is to be saved.
|
||||||
* These will be set when the request is loaded
|
* These will be set when the request is loaded
|
||||||
* into the request session (RESTSession)
|
* into the request session
|
||||||
*/
|
*/
|
||||||
export type HoppRequestSaveContext =
|
export type HoppRequestSaveContext =
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -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: <HoppRESTReqBody>{
|
|
||||||
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: <HoppRESTReqBody>{
|
|
||||||
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: <HoppRESTReqBody>{
|
|
||||||
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: <HoppRESTReqBody>{
|
|
||||||
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<string> {
|
|
||||||
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<string> {
|
|
||||||
return useStream(
|
|
||||||
restTestScript$,
|
|
||||||
restSessionStore.value.request.testScript,
|
|
||||||
(value) => {
|
|
||||||
setRESTTestScript(value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRESTRequestBody(): Ref<HoppRESTReqBody> {
|
|
||||||
return useStream(
|
|
||||||
restReqBody$,
|
|
||||||
restSessionStore.value.request.body,
|
|
||||||
setRESTReqBody
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRESTRequestName(): Ref<string> {
|
|
||||||
return useStream(
|
|
||||||
restRequestName$,
|
|
||||||
restSessionStore.value.request.name,
|
|
||||||
setRESTRequestName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
makeCollection,
|
makeCollection,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||||
import { getRESTSaveContext, setRESTSaveContext } from "./RESTSession"
|
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
|
import { getTabRefWithSaveContext } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const defaultRESTCollectionState = {
|
const defaultRESTCollectionState = {
|
||||||
state: [
|
state: [
|
||||||
@@ -400,14 +400,17 @@ const restCollectionDispatchers = defineDispatchers({
|
|||||||
|
|
||||||
targetLocation.requests.splice(requestIndex, 1)
|
targetLocation.requests.splice(requestIndex, 1)
|
||||||
|
|
||||||
// If the save context is set and is set to the same source, we invalidate it
|
// Deal with situations where a tab with the given thing is deleted
|
||||||
const saveCtx = getRESTSaveContext()
|
// We are just going to dissociate the save context of the tab and mark it dirty
|
||||||
if (
|
const tab = getTabRefWithSaveContext({
|
||||||
saveCtx?.originLocation === "user-collection" &&
|
originLocation: "user-collection",
|
||||||
saveCtx.folderPath === path &&
|
folderPath: path,
|
||||||
saveCtx.requestIndex === requestIndex
|
requestIndex: requestIndex,
|
||||||
) {
|
})
|
||||||
setRESTSaveContext(null)
|
|
||||||
|
if (tab) {
|
||||||
|
tab.value.document.saveContext = undefined
|
||||||
|
tab.value.document.isDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -457,6 +460,20 @@ const restCollectionDispatchers = defineDispatchers({
|
|||||||
destLocation.requests.push(req)
|
destLocation.requests.push(req)
|
||||||
targetLocation.requests.splice(requestIndex, 1)
|
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 {
|
return {
|
||||||
state: newState,
|
state: newState,
|
||||||
}
|
}
|
||||||
@@ -719,16 +736,6 @@ const gqlCollectionDispatchers = defineDispatchers({
|
|||||||
|
|
||||||
targetLocation.requests.splice(requestIndex, 1)
|
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 {
|
return {
|
||||||
state: newState,
|
state: newState,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
GQL_REQ_SCHEMA_VERSION,
|
GQL_REQ_SCHEMA_VERSION,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||||
import { completedRESTResponse$ } from "./RESTSession"
|
import { executedResponses$ } from "~/helpers/RequestRunner"
|
||||||
|
|
||||||
export type RESTHistoryEntry = {
|
export type RESTHistoryEntry = {
|
||||||
v: number
|
v: number
|
||||||
@@ -340,15 +340,7 @@ export function removeDuplicateGraphqlHistoryEntry(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Listen to completed responses to add to history
|
// Listen to completed responses to add to history
|
||||||
completedRESTResponse$.subscribe((res) => {
|
executedResponses$.subscribe((res) => {
|
||||||
if (res !== null) {
|
|
||||||
if (
|
|
||||||
res.type === "loading" ||
|
|
||||||
res.type === "network_fail" ||
|
|
||||||
res.type === "script_fail"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
addRESTHistoryEntry(
|
addRESTHistoryEntry(
|
||||||
makeRESTHistoryEntry({
|
makeRESTHistoryEntry({
|
||||||
request: {
|
request: {
|
||||||
@@ -371,5 +363,4 @@ completedRESTResponse$.subscribe((res) => {
|
|||||||
updatedOn: new Date(),
|
updatedOn: new Date(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable no-restricted-globals, no-restricted-syntax */
|
/* 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 * as O from "fp-ts/Option"
|
||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import {
|
import {
|
||||||
safelyExtractRESTRequest,
|
|
||||||
translateToNewRequest,
|
|
||||||
translateToNewRESTCollection,
|
translateToNewRESTCollection,
|
||||||
translateToNewGQLCollection,
|
translateToNewGQLCollection,
|
||||||
Environment,
|
Environment,
|
||||||
@@ -41,17 +39,16 @@ import {
|
|||||||
setSelectedEnvironmentIndex,
|
setSelectedEnvironmentIndex,
|
||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
} from "./environments"
|
} from "./environments"
|
||||||
import {
|
|
||||||
getDefaultRESTRequest,
|
|
||||||
restRequest$,
|
|
||||||
setRESTRequest,
|
|
||||||
} from "./RESTSession"
|
|
||||||
import { WSRequest$, setWSRequest } from "./WebSocketSession"
|
import { WSRequest$, setWSRequest } from "./WebSocketSession"
|
||||||
import { SIORequest$, setSIORequest } from "./SocketIOSession"
|
import { SIORequest$, setSIORequest } from "./SocketIOSession"
|
||||||
import { SSERequest$, setSSERequest } from "./SSESession"
|
import { SSERequest$, setSSERequest } from "./SSESession"
|
||||||
import { MQTTRequest$, setMQTTRequest } from "./MQTTSession"
|
import { MQTTRequest$, setMQTTRequest } from "./MQTTSession"
|
||||||
import { bulkApplyLocalState, localStateStore } from "./localstate"
|
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() {
|
function checkAndMigrateOldSettings() {
|
||||||
const vuexData = JSON.parse(window.localStorage.getItem("vuex") || "{}")
|
const vuexData = JSON.parse(window.localStorage.getItem("vuex") || "{}")
|
||||||
@@ -305,33 +302,28 @@ function setupGlobalEnvsPersistence() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupRequestPersistence() {
|
// TODO: Graceful error handling ?
|
||||||
const localRequest = JSON.parse(
|
export function setupRESTTabsPersistence() {
|
||||||
window.localStorage.getItem("restRequest") || "null"
|
try {
|
||||||
)
|
const state = window.localStorage.getItem("restTabState")
|
||||||
|
if (state) {
|
||||||
if (localRequest) {
|
const data = JSON.parse(state)
|
||||||
const parsedLocal = translateToNewRequest(localRequest)
|
loadTabsFromPersistedState(data)
|
||||||
setRESTRequest(
|
}
|
||||||
safelyExtractRESTRequest(parsedLocal, getDefaultRESTRequest())
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Failed parsing persisted tab state, state:`,
|
||||||
|
window.localStorage.getItem("restTabState")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
restRequest$.subscribe((req) => {
|
watchDebounced(
|
||||||
const reqClone = cloneDeep(req)
|
persistableTabState,
|
||||||
if (reqClone.body.contentType === "multipart/form-data") {
|
(state) => {
|
||||||
reqClone.body.body = reqClone.body.body.map((x) => {
|
window.localStorage.setItem("restTabState", JSON.stringify(state))
|
||||||
if (x.isFile)
|
},
|
||||||
return {
|
{ debounce: 500, deep: true }
|
||||||
...x,
|
)
|
||||||
isFile: false,
|
|
||||||
value: "",
|
|
||||||
}
|
|
||||||
else return x
|
|
||||||
})
|
|
||||||
}
|
|
||||||
window.localStorage.setItem("restRequest", JSON.stringify(reqClone))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupLocalPersistence() {
|
export function setupLocalPersistence() {
|
||||||
@@ -339,7 +331,7 @@ export function setupLocalPersistence() {
|
|||||||
|
|
||||||
setupLocalStatePersistence()
|
setupLocalStatePersistence()
|
||||||
setupSettingsPersistence()
|
setupSettingsPersistence()
|
||||||
setupRequestPersistence()
|
setupRESTTabsPersistence()
|
||||||
setupHistoryPersistence()
|
setupHistoryPersistence()
|
||||||
setupCollectionsPersistence()
|
setupCollectionsPersistence()
|
||||||
setupGlobalEnvsPersistence()
|
setupGlobalEnvsPersistence()
|
||||||
|
|||||||
@@ -1,51 +1,109 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppPaneLayout layout-id="http">
|
<AppPaneLayout layout-id="http">
|
||||||
<template #primary>
|
<template #primary>
|
||||||
<HttpRequest />
|
<HoppSmartWindows
|
||||||
<HttpRequestOptions />
|
v-if="currentTabID"
|
||||||
|
:id="'rest_windows'"
|
||||||
|
v-model="currentTabID"
|
||||||
|
@remove-tab="removeTab"
|
||||||
|
@add-tab="addNewTab"
|
||||||
|
@sort="sortTabs"
|
||||||
|
>
|
||||||
|
<HoppSmartWindow
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="tab.id"
|
||||||
|
:label="tab.document.request.name"
|
||||||
|
:is-removable="tabs.length > 1"
|
||||||
|
>
|
||||||
|
<template #tabhead>
|
||||||
|
<span
|
||||||
|
class="font-semibold truncate text-tiny w-10"
|
||||||
|
:class="getMethodLabelColorClassOf(tab.document.request)"
|
||||||
|
>
|
||||||
|
{{ tab.document.request.method }}
|
||||||
|
</span>
|
||||||
|
<span class="text-green-600 mr-1" v-if="tab.document.isDirty">
|
||||||
|
•
|
||||||
|
</span>
|
||||||
|
<span class="truncate flex-1">
|
||||||
|
{{ tab.document.request.name }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template #secondary>
|
|
||||||
<HttpResponse />
|
<HttpRequestTab
|
||||||
|
:model-value="tab"
|
||||||
|
@update:model-value="onTabUpdate"
|
||||||
|
/>
|
||||||
|
</HoppSmartWindow>
|
||||||
|
</HoppSmartWindows>
|
||||||
</template>
|
</template>
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<HttpSidebar />
|
<HttpSidebar />
|
||||||
</template>
|
</template>
|
||||||
</AppPaneLayout>
|
</AppPaneLayout>
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="confirmingCloseForTabID !== null"
|
||||||
|
:confirm="t('modal.close_unsaved_tab')"
|
||||||
|
:title="t('confirm.save_unsaved_tab')"
|
||||||
|
@hide-modal="onCloseConfirmSaveTab"
|
||||||
|
@resolve="onResolveConfirmSaveTab"
|
||||||
|
/>
|
||||||
|
<CollectionsSaveRequest
|
||||||
|
:show="savingRequest"
|
||||||
|
:mode="'rest'"
|
||||||
|
@hide-modal="onSaveModalClose"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import {
|
import { ref, onMounted, onBeforeUnmount, watch, onBeforeMount } from "vue"
|
||||||
defineComponent,
|
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
||||||
onBeforeMount,
|
|
||||||
onBeforeUnmount,
|
|
||||||
onMounted,
|
|
||||||
Ref,
|
|
||||||
ref,
|
|
||||||
watch,
|
|
||||||
} from "vue"
|
|
||||||
import type { Subscription } from "rxjs"
|
|
||||||
import {
|
|
||||||
HoppRESTRequest,
|
|
||||||
HoppRESTAuthOAuth2,
|
|
||||||
safelyExtractRESTRequest,
|
|
||||||
isEqualHoppRESTRequest,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import {
|
|
||||||
getRESTRequest,
|
|
||||||
setRESTRequest,
|
|
||||||
setRESTAuth,
|
|
||||||
restAuth$,
|
|
||||||
getDefaultRESTRequest,
|
|
||||||
} from "~/newstore/RESTSession"
|
|
||||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||||
import { pluckRef } from "@composables/ref"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useStream } from "@composables/stream"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { onLoggedIn } from "@composables/auth"
|
|
||||||
import { loadRequestFromSync, startRequestSync } from "~/helpers/fb/request"
|
|
||||||
import { oauthRedirect } from "~/helpers/oauth"
|
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
|
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import {
|
||||||
|
closeTab,
|
||||||
|
createNewTab,
|
||||||
|
currentActiveTab,
|
||||||
|
currentTabID,
|
||||||
|
getActiveTabs,
|
||||||
|
getTabRef,
|
||||||
|
HoppRESTTab,
|
||||||
|
loadTabsFromPersistedState,
|
||||||
|
persistableTabState,
|
||||||
|
updateTab,
|
||||||
|
updateTabOrdering,
|
||||||
|
} from "~/helpers/rest/tab"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
import { onLoggedIn } from "~/composables/auth"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import {
|
||||||
|
audit,
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
EMPTY,
|
||||||
|
from,
|
||||||
|
map,
|
||||||
|
Subscription,
|
||||||
|
} from "rxjs"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { PersistableRESTTabState } from "~/helpers/rest/tab"
|
||||||
|
import { watchDebounced } from "@vueuse/core"
|
||||||
|
import { oauthRedirect } from "~/helpers/oauth"
|
||||||
|
|
||||||
|
const savingRequest = ref(false)
|
||||||
|
const confirmingCloseForTabID = ref<string | null>(null)
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const tabs = getActiveTabs()
|
||||||
|
|
||||||
|
const confirmSync = ref(false)
|
||||||
|
const tabStateForSync = ref<PersistableRESTTabState | null>(null)
|
||||||
|
|
||||||
function bindRequestToURLParams() {
|
function bindRequestToURLParams() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -55,89 +113,85 @@ function bindRequestToURLParams() {
|
|||||||
// If query params are empty, or contains code or error param (these are from Oauth Redirect)
|
// If query params are empty, or contains code or error param (these are from Oauth Redirect)
|
||||||
// We skip URL params parsing
|
// We skip URL params parsing
|
||||||
if (Object.keys(query).length === 0 || query.code || query.error) return
|
if (Object.keys(query).length === 0 || query.code || query.error) return
|
||||||
setRESTRequest(
|
currentActiveTab.value.document.request = safelyExtractRESTRequest(
|
||||||
safelyExtractRESTRequest(
|
|
||||||
translateExtURLParams(query),
|
translateExtURLParams(query),
|
||||||
getDefaultRESTRequest()
|
getDefaultRESTRequest()
|
||||||
)
|
)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function oAuthURL() {
|
const onTabUpdate = (tab: HoppRESTTab) => {
|
||||||
const auth = useStream(
|
updateTab(tab)
|
||||||
restAuth$,
|
|
||||||
{ authType: "none", authActive: true },
|
|
||||||
setRESTAuth
|
|
||||||
)
|
|
||||||
|
|
||||||
const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
try {
|
|
||||||
const tokenInfo = await oauthRedirect()
|
|
||||||
if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
|
|
||||||
if (typeof tokenInfo === "object") {
|
|
||||||
oauth2Token.value = tokenInfo.access_token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty
|
|
||||||
} catch (_) {}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupRequestSync(
|
const addNewTab = () => {
|
||||||
confirmSync: Ref<boolean>,
|
const tab = createNewTab({
|
||||||
requestForSync: Ref<HoppRESTRequest | null>
|
request: getDefaultRESTRequest(),
|
||||||
) {
|
isDirty: false,
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
// Subscription to request sync
|
|
||||||
let sub: Subscription | null = null
|
|
||||||
|
|
||||||
// Load request on login resolve and start sync
|
|
||||||
onLoggedIn(async () => {
|
|
||||||
if (
|
|
||||||
Object.keys(route.query).length === 0 &&
|
|
||||||
!(route.query.code || route.query.error)
|
|
||||||
) {
|
|
||||||
const request = await loadRequestFromSync()
|
|
||||||
if (request) {
|
|
||||||
if (!isEqualHoppRESTRequest(request, getRESTRequest())) {
|
|
||||||
requestForSync.value = request
|
|
||||||
confirmSync.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub = startRequestSync()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Stop subscription to stop syncing
|
currentTabID.value = tab.id
|
||||||
onBeforeUnmount(() => {
|
}
|
||||||
sub?.unsubscribe()
|
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
|
||||||
})
|
updateTabOrdering(e.oldIndex, e.newIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
const removeTab = (tabID: string) => {
|
||||||
setup() {
|
const tab = getTabRef(tabID)
|
||||||
const requestForSync = ref<HoppRESTRequest | null>(null)
|
|
||||||
|
|
||||||
const confirmSync = ref(false)
|
if (tab.value.document.isDirty) {
|
||||||
|
confirmingCloseForTabID.value = tabID
|
||||||
|
} else {
|
||||||
|
closeTab(tab.value.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toast = useToast()
|
/**
|
||||||
const t = useI18n()
|
* This function is closed when the confirm tab is closed by some means (even saving triggers close)
|
||||||
|
*/
|
||||||
|
const onCloseConfirmSaveTab = () => {
|
||||||
|
if (!savingRequest.value && confirmingCloseForTabID.value) {
|
||||||
|
closeTab(confirmingCloseForTabID.value)
|
||||||
|
confirmingCloseForTabID.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(confirmSync, (newValue) => {
|
/**
|
||||||
|
* Called when the user confirms they want to save the tab
|
||||||
|
*/
|
||||||
|
const onResolveConfirmSaveTab = () => {
|
||||||
|
if (currentActiveTab.value.document.saveContext) {
|
||||||
|
invokeAction("request.save")
|
||||||
|
|
||||||
|
if (confirmingCloseForTabID.value) {
|
||||||
|
closeTab(confirmingCloseForTabID.value)
|
||||||
|
confirmingCloseForTabID.value = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
savingRequest.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the Save Request modal is done and is closed
|
||||||
|
*/
|
||||||
|
const onSaveModalClose = () => {
|
||||||
|
savingRequest.value = false
|
||||||
|
if (confirmingCloseForTabID.value) {
|
||||||
|
closeTab(confirmingCloseForTabID.value)
|
||||||
|
confirmingCloseForTabID.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(confirmSync, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
toast.show(`${t("confirm.sync")}`, {
|
toast.show(t("confirm.sync"), {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
action: [
|
action: [
|
||||||
{
|
{
|
||||||
text: `${t("action.yes")}`,
|
text: `${t("action.yes")}`,
|
||||||
onClick: (_, toastObject) => {
|
onClick: (_, toastObject) => {
|
||||||
syncRequest()
|
syncTabState()
|
||||||
toastObject.goAway(0)
|
toastObject.goAway(0)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -150,24 +204,98 @@ export default defineComponent({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const syncTabState = () => {
|
||||||
|
if (tabStateForSync.value) loadTabsFromPersistedState(tabStateForSync.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs sync of the REST Tab session with Firestore.
|
||||||
|
*
|
||||||
|
* @returns A subscription to the sync observable stream.
|
||||||
|
* Unsubscribe to stop syncing.
|
||||||
|
*/
|
||||||
|
function startTabStateSync(): Subscription {
|
||||||
|
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||||
|
const tabState$ = new BehaviorSubject<PersistableRESTTabState | null>(null)
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
persistableTabState,
|
||||||
|
(state) => {
|
||||||
|
tabState$.next(state)
|
||||||
|
},
|
||||||
|
{ debounce: 500, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const sub = combineLatest([currentUser$, tabState$])
|
||||||
|
.pipe(
|
||||||
|
map(([user, tabState]) =>
|
||||||
|
user && tabState
|
||||||
|
? from(platform.sync.tabState.writeCurrentTabState(user, tabState))
|
||||||
|
: EMPTY
|
||||||
|
),
|
||||||
|
audit((x) => x)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
// NOTE: This subscription should be kept
|
||||||
})
|
})
|
||||||
|
|
||||||
const syncRequest = () => {
|
return sub
|
||||||
setRESTRequest(
|
}
|
||||||
safelyExtractRESTRequest(requestForSync.value!, getDefaultRESTRequest())
|
|
||||||
)
|
function setupTabStateSync() {
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// Subscription to request sync
|
||||||
|
let sub: Subscription | null = null
|
||||||
|
|
||||||
|
// Load request on login resolve and start sync
|
||||||
|
onLoggedIn(async () => {
|
||||||
|
if (
|
||||||
|
Object.keys(route.query).length === 0 &&
|
||||||
|
!(route.query.code || route.query.error)
|
||||||
|
) {
|
||||||
|
const tabStateFromSync =
|
||||||
|
await platform.sync.tabState.loadTabStateFromSync()
|
||||||
|
|
||||||
|
if (tabStateFromSync) {
|
||||||
|
tabStateForSync.value = tabStateFromSync
|
||||||
|
confirmSync.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupRequestSync(confirmSync, requestForSync)
|
sub = startTabStateSync()
|
||||||
bindRequestToURLParams()
|
})
|
||||||
oAuthURL()
|
|
||||||
|
|
||||||
return {
|
// Stop subscription to stop syncing
|
||||||
confirmSync,
|
onBeforeUnmount(() => {
|
||||||
syncRequest,
|
sub?.unsubscribe()
|
||||||
oAuthURL,
|
})
|
||||||
requestForSync,
|
}
|
||||||
|
|
||||||
|
function oAuthURL() {
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
try {
|
||||||
|
const tokenInfo = await oauthRedirect()
|
||||||
|
if (
|
||||||
|
typeof tokenInfo === "object" &&
|
||||||
|
tokenInfo.hasOwnProperty("access_token")
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
currentActiveTab.value.document.request.auth.authType === "oauth-2"
|
||||||
|
) {
|
||||||
|
currentActiveTab.value.document.request.auth.token =
|
||||||
|
tokenInfo.access_token
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (_) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTabStateSync()
|
||||||
|
bindRequestToURLParams()
|
||||||
|
oAuthURL()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -71,10 +71,11 @@ import {
|
|||||||
ResolveShortcodeQuery,
|
ResolveShortcodeQuery,
|
||||||
ResolveShortcodeQueryVariables,
|
ResolveShortcodeQueryVariables,
|
||||||
} from "~/helpers/backend/graphql"
|
} from "~/helpers/backend/graphql"
|
||||||
import { getDefaultRESTRequest, setRESTRequest } from "~/newstore/RESTSession"
|
|
||||||
|
|
||||||
import IconHome from "~icons/lucide/home"
|
import IconHome from "~icons/lucide/home"
|
||||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||||
|
import { createNewTab } from "~/helpers/rest/tab"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
@@ -106,9 +107,10 @@ export default defineComponent({
|
|||||||
data.right.shortcode?.request as string
|
data.right.shortcode?.request as string
|
||||||
)
|
)
|
||||||
|
|
||||||
setRESTRequest(
|
createNewTab({
|
||||||
safelyExtractRESTRequest(request, getDefaultRESTRequest())
|
request: safelyExtractRESTRequest(request, getDefaultRESTRequest()),
|
||||||
)
|
isDirty: false,
|
||||||
|
})
|
||||||
|
|
||||||
router.push({ path: "/" })
|
router.push({ path: "/" })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { EnvironmentsPlatformDef } from "./environments"
|
|||||||
import { CollectionsPlatformDef } from "./collections"
|
import { CollectionsPlatformDef } from "./collections"
|
||||||
import { SettingsPlatformDef } from "./settings"
|
import { SettingsPlatformDef } from "./settings"
|
||||||
import { HistoryPlatformDef } from "./history"
|
import { HistoryPlatformDef } from "./history"
|
||||||
|
import { TabStatePlatformDef } from "./tab"
|
||||||
|
|
||||||
export type PlatformDef = {
|
export type PlatformDef = {
|
||||||
ui?: UIPlatformDef
|
ui?: UIPlatformDef
|
||||||
@@ -13,6 +14,7 @@ export type PlatformDef = {
|
|||||||
collections: CollectionsPlatformDef
|
collections: CollectionsPlatformDef
|
||||||
settings: SettingsPlatformDef
|
settings: SettingsPlatformDef
|
||||||
history: HistoryPlatformDef
|
history: HistoryPlatformDef
|
||||||
|
tabState: TabStatePlatformDef
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
packages/hoppscotch-common/src/platform/tab.ts
Normal file
10
packages/hoppscotch-common/src/platform/tab.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { PersistableRESTTabState } from "~/helpers/rest/tab"
|
||||||
|
import { HoppUser } from "./auth"
|
||||||
|
|
||||||
|
export type TabStatePlatformDef = {
|
||||||
|
loadTabStateFromSync: () => Promise<PersistableRESTTabState | null>
|
||||||
|
writeCurrentTabState: (
|
||||||
|
user: HoppUser,
|
||||||
|
persistableTabState: PersistableRESTTabState
|
||||||
|
) => Promise<void>
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="active" class="flex flex-col flex-1 overflow-y-auto">
|
<div
|
||||||
|
v-if="shouldRender"
|
||||||
|
v-show="active"
|
||||||
|
class="flex flex-col flex-1 overflow-y-auto"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -27,16 +31,33 @@ const props = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabMeta = computed<TabMeta>(() => ({
|
const tabMeta = computed<TabMeta>(() => ({
|
||||||
info: props.info,
|
info: props.info,
|
||||||
label: props.label,
|
label: props.label,
|
||||||
isRemovable: props.isRemovable,
|
isRemovable: props.isRemovable,
|
||||||
icon: slots.icon,
|
icon: slots.icon,
|
||||||
|
tabhead: slots.tabhead
|
||||||
}))
|
}))
|
||||||
const { activeTabID, addTabEntry, updateTabEntry, removeTabEntry } =
|
|
||||||
inject<TabProvider>("tabs-system")!
|
const {
|
||||||
|
activeTabID,
|
||||||
|
renderInactive,
|
||||||
|
addTabEntry,
|
||||||
|
updateTabEntry,
|
||||||
|
removeTabEntry,
|
||||||
|
} = inject<TabProvider>("tabs-system")!
|
||||||
|
|
||||||
const active = computed(() => activeTabID.value === props.id)
|
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(() => {
|
onMounted(() => {
|
||||||
addTabEntry(props.id, tabMeta.value)
|
addTabEntry(props.id, tabMeta.value)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
||||||
<div class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto tabs bg-primaryLight">
|
<div class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto tabs bg-primaryLight">
|
||||||
<div class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto">
|
<div class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto" ref="scrollContainer">
|
||||||
<div class="flex justify-between divide-x divide-dividerLight">
|
<div class="flex justify-between divide-x divide-dividerLight" @wheel="scrollOnWindows">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<draggable v-bind="dragOptions" :list="tabEntries" :style="tabStyles" :item-key="'window-'"
|
<draggable v-bind="dragOptions" :list="tabEntries" :style="tabStyles" :item-key="'window-'"
|
||||||
class="flex flex-shrink-0 overflow-x-auto transition divide-x divide-dividerLight" @sort="sortTabs">
|
class="flex flex-shrink-0 overflow-x-auto transition divide-x divide-dividerLight" @sort="sortTabs">
|
||||||
@@ -10,16 +10,22 @@
|
|||||||
<button :key="`removable-tab-${tabID}`" class="tab" :class="[{ active: modelValue === tabID }]"
|
<button :key="`removable-tab-${tabID}`" class="tab" :class="[{ active: modelValue === tabID }]"
|
||||||
:aria-label="tabMeta.label || ''" role="button" @keyup.enter="selectTab(tabID)"
|
:aria-label="tabMeta.label || ''" role="button" @keyup.enter="selectTab(tabID)"
|
||||||
@click="selectTab(tabID)">
|
@click="selectTab(tabID)">
|
||||||
<div class="flex items-stretch truncate">
|
|
||||||
|
<div v-if="!tabMeta.tabhead" class="flex items-stretch truncate">
|
||||||
<span v-if="tabMeta.icon" class="flex items-center justify-center mx-4 cursor-pointer">
|
<span v-if="tabMeta.icon" class="flex items-center justify-center mx-4 cursor-pointer">
|
||||||
<component :is="tabMeta.icon" class="w-4 h-4 svg-icons" />
|
<component :is="tabMeta.icon" class="w-4 h-4 svg-icons" />
|
||||||
</span>
|
</span>
|
||||||
<span class="truncate">
|
<span class="truncate pl-4">
|
||||||
{{ tabMeta.label }}
|
{{ tabMeta.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="truncate flex items-center justify-start mx-4">
|
||||||
|
<component :is="tabMeta.tabhead" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<HoppButtonSecondary v-tippy="{ theme: 'tooltip', delay: [500, 20] }" :icon="IconX" :style="{
|
<HoppButtonSecondary v-tippy="{ theme: 'tooltip', delay: [500, 20] }" :icon="IconX" :style="{
|
||||||
visibility: tabMeta.isRemovable ? 'visible' : 'hidden',
|
display: tabMeta.isRemovable ? 'flex' : 'none',
|
||||||
}" :title="closeText ?? t?.('action.close') ?? 'Close'"
|
}" :title="closeText ?? t?.('action.close') ?? 'Close'"
|
||||||
:class="[{ active: modelValue === tabID }, 'close']" class="mx-2 !p-0.5"
|
:class="[{ active: modelValue === tabID }, 'close']" class="mx-2 !p-0.5"
|
||||||
@click.stop="emit('removeTab', tabID)" />
|
@click.stop="emit('removeTab', tabID)" />
|
||||||
@@ -60,10 +66,13 @@ import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
|
|||||||
export type TabMeta = {
|
export type TabMeta = {
|
||||||
label: string | null
|
label: string | null
|
||||||
icon: Slot | undefined
|
icon: Slot | undefined
|
||||||
|
tabhead: Slot | undefined
|
||||||
info: string | null
|
info: string | null
|
||||||
isRemovable: boolean
|
isRemovable: boolean
|
||||||
}
|
}
|
||||||
export type TabProvider = {
|
export type TabProvider = {
|
||||||
|
// Whether inactive tabs should remain rendered
|
||||||
|
renderInactive: ComputedRef<boolean>
|
||||||
activeTabID: ComputedRef<string>
|
activeTabID: ComputedRef<string>
|
||||||
addTabEntry: (tabID: string, meta: TabMeta) => void
|
addTabEntry: (tabID: string, meta: TabMeta) => void
|
||||||
updateTabEntry: (tabID: string, newMeta: TabMeta) => void
|
updateTabEntry: (tabID: string, newMeta: TabMeta) => void
|
||||||
@@ -81,6 +90,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
renderInactiveTabs: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
canAddNewTab: {
|
canAddNewTab: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@@ -161,6 +174,7 @@ const sortTabs = (e: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
provide<TabProvider>("tabs-system", {
|
provide<TabProvider>("tabs-system", {
|
||||||
|
renderInactive: computed(() => props.renderInactiveTabs),
|
||||||
activeTabID: computed(() => props.modelValue),
|
activeTabID: computed(() => props.modelValue),
|
||||||
addTabEntry,
|
addTabEntry,
|
||||||
updateTabEntry,
|
updateTabEntry,
|
||||||
@@ -172,6 +186,14 @@ const selectTab = (id: string) => {
|
|||||||
const addTab = () => {
|
const addTab = () => {
|
||||||
emit("addTab")
|
emit("addTab")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollContainer = ref<HTMLElement|null>(null)
|
||||||
|
|
||||||
|
const scrollOnWindows = (event: WheelEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if(scrollContainer.value)
|
||||||
|
scrollContainer.value.scrollLeft += event.deltaY
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -8,11 +8,32 @@
|
|||||||
</HoppSmartWindow>
|
</HoppSmartWindow>
|
||||||
</HoppSmartWindows>
|
</HoppSmartWindows>
|
||||||
</Variant>
|
</Variant>
|
||||||
|
<Variant title="Custom Tab Heads">
|
||||||
|
<HoppSmartWindows
|
||||||
|
v-model="selectedWindow"
|
||||||
|
@add-tab="openNewTab"
|
||||||
|
@remove-tab="removeTab"
|
||||||
|
@sort="sortTabs"
|
||||||
|
>
|
||||||
|
<HoppSmartWindow
|
||||||
|
v-for="window in tabs"
|
||||||
|
:id="window.id"
|
||||||
|
:key="'tab_' + window.id"
|
||||||
|
:label="window.name"
|
||||||
|
:is-removable="window.removable"
|
||||||
|
>
|
||||||
|
<template #tabhead>
|
||||||
|
<icon-lucide-train class="svg-icons" /> <span class="truncate w-2"> - Lorem ipsum dolor sit amet</span>
|
||||||
|
</template>
|
||||||
|
</HoppSmartWindow>
|
||||||
|
</HoppSmartWindows>
|
||||||
|
</Variant>
|
||||||
</Story>
|
</Story>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { HoppSmartWindows, HoppSmartWindow } from "../components/smart"
|
import { HoppSmartWindows, HoppSmartWindow } from "../components/smart"
|
||||||
|
import IconLucideTrain from "~icons/lucide/train"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
|
|
||||||
const selectedWindow = ref("window1")
|
const selectedWindow = ref("window1")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { def as envDef } from "./environments"
|
|||||||
import { def as collectionsDef } from "./collections"
|
import { def as collectionsDef } from "./collections"
|
||||||
import { def as settingsDef } from "./settings"
|
import { def as settingsDef } from "./settings"
|
||||||
import { def as historyDef } from "./history"
|
import { def as historyDef } from "./history"
|
||||||
|
import { def as tabStateDef } from "./tab"
|
||||||
|
|
||||||
createHoppApp("#app", {
|
createHoppApp("#app", {
|
||||||
auth: authDef,
|
auth: authDef,
|
||||||
@@ -12,5 +13,6 @@ createHoppApp("#app", {
|
|||||||
collections: collectionsDef,
|
collections: collectionsDef,
|
||||||
settings: settingsDef,
|
settings: settingsDef,
|
||||||
history: historyDef,
|
history: historyDef,
|
||||||
|
tabState: tabStateDef,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
48
packages/hoppscotch-web/src/tab.ts
Normal file
48
packages/hoppscotch-web/src/tab.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { PersistableRESTTabState } from "@hoppscotch/common/helpers/rest/tab"
|
||||||
|
import { HoppUser } from "@hoppscotch/common/platform/auth"
|
||||||
|
import { TabStatePlatformDef } from "@hoppscotch/common/platform/tab"
|
||||||
|
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
|
||||||
|
import { def as platformAuth } from "./firebase/auth"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes tab state to a user's firestore sync
|
||||||
|
*
|
||||||
|
* @param persistableTabState The tab state to write to the request sync
|
||||||
|
*/
|
||||||
|
function writeCurrentTabState(
|
||||||
|
user: HoppUser,
|
||||||
|
persistableTabState: PersistableRESTTabState
|
||||||
|
) {
|
||||||
|
// Remove FormData entries because those can't be stored on Firestore ?
|
||||||
|
|
||||||
|
return setDoc(
|
||||||
|
doc(getFirestore(), "users", user.uid, "requests", "tab-state"),
|
||||||
|
persistableTabState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the synced tab state from the firestore sync
|
||||||
|
*
|
||||||
|
* @returns Fetched tab state object if exists else null
|
||||||
|
*/
|
||||||
|
async function loadTabStateFromSync(): Promise<PersistableRESTTabState | null> {
|
||||||
|
const currentUser = platformAuth.getCurrentUser()
|
||||||
|
|
||||||
|
if (!currentUser)
|
||||||
|
throw new Error("Cannot load request from sync without login")
|
||||||
|
|
||||||
|
const fbDoc = await getDoc(
|
||||||
|
doc(getFirestore(), "users", currentUser.uid, "requests", "tab-state")
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = fbDoc.data()
|
||||||
|
|
||||||
|
if (!data) return null
|
||||||
|
else return data as PersistableRESTTabState
|
||||||
|
}
|
||||||
|
|
||||||
|
export const def: TabStatePlatformDef = {
|
||||||
|
loadTabStateFromSync,
|
||||||
|
writeCurrentTabState,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user