feat: tab service added (#3367)

This commit is contained in:
Anwarul Islam
2023-10-11 18:51:07 +06:00
committed by GitHub
parent 51510566bc
commit ba31cdabea
60 changed files with 1112 additions and 841 deletions

View File

@@ -37,7 +37,8 @@
import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { currentActiveTab } from "~/helpers/rest/tab"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
const toast = useToast()
const t = useI18n()
@@ -60,11 +61,12 @@ const emit = defineEmits<{
const editingName = ref("")
const tabs = useService(RESTTabService)
watch(
() => props.show,
(show) => {
if (show) {
editingName.value = currentActiveTab.value.document.request.name
editingName.value = tabs.currentActiveTab.value.document.request.name
}
}
)

View File

@@ -327,7 +327,8 @@ import { useColorMode } from "@composables/theming"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js"
import { currentActiveTab } from "~/helpers/rest/tab"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
export type Collection = {
type: "collections"
@@ -535,7 +536,8 @@ const isSelected = ({
}
}
const active = computed(() => currentActiveTab.value.document.saveContext)
const tabs = useService(RESTTabService)
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
const isActiveRequest = (folderPath: string, requestIndex: number) => {
return pipe(

View File

@@ -82,12 +82,16 @@ import {
import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core"
import { platform } from "~/platform"
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n()
const toast = useToast()
const RESTTabs = useService(RESTTabService)
const GQLTabs = useService(GQLTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
@@ -123,13 +127,13 @@ const emit = defineEmits<{
}>()
const gqlRequestName = computedWithControl(
() => activeGQLTab.value,
() => activeGQLTab.value.document.request.name
() => GQLTabs.currentActiveTab.value,
() => GQLTabs.currentActiveTab.value.document.request.name
)
const restRequestName = computedWithControl(
() => activeRESTTab.value,
() => activeRESTTab.value.document.request.name
() => RESTTabs.currentActiveTab.value,
() => RESTTabs.currentActiveTab.value.document.request.name
)
const reqName = computed(() => {
@@ -145,12 +149,14 @@ const reqName = computed(() => {
const requestName = ref(reqName.value)
watch(
() => [activeRESTTab.value, activeGQLTab.value],
() => [RESTTabs.currentActiveTab.value, GQLTabs.currentActiveTab.value],
() => {
if (props.mode === "rest") {
requestName.value = activeRESTTab.value?.document.request.name ?? ""
requestName.value =
RESTTabs.currentActiveTab.value?.document.request.name ?? ""
} else {
requestName.value = activeGQLTab.value?.document.request.name ?? ""
requestName.value =
GQLTabs.currentActiveTab.value?.document.request.name ?? ""
}
}
)
@@ -210,8 +216,8 @@ const saveRequestAs = async () => {
const requestUpdated =
props.mode === "rest"
? cloneDeep(activeRESTTab.value.document.request)
: cloneDeep(activeGQLTab.value.document.request)
? cloneDeep(RESTTabs.currentActiveTab.value.document.request)
: cloneDeep(GQLTabs.currentActiveTab.value.document.request)
requestUpdated.name = requestName.value
@@ -224,7 +230,7 @@ const saveRequestAs = async () => {
requestUpdated
)
activeRESTTab.value.document = {
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
saveContext: {
@@ -251,7 +257,7 @@ const saveRequestAs = async () => {
requestUpdated
)
activeRESTTab.value.document = {
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
saveContext: {
@@ -279,7 +285,7 @@ const saveRequestAs = async () => {
requestUpdated
)
activeRESTTab.value.document = {
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
saveContext: {
@@ -439,7 +445,7 @@ const updateTeamCollectionOrFolder = (
(result) => {
const { createRequestInCollection } = result
activeRESTTab.value.document = {
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
saveContext: {
@@ -460,7 +466,7 @@ const updateTeamCollectionOrFolder = (
const requestSaved = () => {
toast.success(`${t("request.added")}`)
nextTick(() => {
activeRESTTab.value.document.isDirty = false
RESTTabs.currentActiveTab.value.document.isDirty = false
})
hideModal()
}

View File

@@ -357,10 +357,12 @@ import { HoppRESTRequest } from "@hoppscotch/data"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js"
import { currentActiveTab } from "~/helpers/rest/tab"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n()
const colorMode = useColorMode()
const tabs = useService(RESTTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
@@ -558,7 +560,7 @@ const isSelected = ({
}
}
const active = computed(() => currentActiveTab.value.document.saveContext)
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
const isActiveRequest = (requestID: string) => {
return pipe(

View File

@@ -36,11 +36,14 @@
import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { currentActiveTab } from "~/helpers/graphql/tab"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
const toast = useToast()
const t = useI18n()
const tabs = useService(GQLTabService)
const props = defineProps<{
show: boolean
folderPath?: string
@@ -63,7 +66,7 @@ watch(
() => props.show,
(show) => {
if (show) {
editingName.value = currentActiveTab.value?.document.request.name
editingName.value = tabs.currentActiveTab.value?.document.request.name
}
}
)

View File

@@ -220,7 +220,8 @@ import {
moveGraphqlRequest,
} from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked"
import { getTabsRefTo } from "~/helpers/graphql/tab"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
const props = defineProps({
picked: { type: Object, default: null },
@@ -235,6 +236,8 @@ const colorMode = useColorMode()
const toast = useToast()
const t = useI18n()
const tabs = useService(GQLTabService)
// TODO: improve types plz
const emit = defineEmits<{
(e: "select", i: Picked | null): void
@@ -295,7 +298,7 @@ const removeCollection = () => {
emit("select", null)
}
const possibleTabs = getTabsRefTo((tab) => {
const possibleTabs = tabs.getTabsRefTo((tab) => {
const ctx = tab.document.saveContext
if (!ctx) return false

View File

@@ -203,12 +203,15 @@ import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
import { computed, ref } from "vue"
import { getTabsRefTo } from "~/helpers/graphql/tab"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
const toast = useToast()
const t = useI18n()
const colorMode = useColorMode()
const tabs = useService(GQLTabService)
const props = defineProps({
picked: { type: Object, default: null },
// Whether the request is in a selectable mode (activates 'select' event)
@@ -277,7 +280,7 @@ const removeFolder = () => {
emit("select", { picked: null })
}
const possibleTabs = getTabsRefTo((tab) => {
const possibleTabs = tabs.getTabsRefTo((tab) => {
const ctx = tab.document.saveContext
if (!ctx) return false

View File

@@ -137,12 +137,8 @@ import { useToast } from "@composables/toast"
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import { removeGraphqlRequest } from "~/newstore/collections"
import {
createNewTab,
getTabRefWithSaveContext,
currentTabID,
currentActiveTab,
} from "~/helpers/graphql/tab"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
// Template refs
const tippyActions = ref<any | null>(null)
@@ -154,6 +150,8 @@ const deleteAction = ref<any | null>(null)
const t = useI18n()
const toast = useToast()
const tabs = useService(GQLTabService)
const props = defineProps({
// Whether the object is selected (show the tick mark)
picked: { type: Object, default: null },
@@ -165,7 +163,7 @@ const props = defineProps({
})
const isActive = computed(() => {
const saveCtx = currentActiveTab.value?.document.saveContext
const saveCtx = tabs.currentActiveTab.value?.document.saveContext
if (!saveCtx) return false
@@ -201,7 +199,7 @@ const selectRequest = () => {
if (props.saveRequest) {
pick()
} else {
const possibleTab = getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
@@ -209,11 +207,11 @@ const selectRequest = () => {
// Switch to that request if that request is open
if (possibleTab) {
currentTabID.value = possibleTab.value.id
tabs.setActiveTab(possibleTab.value.id)
return
}
createNewTab({
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: props.folderPath,
@@ -253,7 +251,7 @@ const removeRequest = () => {
}
// Detach the request from any of the tabs
const possibleTab = getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,

View File

@@ -145,7 +145,8 @@ import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming"
import { platform } from "~/platform"
import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
export default defineComponent({
props: {
@@ -158,11 +159,13 @@ export default defineComponent({
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
const colorMode = useColorMode()
const t = useI18n()
const tabs = useService(GQLTabService)
return {
collections,
colorMode,
t,
tabs,
IconPlus,
IconHelpCircle,
IconArchive,
@@ -267,13 +270,13 @@ export default defineComponent({
},
onAddRequest({ name, path, index }) {
const newRequest = {
...currentActiveTab.value.document.request,
...this.tabs.currentActiveTab.value.document.request,
name,
}
saveGraphqlRequestAs(path, newRequest)
createNewTab({
this.tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: path,

View File

@@ -219,12 +219,6 @@ import {
import * as E from "fp-ts/Either"
import { platform } from "~/platform"
import { createCollectionGists } from "~/helpers/gist"
import {
createNewTab,
currentActiveTab,
currentTabID,
getTabRefWithSaveContext,
} from "~/helpers/rest/tab"
import {
getRequestsByPath,
resolveSaveContextOnRequestReorder,
@@ -239,9 +233,11 @@ import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
const t = useI18n()
const toast = useToast()
const tabs = useService(RESTTabService)
const props = defineProps({
saveRequest: {
@@ -654,7 +650,7 @@ const addRequest = (payload: {
const onAddRequest = (requestName: string) => {
const newRequest = {
...cloneDeep(currentActiveTab.value.document.request),
...cloneDeep(tabs.currentActiveTab.value.document.request),
name: requestName,
}
@@ -663,7 +659,7 @@ const onAddRequest = (requestName: string) => {
if (!path) return
const insertionIndex = saveRESTRequestAs(path, newRequest)
createNewTab({
tabs.createNewTab({
request: newRequest,
isDirty: false,
saveContext: {
@@ -712,7 +708,7 @@ const onAddRequest = (requestName: string) => {
(result) => {
const { createRequestInCollection } = result
createNewTab({
tabs.createNewTab({
request: newRequest,
isDirty: false,
saveContext: {
@@ -935,7 +931,7 @@ const updateEditingRequest = (newName: string) => {
if (folderPath === null || requestIndex === null) return
const possibleActiveTab = getTabRefWithSaveContext({
const possibleActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex,
folderPath,
@@ -979,7 +975,7 @@ const updateEditingRequest = (newName: string) => {
)
)()
const possibleTab = getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID,
})
@@ -1215,7 +1211,7 @@ const onRemoveRequest = () => {
emit("select", null)
}
const possibleTab = getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
requestIndex,
@@ -1275,7 +1271,7 @@ const onRemoveRequest = () => {
)()
// If there is a tab attached to this request, dissociate its state and mark it dirty
const possibleTab = getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID,
})
@@ -1308,14 +1304,14 @@ const selectRequest = (selectedRequest: {
let possibleTab = null
if (collectionsType.value.type === "team-collections") {
possibleTab = getTabRefWithSaveContext({
possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID: requestIndex,
})
if (possibleTab) {
currentTabID.value = possibleTab.value.id
tabs.setActiveTab(possibleTab.value.id)
} else {
createNewTab({
tabs.createNewTab({
request: cloneDeep(request),
isDirty: false,
saveContext: {
@@ -1325,16 +1321,16 @@ const selectRequest = (selectedRequest: {
})
}
} else {
possibleTab = getTabRefWithSaveContext({
possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex: parseInt(requestIndex),
folderPath: folderPath!,
})
if (possibleTab) {
currentTabID.value = possibleTab.value.id
tabs.setActiveTab(possibleTab.value.id)
} else {
// If not, open the request in a new tab
createNewTab({
tabs.createNewTab({
request: cloneDeep(request),
isDirty: false,
saveContext: {
@@ -1377,7 +1373,7 @@ const dropRequest = (payload: {
destinationCollectionIndex
)
const possibleTab = getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
requestIndex: pathToLastIndex(requestIndex),
@@ -1426,7 +1422,7 @@ const dropRequest = (payload: {
1
)
const possibleTab = getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID: requestIndex,
})

View File

@@ -83,11 +83,14 @@ import {
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { currentActiveTab } from "~/helpers/rest/tab"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n()
const toast = useToast()
const tabs = useService(RESTTabService)
const props = defineProps<{
show: boolean
position: { top: number; left: number }
@@ -189,8 +192,8 @@ const addEnvironment = async () => {
//replace the current tab endpoint with the variable name with << and >>
const variableName = `<<${editingName.value}>>`
//replace the currenttab endpoint containing the value in the text with variablename
currentActiveTab.value.document.request.endpoint =
currentActiveTab.value.document.request.endpoint.replace(
tabs.currentActiveTab.value.document.request.endpoint =
tabs.currentActiveTab.value.document.request.endpoint.replace(
editingValue.value,
variableName
)

View File

@@ -64,7 +64,6 @@
<script setup lang="ts">
import { platform } from "~/platform"
import { useI18n } from "@composables/i18n"
import { currentActiveTab } from "~/helpers/graphql/tab"
import { computed, ref, watch } from "vue"
import { connection } from "~/helpers/graphql/connection"
import { connect } from "~/helpers/graphql/connection"
@@ -72,8 +71,10 @@ import { disconnect } from "~/helpers/graphql/connection"
import { InterceptorService } from "~/services/interceptor.service"
import { useService } from "dioc/vue"
import { defineActionHandler } from "~/helpers/actions"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n()
const tabs = useService(GQLTabService)
const interceptorService = useService(InterceptorService)
@@ -82,9 +83,9 @@ const connectionSwitchModal = ref(false)
const connected = computed(() => connection.state === "CONNECTED")
const url = computed({
get: () => currentActiveTab.value?.document.request.url ?? "",
get: () => tabs.currentActiveTab.value?.document.request.url ?? "",
set: (value) => {
currentActiveTab.value!.document.request.url = value
tabs.currentActiveTab.value!.document.request.url = value
},
})
@@ -97,7 +98,7 @@ const onConnectClick = () => {
}
const gqlConnect = () => {
connect(url.value, currentActiveTab.value?.document.request.headers)
connect(url.value, tabs.currentActiveTab.value?.document.request.headers)
platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
@@ -114,7 +115,7 @@ const switchConnection = () => {
const lastTwoUrls = ref<string[]>([])
watch(
currentActiveTab,
tabs.currentActiveTab,
(newVal) => {
if (newVal) {
lastTwoUrls.value.push(newVal.document.request.url)

View File

@@ -58,8 +58,7 @@ import { computed, ref, watch } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { HoppGQLRequest } from "@hoppscotch/data"
import { platform } from "~/platform"
import { currentActiveTab } from "~/helpers/graphql/tab"
import { computedWithControl } from "@vueuse/core"
import { computedWithControl, useVModel } from "@vueuse/core"
import {
GQLResponseEvent,
runGQLOperation,
@@ -68,26 +67,39 @@ import {
import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql"
const VALID_GQL_OPERATIONS = [
"query",
"headers",
"variables",
"authorization",
] as const
export type GQLOptionTabs = (typeof VALID_GQL_OPERATIONS)[number]
export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization"
const selectedOptionTab = ref<GQLOptionTabs>("query")
const interceptorService = useService(InterceptorService)
const t = useI18n()
const toast = useToast()
const tabs = useService(GQLTabService)
// v-model integration with props and emit
const props = withDefaults(
defineProps<{
modelValue: HoppGQLRequest
response?: GQLResponseEvent[] | null
optionTab?: GQLOptionTabs
tabId: string
}>(),
{
response: null,
optionTab: "query",
}
)
const emit = defineEmits(["update:modelValue", "update:response"])
const selectedOptionTab = useVModel(props, "optionTab", emit)
const request = ref(props.modelValue)
@@ -100,8 +112,8 @@ watch(
)
const url = computedWithControl(
() => currentActiveTab.value,
() => currentActiveTab.value.document.request.url
() => tabs.currentActiveTab.value,
() => tabs.currentActiveTab.value.document.request.url
)
const activeGQLHeadersCount = computed(
@@ -185,17 +197,17 @@ const hideRequestModal = () => {
}
const saveRequest = () => {
if (
currentActiveTab.value.document.saveContext &&
currentActiveTab.value.document.saveContext.originLocation ===
tabs.currentActiveTab.value.document.saveContext &&
tabs.currentActiveTab.value.document.saveContext.originLocation ===
"user-collection"
) {
editGraphqlRequest(
currentActiveTab.value.document.saveContext.folderPath,
currentActiveTab.value.document.saveContext.requestIndex,
currentActiveTab.value.document.request
tabs.currentActiveTab.value.document.saveContext.folderPath,
tabs.currentActiveTab.value.document.saveContext.requestIndex,
tabs.currentActiveTab.value.document.request
)
currentActiveTab.value.document.isDirty = false
tabs.currentActiveTab.value.document.isDirty = false
} else {
showSaveRequestModal.value = true
}

View File

@@ -3,12 +3,13 @@
<template #primary>
<GraphqlRequestOptions
v-model="tab.document.request"
v-model:response="tab.response"
v-model:response="tab.document.response"
v-model:option-tab="tab.document.optionTabPreference"
:tab-id="tab.id"
/>
</template>
<template #secondary>
<GraphqlResponse :response="tab.response" />
<GraphqlResponse :response="tab.document.response" />
</template>
</AppPaneLayout>
</template>
@@ -18,14 +19,15 @@ import { useVModel } from "@vueuse/core"
import { cloneDeep } from "lodash-es"
import { watch } from "vue"
import { isEqualHoppGQLRequest } from "~/helpers/graphql"
import { HoppGQLTab } from "~/helpers/graphql/tab"
import { HoppGQLDocument } from "~/helpers/graphql/document"
import { HoppTab } from "~/services/tab"
// TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppGQLTab }>()
const props = defineProps<{ modelValue: HoppTab<HoppGQLDocument> }>()
const emit = defineEmits<{
(e: "update:modelValue", val: HoppGQLTab): void
(e: "update:modelValue", val: HoppTab<HoppGQLDocument>): void
}>()
const tab = useVModel(props, "modelValue", emit)

View File

@@ -92,12 +92,13 @@ import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy"
import { HoppGQLTab } from "~/helpers/graphql/tab"
import { HoppTab } from "~/services/tab"
import { HoppGQLDocument } from "~/helpers/graphql/document"
const t = useI18n()
defineProps<{
tab: HoppGQLTab
tab: HoppTab<HoppGQLDocument>
isRemovable: boolean
}>()

View File

@@ -67,9 +67,11 @@ import IconMaximize2 from "~icons/lucide/maximize-2"
import { useI18n } from "@composables/i18n"
import { makeGQLRequest } from "@hoppscotch/data"
import { createNewTab } from "~/helpers/graphql/tab"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n()
const tabs = useService(GQLTabService)
const props = defineProps<{
entry: GQLHistoryEntry
@@ -93,7 +95,7 @@ const query = computed(() =>
)
const useEntry = () => {
createNewTab({
tabs.createNewTab({
request: makeGQLRequest({
name: props.entry.request.name,
url: props.entry.request.url,

View File

@@ -176,8 +176,9 @@ import {
import HistoryRestCard from "./rest/Card.vue"
import HistoryGraphqlCard from "./graphql/Card.vue"
import { createNewTab } from "~/helpers/rest/tab"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
@@ -293,8 +294,9 @@ const clearHistory = () => {
// NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry
// (That is not a really good behaviour tho ¯\_(ツ)_/¯)
const tabs = useService(RESTTabService)
const useHistory = (entry: RESTHistoryEntry) => {
createNewTab({
tabs.createNewTab({
request: entry.request,
isDirty: false,
})

View File

@@ -59,7 +59,9 @@
:key="`contentTypeItem-${contentTypeIndex}`"
:label="contentTypeItem"
:info-icon="
contentTypeItem === body.contentType ? IconDone : null
contentTypeItem === body.contentType
? IconDone
: undefined
"
:active-info-icon="contentTypeItem === body.contentType"
@click="
@@ -136,7 +138,7 @@ import IconDone from "~icons/lucide/check"
import IconExternalLink from "~icons/lucide/external-link"
import IconInfo from "~icons/lucide/info"
import IconRefreshCW from "~icons/lucide/refresh-cw"
import { RequestOptionTabs } from "./RequestOptions.vue"
import { RESTOptionTabs } from "./RequestOptions.vue"
const colorMode = useColorMode()
const t = useI18n()
@@ -147,7 +149,7 @@ const props = defineProps<{
}>()
const emit = defineEmits<{
(e: "change-tab", value: RequestOptionTabs): void
(e: "change-tab", value: RESTOptionTabs): void
(e: "update:headers", value: HoppRESTHeader[]): void
(e: "update:body", value: HoppRESTReqBody): void
}>()
@@ -164,7 +166,7 @@ const overridenContentType = computed(() =>
)
)
const contentTypeOverride = (tab: RequestOptionTabs) => {
const contentTypeOverride = (tab: RESTOptionTabs) => {
emit("change-tab", tab)
if (!isContentTypeAlreadyExist()) {
// TODO: Fix this

View File

@@ -157,9 +157,10 @@ import {
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconWrapText from "~icons/lucide/wrap-text"
import { currentActiveTab } from "~/helpers/rest/tab"
import cloneDeep from "lodash-es/cloneDeep"
import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n()
@@ -173,7 +174,8 @@ const emit = defineEmits<{
const toast = useToast()
const request = ref(cloneDeep(currentActiveTab.value.document.request))
const tabs = useService(RESTTabService)
const request = ref(cloneDeep(tabs.currentActiveTab.value.document.request))
const codegenType = ref<CodegenName>("shell-curl")
const errorState = ref(false)
@@ -242,7 +244,7 @@ watch(
() => props.show,
(goingToShow) => {
if (goingToShow) {
request.value = cloneDeep(currentActiveTab.value.document.request)
request.value = cloneDeep(tabs.currentActiveTab.value.document.request)
platform.analytics?.logEvent({
type: "HOPP_REST_CODEGEN_OPENED",

View File

@@ -273,10 +273,13 @@ import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab"
import { RESTTabService } from "~/services/tab/rest"
const t = useI18n()
const toast = useToast()
const tabs = useService(RESTTabService)
const colorMode = useColorMode()
const idTicker = ref(0)
@@ -515,13 +518,13 @@ const changeTab = (tab: ComputedHeader["source"]) => {
const inspectionService = useService(InspectionService)
const headerKeyResults = inspectionService.getResultViewFor(
currentTabID.value,
tabs.currentTabID.value,
(result) =>
result.locations.type === "header" && result.locations.position === "key"
)
const headerValueResults = inspectionService.getResultViewFor(
currentTabID.value,
tabs.currentTabID.value,
(result) =>
result.locations.type === "header" && result.locations.position === "value"
)

View File

@@ -93,13 +93,16 @@ import IconWrapText from "~icons/lucide/wrap-text"
import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check"
import IconTrash2 from "~icons/lucide/trash-2"
import { currentActiveTab } from "~/helpers/rest/tab"
import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n()
const toast = useToast()
const tabs = useService(RESTTabService)
const curl = ref("")
const curlEditor = ref<any | null>(null)
@@ -149,7 +152,7 @@ const handleImport = () => {
type: "HOPP_REST_IMPORT_CURL",
})
currentActiveTab.value.document.request = req
tabs.currentActiveTab.value.document.request = req
} catch (e) {
console.error(e)
toast.error(`${t("error.curl_invalid_format")}`)

View File

@@ -202,12 +202,13 @@ import { objRemoveKey } from "@functional/object"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab"
import { RESTTabService } from "~/services/tab/rest"
const colorMode = useColorMode()
const t = useI18n()
const toast = useToast()
const tabs = useService(RESTTabService)
const idTicker = ref(0)
@@ -410,13 +411,13 @@ const clearContent = () => {
const inspectionService = useService(InspectionService)
const parameterKeyResults = inspectionService.getResultViewFor(
currentTabID.value,
tabs.currentTabID.value,
(result) =>
result.locations.type === "parameter" && result.locations.position === "key"
)
const parameterValueResults = inspectionService.getResultViewFor(
currentTabID.value,
tabs.currentTabID.value,
(result) =>
result.locations.type === "parameter" &&
result.locations.position === "value"

View File

@@ -217,6 +217,7 @@
@hide-modal="showCurlImportModal = false"
/>
<HttpCodegenModal
v-if="showCodegenModal"
:show="showCodegenModal"
@hide-modal="showCodegenModal = false"
/>
@@ -257,7 +258,6 @@ 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, currentTabID } from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
import { platform } from "~/platform"
@@ -265,6 +265,9 @@ import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { RESTTabService } from "~/services/tab/rest"
const t = useI18n()
const interceptorService = useService(InterceptorService)
@@ -286,7 +289,7 @@ const toast = useToast()
const { subscribeToStream } = useStreamSubscriber()
const props = defineProps<{ modelValue: HoppRESTTab }>()
const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>()
const emit = defineEmits(["update:modelValue"])
const tab = useVModel(props, "modelValue", emit)
@@ -434,7 +437,7 @@ const clearContent = () => {
}
const updateRESTResponse = (response: HoppRESTResponse | null) => {
tab.value.response = response
tab.value.document.response = response
}
const copyLinkIcon = refAutoReset<
@@ -642,5 +645,6 @@ const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService)
const tabResults = inspectionService.getResultViewFor(currentTabID.value)
const tabs = useService(RESTTabService)
const tabResults = inspectionService.getResultViewFor(tabs.currentTabID.value)
</script>

View File

@@ -1,6 +1,6 @@
<template>
<HoppSmartTabs
v-model="selectedOptionsTab"
v-model="selectedOptionTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
render-inactive-tabs
>
@@ -15,7 +15,7 @@
<HttpBody
v-model:headers="request.headers"
v-model:body="request.body"
@change-tab="changeTab"
@change-tab="changeOptionTab"
/>
</HoppSmartTab>
<HoppSmartTab
@@ -23,7 +23,7 @@
:label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`"
>
<HttpHeaders v-model="request" @change-tab="changeTab" />
<HttpHeaders v-model="request" @change-tab="changeOptionTab" />
</HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<HttpAuthorization v-model="request.auth" />
@@ -55,31 +55,43 @@
import { useI18n } from "@composables/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { computed, ref } from "vue"
import { computed } from "vue"
import { defineActionHandler } from "~/helpers/actions"
export type RequestOptionTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "preRequestScript"
| "tests"
const VALID_OPTION_TABS = [
"params",
"bodyParams",
"headers",
"authorization",
"preRequestScript",
"tests",
] as const
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
const t = useI18n()
// v-model integration with props and emit
const props = defineProps<{ modelValue: HoppRESTRequest }>()
const props = withDefaults(
defineProps<{
modelValue: HoppRESTRequest
optionTab: RESTOptionTabs
}>(),
{
optionTab: "params",
}
)
const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTRequest): void
(e: "update:optionTab", value: RESTOptionTabs): void
}>()
const request = useVModel(props, "modelValue", emit)
const selectedOptionTab = useVModel(props, "optionTab", emit)
const selectedOptionsTab = ref<RequestOptionTabs>("params")
const changeTab = (e: RequestOptionTabs) => {
selectedOptionsTab.value = e
const changeOptionTab = (e: RESTOptionTabs) => {
selectedOptionTab.value = e
}
const newActiveParamsCount$ = computed(() => {
@@ -101,6 +113,6 @@ const newActiveHeadersCount$ = computed(() => {
})
defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionsTab.value = tab as RequestOptionTabs
selectedOptionTab.value = tab as RESTOptionTabs
})
</script>

View File

@@ -2,10 +2,13 @@
<AppPaneLayout layout-id="rest-primary">
<template #primary>
<HttpRequest v-model="tab" />
<HttpRequestOptions v-model="tab.document.request" />
<HttpRequestOptions
v-model="tab.document.request"
v-model:option-tab="tab.document.optionTabPreference"
/>
</template>
<template #secondary>
<HttpResponse v-model:tab="tab" />
<HttpResponse v-model:document="tab.document" />
</template>
</AppPaneLayout>
</template>
@@ -13,16 +16,17 @@
<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"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
// TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppRESTTab }>()
const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>()
const emit = defineEmits<{
(e: "update:modelValue", val: HoppRESTTab): void
(e: "update:modelValue", val: HoppTab<HoppRESTDocument>): void
}>()
const tab = useVModel(props, "modelValue", emit)

View File

@@ -1,36 +1,33 @@
<template>
<div class="flex flex-col flex-1 relative">
<HttpResponseMeta :response="tab.response" />
<HttpResponseMeta :response="doc.response" />
<LensesResponseBodyRenderer
v-if="!loading && hasResponse"
v-model:selected-tab-preference="selectedTabPreference"
v-model:tab="tab"
v-model:document="doc"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { useVModel } from "@vueuse/core"
import { computed } from "vue"
import { HoppRESTDocument } from "~/helpers/rest/document"
const props = defineProps<{
tab: HoppRESTTab
document: HoppRESTDocument
}>()
const emit = defineEmits<{
(e: "update:tab", val: HoppRESTTab): void
(e: "update:tab", val: HoppRESTDocument): void
}>()
const tab = useVModel(props, "tab", emit)
const selectedTabPreference = ref<string | null>(null)
const doc = useVModel(props, "document", emit)
const hasResponse = computed(
() =>
tab.value.response?.type === "success" ||
tab.value.response?.type === "fail"
doc.value.response?.type === "success" ||
doc.value.response?.type === "fail"
)
const loading = computed(() => tab.value.response?.type === "loading")
const loading = computed(() => doc.value.response?.type === "loading")
</script>

View File

@@ -93,10 +93,11 @@ import { useColorMode } from "@composables/theming"
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab"
import { RESTTabService } from "~/services/tab/rest"
const t = useI18n()
const colorMode = useColorMode()
const tabs = useService(RESTTabService)
const props = defineProps<{
response: HoppRESTResponse | null | undefined
@@ -146,7 +147,7 @@ const statusCategory = computed(() => {
const inspectionService = useService(InspectionService)
const tabResults = inspectionService.getResultViewFor(
currentTabID.value,
tabs.currentTabID.value,
(result) => result.locations.type === "response"
)
</script>

View File

@@ -96,16 +96,17 @@ import { ref } from "vue"
import { TippyComponent } from "vue-tippy"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { useI18n } from "~/composables/i18n"
import { HoppRESTTab } from "~/helpers/rest/tab"
import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
const t = useI18n()
defineProps<{
tab: HoppRESTTab
tab: HoppTab<HoppRESTDocument>
isRemovable: boolean
}>()

View File

@@ -1,6 +1,6 @@
<template>
<HoppSmartTabs
v-if="tab.response"
v-if="doc.response"
v-model="selectedLensTab"
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary top-lowerPrimaryStickyFold"
>
@@ -13,7 +13,7 @@
>
<component
:is="lensRendererFor(lens.renderer)"
:response="tab.response"
:response="doc.response"
/>
</HoppSmartTab>
<HoppSmartTab
@@ -28,19 +28,10 @@
<HoppSmartTab
id="results"
:label="t('test.results')"
:indicator="
tab.testResults &&
(tab.testResults.expectResults.length ||
tab.testResults.tests.length ||
tab.testResults.envDiff.selected.additions.length ||
tab.testResults.envDiff.selected.updations.length ||
tab.testResults.envDiff.global.updations.length)
? true
: false
"
:indicator="showIndicator"
class="flex flex-col flex-1"
>
<HttpTestResult v-model="tab.testResults" />
<HttpTestResult v-model="doc.testResults" />
</HoppSmartTab>
</HoppSmartTabs>
</template>
@@ -54,20 +45,30 @@ import {
} from "~/helpers/lenses/lenses"
import { useI18n } from "@composables/i18n"
import { useVModel } from "@vueuse/core"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
const props = defineProps<{
tab: HoppRESTTab
selectedTabPreference: string | null
document: HoppRESTDocument
}>()
const emit = defineEmits<{
(e: "update:tab", val: HoppRESTTab): void
(e: "update:selectedTabPreference", newTab: string): void
(e: "update:document", document: HoppRESTDocument): void
}>()
const tab = useVModel(props, "tab", emit)
const selectedTabPreference = useVModel(props, "selectedTabPreference", emit)
const doc = useVModel(props, "document", emit)
const showIndicator = computed(() => {
if (!doc.value.testResults) return false
const { expectResults, tests, envDiff } = doc.value.testResults
return Boolean(
expectResults.length ||
tests.length ||
envDiff.selected.additions.length ||
envDiff.selected.updations.length ||
envDiff.global.updations.length
)
})
const allLensRenderers = getLensRenderers()
@@ -81,19 +82,19 @@ const selectedLensTab = ref("")
const maybeHeaders = computed(() => {
if (
!tab.value.response ||
!doc.value.response ||
!(
tab.value.response.type === "success" ||
tab.value.response.type === "fail"
doc.value.response.type === "success" ||
doc.value.response.type === "fail"
)
)
return null
return tab.value.response.headers
return doc.value.response.headers
})
const validLenses = computed(() => {
if (!tab.value.response) return []
return getSuitableLenses(tab.value.response)
if (!doc.value.response) return []
return getSuitableLenses(doc.value.response)
})
watch(
@@ -107,11 +108,13 @@ watch(
"results",
]
const { responseTabPreference } = doc.value
if (
selectedTabPreference.value &&
validRenderers.includes(selectedTabPreference.value)
responseTabPreference &&
validRenderers.includes(responseTabPreference)
) {
selectedLensTab.value = selectedTabPreference.value
selectedLensTab.value = responseTabPreference
} else {
selectedLensTab.value = newLenses[0].renderer
}
@@ -120,6 +123,6 @@ watch(
)
watch(selectedLensTab, (newLensID) => {
selectedTabPreference.value = newLensID
doc.value.responseTabPreference = newLensID
})
</script>

View File

@@ -29,8 +29,9 @@ import {
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
import { HoppRESTTab } from "./rest/tab"
import { Ref } from "vue"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "./rest/document"
const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" }
@@ -69,7 +70,7 @@ export const executedResponses$ = new Subject<
>()
export function runRESTRequest$(
tab: Ref<HoppRESTTab>
tab: Ref<HoppTab<HoppRESTDocument>>
): [
() => void,
Promise<
@@ -127,7 +128,7 @@ export function runRESTRequest$(
)()
if (E.isRight(runResult)) {
tab.value.testResults = translateToSandboxTestResults(
tab.value.document.testResults = translateToSandboxTestResults(
runResult.right
)
@@ -163,7 +164,7 @@ export function runRESTRequest$(
)()
}
} else {
tab.value.testResults = {
tab.value.document.testResults = {
description: "",
expectResults: [],
tests: [],

View File

@@ -1,9 +1,10 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { getTabsRefTo } from "../rest/tab"
import { getAffectedIndexes } from "./affectedIndex"
import { GetSingleRequestDocument } from "../backend/graphql"
import { runGQLQuery } from "../backend/GQLClient"
import * as E from "fp-ts/Either"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
/**
* Resolve save context on reorder
@@ -56,7 +57,9 @@ export function resolveSaveContextOnCollectionReorder(
}
}
const tabs = getTabsRefTo((tab) => {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
affectedPaths.has(tab.document.saveContext.folderPath)
@@ -84,7 +87,8 @@ export function updateSaveContextForAffectedRequests(
oldFolderPath: string,
newFolderPath: string
) {
const tabs = getTabsRefTo((tab) => {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(oldFolderPath)
@@ -105,7 +109,8 @@ export function updateSaveContextForAffectedRequests(
}
function resetSaveContextForAffectedRequests(folderPath: string) {
const tabs = getTabsRefTo((tab) => {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(folderPath)
@@ -124,7 +129,8 @@ function resetSaveContextForAffectedRequests(folderPath: string) {
*/
export async function resetTeamRequestsContext() {
const tabs = getTabsRefTo((tab) => {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return tab.document.saveContext?.originLocation === "team-collection"
})

View File

@@ -1,6 +1,7 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { getTabsRefTo } from "../rest/tab"
import { getAffectedIndexes } from "./affectedIndex"
import { RESTTabService } from "~/services/tab/rest"
import { getService } from "~/modules/dioc"
/**
* Resolve save context on reorder
@@ -32,7 +33,8 @@ export function resolveSaveContextOnRequestReorder(payload: {
// if (newIndex === -1) remove it from the map because it will be deleted
if (newIndex === -1) affectedIndexes.delete(lastIndex)
const tabs = getTabsRefTo((tab) => {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath === folderPath &&

View File

@@ -1,4 +1,6 @@
import { HoppGQLRequest } from "@hoppscotch/data"
import { GQLResponseEvent } from "./connection"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
export type HoppGQLSaveContext =
| {
@@ -55,4 +57,20 @@ export type HoppGQLDocument = {
* This contains where the request is originated from basically.
*/
saveContext?: HoppGQLSaveContext
/**
* The response as it is in the document
* (if any)
*/
response?: GQLResponseEvent[] | null
/**
* Response tab preference for the current tab's document
*/
responseTabPreference?: string
/**
* Options tab preference for the current tab's document
*/
optionTabPreference?: GQLOptionTabs
}

View File

@@ -1,226 +0,0 @@
import { refWithControl } from "@vueuse/core"
import { isEqual } from "lodash-es"
import { v4 as uuidV4 } from "uuid"
import { computed, reactive, ref, shallowReadonly, watch } from "vue"
import { HoppTestResult } from "../types/HoppTestResult"
import { GQLResponseEvent } from "./connection"
import { getDefaultGQLRequest } from "./default"
import { HoppGQLDocument, HoppGQLSaveContext } from "./document"
export type HoppGQLTab = {
id: string
document: HoppGQLDocument
response?: GQLResponseEvent[] | null
testResults?: HoppTestResult | null
}
export type PersistableGQLTabState = {
lastActiveTabID: string
orderedDocs: Array<{
tabID: string
doc: HoppGQLDocument
}>
}
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, HoppGQLTab>([
[
"test",
{
id: "test",
document: {
request: getDefaultGQLRequest(),
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<PersistableGQLTabState>(() => ({
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: PersistableGQLTabState) {
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: HoppGQLTab) {
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: HoppGQLDocument, switchToIt = true) {
const id = generateNewTabID()
const tab: HoppGQLTab = { 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: HoppGQLSaveContext) {
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: HoppGQLTab) => boolean) {
return Array.from(tabMap.values())
.filter(func)
.map((tab) => getTabRef(tab.id))
}
export function closeOtherTabs(tabID: string) {
if (!tabMap.has(tabID)) {
console.warn(
`The tab to close other tabs does not exist (tab id: ${tabID})`
)
return
}
tabOrdering.value = [tabID]
tabMap.forEach((_, id) => {
if (id !== tabID) tabMap.delete(id)
})
currentTabID.value = tabID
}
export function getDirtyTabsCount() {
let count = 0
for (const tab of tabMap.values()) {
if (tab.document.isDirty) count++
}
return count
}

View File

@@ -1,4 +1,7 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { HoppRESTResponse } from "../types/HoppRESTResponse"
import { HoppTestResult } from "../types/HoppTestResult"
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
export type HoppRESTSaveContext =
| {
@@ -55,4 +58,26 @@ export type HoppRESTDocument = {
* This contains where the request is originated from basically.
*/
saveContext?: HoppRESTSaveContext
/**
* The response as it is in the document
* (if any)
*/
response?: HoppRESTResponse | null
/**
* The test results as it is in the document
* (if any)
*/
testResults?: HoppTestResult | null
/**
* Response tab preference for the current tab's document
*/
responseTabPreference?: string
/**
* Options tab preference for the current tab's document
*/
optionTabPreference?: RESTOptionTabs
}

View File

@@ -1,234 +0,0 @@
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"
import { platform } from "~/platform"
import { nextTick } from "vue"
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
}
platform.analytics?.logEvent({
type: "HOPP_REST_NEW_TAB_OPENED",
})
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)
nextTick(() => {
tabMap.delete(tabID)
})
}
export function closeOtherTabs(tabID: string) {
if (!tabMap.has(tabID)) {
console.warn(
`The tab to close other tabs does not exist (tab id: ${tabID})`
)
return
}
tabOrdering.value = [tabID]
tabMap.forEach((_, id) => {
if (id !== tabID) tabMap.delete(id)
})
currentTabID.value = tabID
}
export function getDirtyTabsCount() {
let count = 0
for (const tab of tabMap.values()) {
if (tab.document.isDirty) count++
}
return count
}
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))
}

View File

@@ -198,11 +198,11 @@ export const resolvesEnvsInBody = (
}
),
}
} else {
return {
contentType: body.contentType,
body: parseTemplateString(body.body, env.variables),
}
}
return {
contentType: body.contentType,
body: parseTemplateString(body.body ?? "", env.variables),
}
}
@@ -210,9 +210,7 @@ function getFinalBodyFromRequest(
request: HoppRESTRequest,
envVariables: Environment["variables"]
): FormData | string | null {
if (request.body.contentType === null) {
return null
}
if (request.body.contentType === null) return null
if (request.body.contentType === "application/x-www-form-urlencoded") {
const parsedBodyRecord = pipe(
@@ -280,7 +278,10 @@ function getFinalBodyFromRequest(
),
toFormData
)
} else return parseBodyEnvVariables(request.body.body, envVariables)
}
// body can be null if the content-type is not set
return parseBodyEnvVariables(request.body.body ?? "", envVariables)
}
/**

View File

@@ -7,8 +7,9 @@ import {
} from "@hoppscotch/data"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { cloneDeep } from "lodash-es"
import { getTabRefWithSaveContext } from "~/helpers/rest/tab"
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
const defaultRESTCollectionState = {
state: [
@@ -454,7 +455,10 @@ const restCollectionDispatchers = defineDispatchers({
// Deal with situations where a tab with the given thing is deleted
// We are just going to dissociate the save context of the tab and mark it dirty
const tab = getTabRefWithSaveContext({
const tabService = getService(RESTTabService)
const tab = tabService.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: path,
requestIndex: requestIndex,
@@ -512,7 +516,8 @@ const restCollectionDispatchers = defineDispatchers({
destLocation.requests.push(req)
targetLocation.requests.splice(requestIndex, 1)
const possibleTab = getTabRefWithSaveContext({
const tabService = getService(RESTTabService)
const possibleTab = tabService.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: path,
requestIndex,

View File

@@ -44,14 +44,9 @@ import { SSERequest$, setSSERequest } from "./SSESession"
import { MQTTRequest$, setMQTTRequest } from "./MQTTSession"
import { bulkApplyLocalState, localStateStore } from "./localstate"
import { StorageLike, watchDebounced } from "@vueuse/core"
import {
loadTabsFromPersistedState,
persistableTabState,
} from "~/helpers/rest/tab"
import {
loadTabsFromPersistedState as loadGQLTabsFromPersistedState,
persistableTabState as persistableGQLTabState,
} from "~/helpers/graphql/tab"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
function checkAndMigrateOldSettings() {
if (window.localStorage.getItem("selectedEnvIndex")) {
@@ -320,11 +315,13 @@ function setupGlobalEnvsPersistence() {
// TODO: Graceful error handling ?
export function setupRESTTabsPersistence() {
const tabService = getService(RESTTabService)
try {
const state = window.localStorage.getItem("restTabState")
if (state) {
const data = JSON.parse(state)
loadTabsFromPersistedState(data)
tabService.loadTabsFromPersistedState(data)
}
} catch (e) {
console.error(
@@ -334,7 +331,7 @@ export function setupRESTTabsPersistence() {
}
watchDebounced(
persistableTabState,
tabService.persistableTabState,
(state) => {
window.localStorage.setItem("restTabState", JSON.stringify(state))
},
@@ -343,11 +340,13 @@ export function setupRESTTabsPersistence() {
}
function setupGQLTabsPersistence() {
const tabService = getService(GQLTabService)
try {
const state = window.localStorage.getItem("gqlTabState")
if (state) {
const data = JSON.parse(state)
loadGQLTabsFromPersistedState(data)
tabService.loadTabsFromPersistedState(data)
}
} catch (e) {
console.error(
@@ -357,7 +356,7 @@ function setupGQLTabsPersistence() {
}
watchDebounced(
persistableGQLTabState,
tabService.persistableTabState,
(state) => {
window.localStorage.setItem("gqlTabState", JSON.stringify(state))
},

View File

@@ -7,23 +7,24 @@
<HoppSmartWindows
v-if="currentTabID"
:id="'gql_windows'"
v-model="currentTabID"
:model-value="currentTabID"
@update:model-value="(tabID) => tabs.setActiveTab(tabID)"
@remove-tab="removeTab"
@add-tab="addNewTab"
@sort="sortTabs"
>
<HoppSmartWindow
v-for="tab in tabs"
v-for="tab in activeTabs"
:id="tab.id"
:key="'removable_tab_' + tab.id"
:label="tab.document.request.name"
:is-removable="tabs.length > 1"
:is-removable="activeTabs.length > 1"
:close-visibility="'hover'"
>
<template #tabhead>
<GraphqlTabHead
:tab="tab"
:is-removable="tabs.length > 1"
:is-removable="activeTabs.length > 1"
@open-rename-modal="openReqRenameModal(tab)"
@close-tab="removeTab(tab.id)"
@close-other-tabs="closeOtherTabsAction(tab.id)"
@@ -89,21 +90,15 @@ import { computed, onBeforeUnmount, ref } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { connection, disconnect } from "~/helpers/graphql/connection"
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
import {
HoppGQLTab,
closeOtherTabs,
closeTab,
createNewTab,
currentTabID,
getActiveTabs,
getDirtyTabsCount,
getTabRef,
updateTab,
updateTabOrdering,
} from "~/helpers/graphql/tab"
import { HoppGQLDocument } from "~/helpers/graphql/document"
import { InspectionService } from "~/services/inspection"
import { HoppTab } from "~/services/tab"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n()
const tabs = useService(GQLTabService)
const currentTabID = computed(() => tabs.currentTabID.value)
const inspectionService = useService(InspectionService)
@@ -113,27 +108,27 @@ usePageHead({
title: computed(() => t("navigation.graphql")),
})
const tabs = getActiveTabs()
const activeTabs = tabs.getActiveTabs()
const addNewTab = () => {
const tab = createNewTab({
const tab = tabs.createNewTab({
request: getDefaultGQLRequest(),
isDirty: false,
})
currentTabID.value = tab.id
tabs.setActiveTab(tab.id)
}
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
updateTabOrdering(e.oldIndex, e.newIndex)
tabs.updateTabOrdering(e.oldIndex, e.newIndex)
}
const removeTab = (tabID: string) => {
const tabState = getTabRef(tabID).value
const tabState = tabs.getTabRef(tabID).value
if (tabState.document.isDirty) {
confirmingCloseForTabID.value = tabID
} else {
closeTab(tabState.id)
tabs.closeTab(tabState.id)
inspectionService.deleteTabInspectorResult(tabState.id)
}
}
@@ -150,7 +145,7 @@ const onCloseConfirm = () => {
*/
const onResolveConfirm = () => {
if (confirmingCloseForTabID.value) {
closeTab(confirmingCloseForTabID.value)
tabs.closeTab(confirmingCloseForTabID.value)
confirmingCloseForTabID.value = null
}
}
@@ -160,24 +155,24 @@ const unsavedTabsCount = ref(0)
const exceptedTabID = ref<string | null>(null)
const closeOtherTabsAction = (tabID: string) => {
const dirtyTabCount = getDirtyTabsCount()
const dirtyTabCount = tabs.getDirtyTabsCount()
// If there are dirty tabs, show the confirm modal
if (dirtyTabCount > 0) {
confirmingCloseAllTabs.value = true
unsavedTabsCount.value = dirtyTabCount
exceptedTabID.value = tabID
} else {
closeOtherTabs(tabID)
tabs.closeOtherTabs(tabID)
}
}
const onResolveConfirmCloseAllTabs = () => {
if (exceptedTabID.value) closeOtherTabs(exceptedTabID.value)
if (exceptedTabID.value) tabs.closeOtherTabs(exceptedTabID.value)
confirmingCloseAllTabs.value = false
}
const onTabUpdate = (tab: HoppGQLTab) => {
updateTab(tab)
const onTabUpdate = (tab: HoppTab<HoppGQLDocument>) => {
tabs.updateTab(tab)
}
onBeforeUnmount(() => {
@@ -189,33 +184,33 @@ onBeforeUnmount(() => {
const editReqModalReqName = ref("")
const showRenamingReqNameModalForTabID = ref<string>()
const openReqRenameModal = (tab: HoppGQLTab) => {
const openReqRenameModal = (tab: HoppTab<HoppGQLDocument>) => {
editReqModalReqName.value = tab.document.request.name
showRenamingReqNameModalForTabID.value = tab.id
}
const renameReqName = () => {
const tab = getTabRef(showRenamingReqNameModalForTabID.value!)
const tab = tabs.getTabRef(showRenamingReqNameModalForTabID.value!)
if (tab.value) {
tab.value.document.request.name = editReqModalReqName.value
updateTab(tab.value)
tabs.updateTab(tab.value)
}
showRenamingReqNameModalForTabID.value = undefined
}
const duplicateTab = (tabID: string) => {
const tab = getTabRef(tabID)
const tab = tabs.getTabRef(tabID)
if (tab.value) {
const newTab = createNewTab({
const newTab = tabs.createNewTab({
request: tab.value.document.request,
isDirty: true,
})
currentTabID.value = newTab.id
tabs.setActiveTab(newTab.id)
}
}
defineActionHandler("gql.request.open", ({ request, saveContext }) => {
createNewTab({
tabs.createNewTab({
saveContext,
request: request,
isDirty: false,
@@ -223,7 +218,7 @@ defineActionHandler("gql.request.open", ({ request, saveContext }) => {
})
defineActionHandler("request.rename", () => {
openReqRenameModal(getTabRef(currentTabID.value).value!)
openReqRenameModal(tabs.getTabRef(currentTabID.value).value!)
})
defineActionHandler("tab.duplicate-tab", ({ tabID }) => {
@@ -233,7 +228,7 @@ defineActionHandler("tab.close-current", () => {
removeTab(currentTabID.value)
})
defineActionHandler("tab.close-other", () => {
closeOtherTabs(currentTabID.value)
tabs.closeOtherTabs(currentTabID.value)
})
defineActionHandler("tab.open-new", addNewTab)
</script>

View File

@@ -11,17 +11,17 @@
@sort="sortTabs"
>
<HoppSmartWindow
v-for="tab in tabs"
v-for="tab in activeTabs"
:id="tab.id"
:key="tab.id"
:label="tab.document.request.name"
:is-removable="tabs.length > 1"
:is-removable="activeTabs.length > 1"
:close-visibility="'hover'"
>
<template #tabhead>
<HttpTabHead
:tab="tab"
:is-removable="tabs.length > 1"
:is-removable="activeTabs.length > 1"
@open-rename-modal="openReqRenameModal(tab.id)"
@close-tab="removeTab(tab.id)"
@close-other-tabs="closeOtherTabsAction(tab.id)"
@@ -99,21 +99,6 @@ import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { useRoute } from "vue-router"
import { useI18n } from "@composables/i18n"
import {
closeTab,
closeOtherTabs,
createNewTab,
currentActiveTab,
currentTabID,
getActiveTabs,
getTabRef,
HoppRESTTab,
loadTabsFromPersistedState,
persistableTabState,
updateTab,
updateTabOrdering,
getDirtyTabsCount,
} from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { onLoggedIn } from "~/composables/auth"
@@ -128,7 +113,6 @@ import {
Subscription,
} from "rxjs"
import { useToast } from "~/composables/toast"
import { PersistableRESTTabState } from "~/helpers/rest/tab"
import { watchDebounced } from "@vueuse/core"
import { oauthRedirect } from "~/helpers/oauth"
import { useReadonlyStream } from "~/composables/stream"
@@ -142,6 +126,9 @@ import { HeaderInspectorService } from "~/services/inspection/inspectors/header.
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
import { cloneDeep } from "lodash-es"
import { RESTTabService } from "~/services/tab/rest"
import { HoppTab, PersistableTabState } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
const savingRequest = ref(false)
const confirmingCloseForTabID = ref<string | null>(null)
@@ -155,6 +142,10 @@ const renameTabID = ref<string | null>(null)
const t = useI18n()
const toast = useToast()
const tabs = useService(RESTTabService)
const currentTabID = tabs.currentTabID
type PopupDetails = {
show: boolean
position: {
@@ -173,13 +164,13 @@ const contextMenu = ref<PopupDetails>({
text: null,
})
const tabs = getActiveTabs()
const activeTabs = tabs.getActiveTabs()
const confirmSync = useReadonlyStream(currentSyncingStatus$, {
isInitialSync: false,
shouldSync: true,
})
const tabStateForSync = ref<PersistableRESTTabState | null>(null)
const tabStateForSync = ref<PersistableTabState<HoppRESTDocument> | null>(null)
function bindRequestToURLParams() {
const route = useRoute()
@@ -190,91 +181,92 @@ function bindRequestToURLParams() {
// We skip URL params parsing
if (Object.keys(query).length === 0 || query.code || query.error) return
const request = currentActiveTab.value.document.request
const request = tabs.currentActiveTab.value.document.request
currentActiveTab.value.document.request = safelyExtractRESTRequest(
tabs.currentActiveTab.value.document.request = safelyExtractRESTRequest(
translateExtURLParams(query, request),
getDefaultRESTRequest()
)
})
}
const onTabUpdate = (tab: HoppRESTTab) => {
updateTab(tab)
const onTabUpdate = (tab: HoppTab<HoppRESTDocument>) => {
tabs.updateTab(tab)
}
const addNewTab = () => {
const tab = createNewTab({
const tab = tabs.createNewTab({
request: getDefaultRESTRequest(),
isDirty: false,
})
currentTabID.value = tab.id
tabs.setActiveTab(tab.id)
}
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
updateTabOrdering(e.oldIndex, e.newIndex)
tabs.updateTabOrdering(e.oldIndex, e.newIndex)
}
const inspectionService = useService(InspectionService)
const removeTab = (tabID: string) => {
const tabState = getTabRef(tabID).value
const tabState = tabs.getTabRef(tabID).value
if (tabState.document.isDirty) {
confirmingCloseForTabID.value = tabID
} else {
closeTab(tabState.id)
tabs.closeTab(tabState.id)
inspectionService.deleteTabInspectorResult(tabState.id)
}
}
const closeOtherTabsAction = (tabID: string) => {
const isTabDirty = getTabRef(tabID).value?.document.isDirty
const dirtyTabCount = getDirtyTabsCount()
const isTabDirty = tabs.getTabRef(tabID).value?.document.isDirty
const dirtyTabCount = tabs.getDirtyTabsCount()
// If current tab is dirty, so we need to subtract 1 from the dirty tab count
const balanceDirtyTabCount = isTabDirty ? dirtyTabCount - 1 : dirtyTabCount
// If there are dirty tabs, show the confirm modal
if (balanceDirtyTabCount > 0) {
confirmingCloseAllTabs.value = true
unsavedTabsCount.value = balanceDirtyTabCount
exceptedTabID.value = tabID
} else {
closeOtherTabs(tabID)
tabs.closeOtherTabs(tabID)
}
}
const duplicateTab = (tabID: string) => {
const tab = getTabRef(tabID)
const tab = tabs.getTabRef(tabID)
if (tab.value) {
const newTab = createNewTab({
const newTab = tabs.createNewTab({
request: cloneDeep(tab.value.document.request),
isDirty: true,
})
currentTabID.value = newTab.id
tabs.setActiveTab(newTab.id)
}
}
const onResolveConfirmCloseAllTabs = () => {
if (exceptedTabID.value) closeOtherTabs(exceptedTabID.value)
if (exceptedTabID.value) tabs.closeOtherTabs(exceptedTabID.value)
confirmingCloseAllTabs.value = false
}
const openReqRenameModal = (tabID?: string) => {
if (tabID) {
const tab = getTabRef(tabID)
const tab = tabs.getTabRef(tabID)
reqName.value = tab.value.document.request.name
renameTabID.value = tabID
} else {
reqName.value = currentActiveTab.value.document.request.name
reqName.value = tabs.currentActiveTab.value.document.request.name
}
showRenamingReqNameModal.value = true
}
const renameReqName = () => {
const tab = getTabRef(renameTabID.value ?? currentTabID.value)
const tab = tabs.getTabRef(renameTabID.value ?? currentTabID.value)
if (tab.value) {
tab.value.document.request.name = reqName.value
updateTab(tab.value)
tabs.updateTab(tab.value)
}
showRenamingReqNameModal.value = false
}
@@ -284,7 +276,7 @@ const renameReqName = () => {
*/
const onCloseConfirmSaveTab = () => {
if (!savingRequest.value && confirmingCloseForTabID.value) {
closeTab(confirmingCloseForTabID.value)
tabs.closeTab(confirmingCloseForTabID.value)
inspectionService.deleteTabInspectorResult(confirmingCloseForTabID.value)
confirmingCloseForTabID.value = null
}
@@ -294,11 +286,11 @@ const onCloseConfirmSaveTab = () => {
* Called when the user confirms they want to save the tab
*/
const onResolveConfirmSaveTab = () => {
if (currentActiveTab.value.document.saveContext) {
if (tabs.currentActiveTab.value.document.saveContext) {
invokeAction("request.save")
if (confirmingCloseForTabID.value) {
closeTab(confirmingCloseForTabID.value)
tabs.closeTab(confirmingCloseForTabID.value)
confirmingCloseForTabID.value = null
}
} else {
@@ -312,13 +304,14 @@ const onResolveConfirmSaveTab = () => {
const onSaveModalClose = () => {
savingRequest.value = false
if (confirmingCloseForTabID.value) {
closeTab(confirmingCloseForTabID.value)
tabs.closeTab(confirmingCloseForTabID.value)
confirmingCloseForTabID.value = null
}
}
const syncTabState = () => {
if (tabStateForSync.value) loadTabsFromPersistedState(tabStateForSync.value)
if (tabStateForSync.value)
tabs.loadTabsFromPersistedState(tabStateForSync.value)
}
/**
@@ -329,10 +322,11 @@ const syncTabState = () => {
*/
function startTabStateSync(): Subscription {
const currentUser$ = platform.auth.getCurrentUserStream()
const tabState$ = new BehaviorSubject<PersistableRESTTabState | null>(null)
const tabState$ =
new BehaviorSubject<PersistableTabState<HoppRESTDocument> | null>(null)
watchDebounced(
persistableTabState,
tabs.persistableTabState,
(state) => {
tabState$.next(state)
},
@@ -429,9 +423,10 @@ function oAuthURL() {
tokenInfo.hasOwnProperty("access_token")
) {
if (
currentActiveTab.value.document.request.auth.authType === "oauth-2"
tabs.currentActiveTab.value.document.request.auth.authType ===
"oauth-2"
) {
currentActiveTab.value.document.request.auth.token =
tabs.currentActiveTab.value.document.request.auth.token =
tokenInfo.access_token
}
}
@@ -462,7 +457,7 @@ bindRequestToURLParams()
oAuthURL()
defineActionHandler("rest.request.open", ({ doc }) => {
createNewTab(doc)
tabs.createNewTab(doc)
})
defineActionHandler("request.rename", openReqRenameModal)
@@ -473,7 +468,7 @@ defineActionHandler("tab.close-current", () => {
removeTab(currentTabID.value)
})
defineActionHandler("tab.close-other", () => {
closeOtherTabs(currentTabID.value)
tabs.closeOtherTabs(currentTabID.value)
})
defineActionHandler("tab.open-new", addNewTab)

View File

@@ -82,15 +82,18 @@ import {
import IconHome from "~icons/lucide/home"
import IconRefreshCW from "~icons/lucide/refresh-cw"
import { createNewTab } from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const route = useRoute()
const router = useRouter()
const t = useI18n()
const tabs = useService(RESTTabService)
const invalidLink = ref(false)
const shortcodeID = ref("")
@@ -127,7 +130,7 @@ const addRequestToTab = () => {
const request: unknown = JSON.parse(data.right.shortcode?.request as string)
createNewTab({
tabs.createNewTab({
request: safelyExtractRESTRequest(request, getDefaultRESTRequest()),
isDirty: false,
})

View File

@@ -1,10 +1,11 @@
import { PersistableRESTTabState } from "~/helpers/rest/tab"
import { PersistableTabState } from "~/services/tab"
import { HoppUser } from "./auth"
import { HoppRESTDocument } from "~/helpers/rest/document"
export type TabStatePlatformDef = {
loadTabStateFromSync: () => Promise<PersistableRESTTabState | null>
loadTabStateFromSync: () => Promise<PersistableTabState<HoppRESTDocument> | null>
writeCurrentTabState: (
user: HoppUser,
persistableTabState: PersistableRESTTabState
persistableTabState: PersistableTabState<HoppRESTDocument>
) => Promise<void>
}

View File

@@ -11,15 +11,6 @@ vi.mock("~/modules/i18n", () => ({
getI18n: () => (x: string) => x,
}))
const tabMock = vi.hoisted(() => ({
currentActiveTab: vi.fn(),
}))
vi.mock("~/helpers/rest/tab", () => ({
__esModule: true,
currentActiveTab: tabMock.currentActiveTab,
}))
describe("ParameterMenuService", () => {
it("registers with the contextmenu service upon initialization", () => {
const container = new TestContainer()

View File

@@ -1,5 +1,6 @@
import { TestContainer } from "dioc/testing"
import { describe, expect, it, vi } from "vitest"
import { RESTTabService } from "~/services/tab/rest"
import { ContextMenuService } from "../.."
import { URLMenuService } from "../url.menu"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
@@ -9,15 +10,6 @@ vi.mock("~/modules/i18n", () => ({
getI18n: () => (x: string) => x,
}))
const tabMock = vi.hoisted(() => ({
createNewTab: vi.fn(),
}))
vi.mock("~/helpers/rest/tab", () => ({
__esModule: true,
createNewTab: tabMock.createNewTab,
}))
describe("URLMenuService", () => {
it("registers with the contextmenu service upon initialization", () => {
const container = new TestContainer()
@@ -64,6 +56,10 @@ describe("URLMenuService", () => {
it("should call the openNewTab function when action is called and a new hoppscotch tab is opened", () => {
const container = new TestContainer()
const createNewTabFn = vi.fn()
container.bindMock(RESTTabService, {
createNewTab: createNewTabFn,
})
const url = container.bind(URLMenuService)
const test = "https://hoppscotch.io"
@@ -76,8 +72,8 @@ describe("URLMenuService", () => {
endpoint: test,
}
expect(tabMock.createNewTab).toHaveBeenCalledOnce()
expect(tabMock.createNewTab).toHaveBeenCalledWith({
expect(createNewTabFn).toHaveBeenCalledOnce()
expect(createNewTabFn).toHaveBeenCalledWith({
request: request,
isDirty: false,
})

View File

@@ -7,8 +7,9 @@ import {
} from "../"
import { markRaw, ref } from "vue"
import IconArrowDownRight from "~icons/lucide/arrow-down-right"
import { currentActiveTab } from "~/helpers/rest/tab"
import { getI18n } from "~/modules/i18n"
import { RESTTabService } from "~/services/tab/rest"
import { getService } from "~/modules/dioc"
//regex containing both url and parameter
const urlAndParameterRegex = new RegExp("[^&?]*?=[^&?]*")
@@ -88,20 +89,23 @@ export class ParameterMenuService extends Service implements ContextMenu {
queryParams.push({ key, value, active: true })
}
const tabService = getService(RESTTabService)
// add the parameters to the current request parameters
currentActiveTab.value.document.request.params = [
...currentActiveTab.value.document.request.params,
tabService.currentActiveTab.value.document.request.params = [
...tabService.currentActiveTab.value.document.request.params,
...queryParams,
]
if (newURL) {
currentActiveTab.value.document.request.endpoint = newURL
tabService.currentActiveTab.value.document.request.endpoint = newURL
} else {
// remove the parameter from the URL
const textRegex = new RegExp(`\\b${text.replace(/\?/g, "")}\\b`, "gi")
const sanitizedWord = currentActiveTab.value.document.request.endpoint
const sanitizedWord =
tabService.currentActiveTab.value.document.request.endpoint
const newURL = sanitizedWord.replace(textRegex, "")
currentActiveTab.value.document.request.endpoint = newURL
tabService.currentActiveTab.value.document.request.endpoint = newURL
}
}

View File

@@ -1,15 +1,15 @@
import { Service } from "dioc"
import { markRaw, ref } from "vue"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { getI18n } from "~/modules/i18n"
import { RESTTabService } from "~/services/tab/rest"
import IconCopyPlus from "~icons/lucide/copy-plus"
import {
ContextMenu,
ContextMenuResult,
ContextMenuService,
ContextMenuState,
} from ".."
import { markRaw, ref } from "vue"
import IconCopyPlus from "~icons/lucide/copy-plus"
import { createNewTab } from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { getI18n } from "~/modules/i18n"
/**
* Used to check if a string is a valid URL
@@ -37,6 +37,7 @@ export class URLMenuService extends Service implements ContextMenu {
public readonly menuID = "url"
private readonly contextMenu = this.bind(ContextMenuService)
private readonly restTab = this.bind(RESTTabService)
constructor() {
super()
@@ -55,7 +56,7 @@ export class URLMenuService extends Service implements ContextMenu {
endpoint: url,
}
createNewTab({
this.restTab.createNewTab({
request: request,
isDirty: false,
})

View File

@@ -3,8 +3,8 @@ import { refDebounced } from "@vueuse/core"
import { Service } from "dioc"
import { computed, markRaw, reactive } from "vue"
import { Component, Ref, ref, watch } from "vue"
import { currentActiveTab } from "~/helpers/rest/tab"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { RESTTabService } from "../tab/rest"
/**
* Defines how to render the text in an Inspector Result
@@ -105,6 +105,8 @@ export class InspectionService extends Service {
private tabs: Ref<Map<string, InspectorResult[]>> = ref(new Map())
private readonly restTab = this.bind(RESTTabService)
constructor() {
super()
@@ -122,10 +124,14 @@ export class InspectionService extends Service {
private initializeListeners() {
watch(
() => [this.inspectors.entries(), currentActiveTab.value.id],
() => [this.inspectors.entries(), this.restTab.currentActiveTab.value.id],
() => {
const reqRef = computed(() => currentActiveTab.value.document.request)
const resRef = computed(() => currentActiveTab.value.response)
const reqRef = computed(
() => this.restTab.currentActiveTab.value.document.request
)
const resRef = computed(
() => this.restTab.currentActiveTab.value.document.response
)
const debouncedReq = refDebounced(reqRef, 1000, { maxWait: 2000 })
const debouncedRes = refDebounced(resRef, 1000, { maxWait: 2000 })
@@ -142,7 +148,7 @@ export class InspectionService extends Service {
() => [...inspectorRefs.flatMap((x) => x!.value)],
() => {
this.tabs.value.set(
currentActiveTab.value.id,
this.restTab.currentActiveTab.value.id,
activeInspections.value
)
},

View File

@@ -7,20 +7,12 @@ import { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { HoppAction, HoppActionWithArgs } from "~/helpers/actions"
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
import { RESTTabService } from "~/services/tab/rest"
async function flushPromises() {
return await new Promise((r) => setTimeout(r))
}
const tabMock = vi.hoisted(() => ({
createNewTab: vi.fn(),
}))
vi.mock("~/helpers/rest/tab", () => ({
__esModule: true,
createNewTab: tabMock.createNewTab,
}))
vi.mock("~/modules/i18n", () => ({
__esModule: true,
getI18n: () => (x: string) => x,
@@ -72,8 +64,16 @@ describe("HistorySpotlightSearcherService", () => {
y = historyMock.restEntries.pop()
}
const container = new TestContainer()
const createNewTabFn = vi.fn()
container.bindMock(RESTTabService, {
createNewTab: createNewTabFn,
})
actionsMock.invokeAction.mockReset()
tabMock.createNewTab.mockReset()
createNewTabFn.mockReset()
})
it("registers with the spotlight service upon initialization", async () => {

View File

@@ -16,10 +16,6 @@ import {
import IconFolder from "~icons/lucide/folder"
import RESTRequestSpotlightEntry from "~/components/app/spotlight/entry/RESTRequest.vue"
import GQLRequestSpotlightEntry from "~/components/app/spotlight/entry/GQLRequest.vue"
import { createNewTab } from "~/helpers/rest/tab"
import { createNewTab as createNewGQLTab } from "~/helpers/graphql/tab"
import { getTabRefWithSaveContext } from "~/helpers/rest/tab"
import { currentTabID } from "~/helpers/rest/tab"
import {
HoppCollection,
HoppGQLRequest,
@@ -27,6 +23,8 @@ import {
} from "@hoppscotch/data"
import { WorkspaceService } from "~/services/workspace.service"
import { invokeAction } from "~/helpers/actions"
import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
/**
* A spotlight searcher that searches through the user's collections
@@ -44,6 +42,9 @@ export class CollectionsSpotlightSearcherService
public searcherID = "collections"
public searcherSectionTitle = this.t("collection.my_collections")
private readonly restTab = this.bind(RESTTabService)
private readonly gqlTab = this.bind(GQLTabService)
private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
@@ -290,21 +291,21 @@ export class CollectionsSpotlightSearcherService
})
}
const possibleTab = getTabRefWithSaveContext({
const possibleTab = this.restTab.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: folderPath.join("/"),
requestIndex: reqIndex,
})
if (possibleTab) {
currentTabID.value = possibleTab.value.id
this.restTab.setActiveTab(possibleTab.value.id)
} else {
const req = this.getRESTFolderFromFolderPath(folderPath.join("/"))
?.requests[reqIndex]
if (!req) return
createNewTab(
this.restTab.createNewTab(
{
request: req,
isDirty: false,
@@ -326,7 +327,7 @@ export class CollectionsSpotlightSearcherService
if (!req) return
createNewGQLTab({
this.gqlTab.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: folderPath.join("/"),

View File

@@ -8,8 +8,7 @@ import {
} from "./base/static.searcher"
import { useRoute } from "vue-router"
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
import { currentActiveTab } from "~/helpers/rest/tab"
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
import IconWindow from "~icons/lucide/app-window"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconCode2 from "~icons/lucide/code-2"
@@ -20,6 +19,7 @@ import IconPlay from "~icons/lucide/play"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { RESTTabService } from "~/services/tab/rest"
type Doc = {
text: string | string[]
@@ -43,6 +43,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
public searcherSectionTitle = this.t("shortcut.request.title")
private readonly spotlight = this.bind(SpotlightService)
private readonly restTab = this.bind(RESTTabService)
private route = useRoute()
private isRESTPage = computed(() => this.route.name === "index")
@@ -247,7 +248,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
}
}
private openRequestTab(tab: RequestOptionTabs | GQLOptionTabs): void {
private openRequestTab(tab: RESTOptionTabs | GQLOptionTabs): void {
invokeAction("request.open-tab", {
tab,
})
@@ -267,7 +268,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
case "save_to_collections":
invokeAction("request.save-as", {
requestType: "rest",
request: currentActiveTab.value?.document.request,
request: this.restTab.currentActiveTab.value?.document.request,
})
break
case "save_request":

View File

@@ -12,8 +12,8 @@ import IconCopyPlus from "~icons/lucide/copy-plus"
import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square"
import { invokeAction } from "~/helpers/actions"
import { getActiveTabs as getRESTActiveTabs } from "~/helpers/rest/tab"
import { getActiveTabs as getGQLActiveTabs } from "~/helpers/graphql/tab"
import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
type Doc = {
text: string | string[]
@@ -42,12 +42,14 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
private showAction = computed(
() => this.route.name === "index" || this.route.name === "graphql"
)
private gqlActiveTabs = getGQLActiveTabs()
private restActiveTabs = getRESTActiveTabs()
private readonly restTab = this.bind(RESTTabService)
private readonly gqlTab = this.bind(GQLTabService)
private isOnlyTab = computed(() =>
this.route.name === "graphql"
? this.gqlActiveTabs.value.length === 1
: this.restActiveTabs.value.length === 1
? this.gqlTab.getActiveTabs().value.length === 1
: this.restTab.getActiveTabs().value.length === 1
)
private documents: Record<string, Doc> = reactive({

View File

@@ -0,0 +1,137 @@
import { describe, expect, it } from "vitest"
import { TestContainer } from "dioc/testing"
import { TabService } from "../tab"
import { reactive } from "vue"
class MockTabService extends TabService<{ request: string }> {
public static readonly ID = "MOCK_TAB_SERVICE"
constructor() {
super()
this.tabMap = reactive(
new Map([
[
"test",
{
id: "test",
document: {
request: "test request",
},
},
],
])
)
this.watchCurrentTabID()
}
}
describe("TabService", () => {
it("initially only one tab is defined", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
expect(service.getActiveTabs().value.length).toEqual(1)
})
it("initially the only tab is the active tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
expect(service.getActiveTab()).not.toBeNull()
})
it("initially active tab id is test", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
expect(service.getActiveTab()?.id).toEqual("test")
})
it("add new tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.createNewTab({
request: "new request",
})
expect(service.getActiveTabs().value.length).toEqual(2)
})
it("get active tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
console.log(service.getActiveTab())
expect(service.getActiveTab()?.id).toEqual("test")
})
it("sort tabs", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
const currentOrder = service.updateTabOrdering(1, 0)
expect(currentOrder[1]).toEqual("test")
})
it("update tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.updateTab({
id: service.currentTabID.value,
document: {
request: "updated request",
},
})
expect(service.getActiveTab()?.document.request).toEqual("updated request")
})
it("set new active tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.setActiveTab("test")
expect(service.getActiveTab()?.id).toEqual("test")
})
it("close other tabs", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.closeOtherTabs("test")
expect(service.getActiveTabs().value.length).toEqual(1)
})
it("close tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.createNewTab({
request: "new request",
})
expect(service.getActiveTabs().value.length).toEqual(2)
service.closeTab("test")
expect(service.getActiveTabs().value.length).toEqual(1)
})
})

View File

@@ -0,0 +1,66 @@
import { isEqual } from "lodash-es"
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
import { HoppGQLDocument, HoppGQLSaveContext } from "~/helpers/graphql/document"
import { TabService } from "./tab"
import { computed } from "vue"
export class GQLTabService extends TabService<HoppGQLDocument> {
public static readonly ID = "GQL_TAB_SERVICE"
constructor() {
super()
this.tabMap.set("test", {
id: "test",
document: {
request: getDefaultGQLRequest(),
isDirty: false,
optionTabPreference: "query",
},
})
this.watchCurrentTabID()
}
// override persistableTabState to remove response from the document
public override persistableTabState = computed(() => ({
lastActiveTabID: this.currentTabID.value,
orderedDocs: this.tabOrdering.value.map((tabID) => {
const tab = this.tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
return {
tabID: tab.id,
doc: {
...tab.document,
response: null,
},
}
}),
}))
public getTabRefWithSaveContext(ctx: HoppGQLSaveContext) {
for (const tab of this.tabMap.values()) {
// For `team-collection` request id can be considered unique
if (ctx?.originLocation === "team-collection") {
if (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.requestID === ctx.requestID
) {
return this.getTabRef(tab.id)
}
} else if (isEqual(ctx, tab.document.saveContext))
return this.getTabRef(tab.id)
}
return null
}
public getDirtyTabsCount() {
let count = 0
for (const tab of this.tabMap.values()) {
if (tab.document.isDirty) count++
}
return count
}
}

View File

@@ -0,0 +1,116 @@
import { ComputedRef, WritableComputedRef } from "vue"
/**
* Represents a tab in HoppScotch.
* @template Doc The type of the document associated with the tab.
*/
export type HoppTab<Doc> = {
/** The unique identifier of the tab. */
id: string
/** The document associated with the tab. */
document: Doc
}
export type PersistableTabState<Doc> = {
lastActiveTabID: string
orderedDocs: Array<{
tabID: string
doc: Doc
}>
}
/**
* Represents a service for managing tabs with documents.
* @template Doc - The type of document associated with each tab.
*/
export interface TabService<Doc> {
/**
* Gets the current active tab.
*/
currentActiveTab: ComputedRef<HoppTab<Doc>>
/**
* Creates a new tab with the given document and sets it as the active tab.
* @param document - The document to associate with the new tab.
* @returns The newly created tab.
*/
createNewTab(document: Doc): HoppTab<Doc>
/**
* Gets an array of all tabs.
* @returns An array of all tabs.
*/
getTabs(): HoppTab<Doc>[]
/**
* Gets the currently active tab.
* @returns The active tab or null if no tab is active.
*/
getActiveTab(): HoppTab<Doc> | null
/**
* Sets the active tab by its ID.
* @param tabID - The ID of the tab to set as active.
*/
setActiveTab(tabID: string): void
/**
* Loads tabs and their ordering from a persisted state.
* @param data - The persisted tab state to load.
*/
loadTabsFromPersistedState(data: PersistableTabState<Doc>): void
/**
* Gets a read-only computed reference to the active tabs.
* @returns A computed reference to the active tabs.
*/
getActiveTabs(): Readonly<ComputedRef<HoppTab<Doc>[]>>
/**
* Gets a computed reference to a specific tab by its ID.
* @param tabID - The ID of the tab to retrieve.
* @returns A computed reference to the specified tab.
* @throws An error if the tab with the specified ID does not exist.
*/
getTabRef(tabID: string): WritableComputedRef<HoppTab<Doc>>
/**
* Updates the properties of a tab.
* @param tabUpdate - The updated tab object.
*/
updateTab(tabUpdate: HoppTab<Doc>): void
/**
* Updates the ordering of tabs by moving a tab from one index to another.
* @param fromIndex - The current index of the tab to move.
* @param toIndex - The target index where the tab should be moved to.
*/
updateTabOrdering(fromIndex: number, toIndex: number): void
/**
* Closes the tab with the specified ID.
* @param tabID - The ID of the tab to close.
*/
closeTab(tabID: string): void
/**
* Closes all tabs except the one with the specified ID.
* @param tabID - The ID of the tab to keep open.
*/
closeOtherTabs(tabID: string): void
/**
* Gets a computed reference to a persistable tab state.
* @returns A computed reference to a persistable tab state object.
*/
persistableTabState: ComputedRef<PersistableTabState<Doc>>
/**
* Gets computed references to tabs that match a specified condition.
* @param func - A function that defines the condition for selecting tabs.
* @returns An array of computed references to matching tabs.
*/
getTabsRefTo(
func: (tab: HoppTab<Doc>) => boolean
): WritableComputedRef<HoppTab<Doc>>[]
}

View File

@@ -0,0 +1,67 @@
import { isEqual } from "lodash-es"
import { computed } from "vue"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { HoppRESTDocument, HoppRESTSaveContext } from "~/helpers/rest/document"
import { TabService } from "./tab"
export class RESTTabService extends TabService<HoppRESTDocument> {
public static readonly ID = "REST_TAB_SERVICE"
constructor() {
super()
this.tabMap.set("test", {
id: "test",
document: {
request: getDefaultRESTRequest(),
isDirty: false,
optionTabPreference: "params",
},
})
this.watchCurrentTabID()
}
// override persistableTabState to remove response from the document
public override persistableTabState = computed(() => ({
lastActiveTabID: this.currentTabID.value,
orderedDocs: this.tabOrdering.value.map((tabID) => {
const tab = this.tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
return {
tabID: tab.id,
doc: {
...tab.document,
response: null,
},
}
}),
}))
public getTabRefWithSaveContext(ctx: HoppRESTSaveContext) {
for (const tab of this.tabMap.values()) {
// For `team-collection` request id can be considered unique
if (ctx?.originLocation === "team-collection") {
if (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.requestID === ctx.requestID
) {
return this.getTabRef(tab.id)
}
} else if (isEqual(ctx, tab.document.saveContext)) {
return this.getTabRef(tab.id)
}
}
return null
}
public getDirtyTabsCount() {
let count = 0
for (const tab of this.tabMap.values()) {
if (tab.document.isDirty) count++
}
return count
}
}

View File

@@ -0,0 +1,207 @@
import { refWithControl } from "@vueuse/core"
import { Service } from "dioc"
import { v4 as uuidV4 } from "uuid"
import {
ComputedRef,
computed,
nextTick,
reactive,
ref,
shallowReadonly,
watch,
} from "vue"
import {
HoppTab,
PersistableTabState,
TabService as TabServiceInterface,
} from "."
export abstract class TabService<Doc>
extends Service
implements TabServiceInterface<Doc>
{
protected tabMap = reactive(new Map<string, HoppTab<Doc>>())
protected tabOrdering = ref<string[]>(["test"])
public currentTabID = refWithControl("test", {
onBeforeChange: (newTabID) => {
if (!newTabID || !this.tabMap.has(newTabID)) {
console.warn(
`Tried to set current tab id to an invalid value. (value: ${newTabID})`
)
// Don't allow change
return false
}
},
})
public currentActiveTab = computed(
() => this.tabMap.get(this.currentTabID.value)!
) // Guaranteed to not be undefined
protected watchCurrentTabID() {
watch(
this.tabOrdering,
(newOrdering) => {
if (
!this.currentTabID.value ||
!newOrdering.includes(this.currentTabID.value)
) {
this.setActiveTab(newOrdering[newOrdering.length - 1]) // newOrdering should always be non-empty
}
},
{ deep: true }
)
}
public createNewTab(document: Doc, switchToIt = true): HoppTab<Doc> {
const id = this.generateNewTabID()
const tab: HoppTab<Doc> = { id, document }
this.tabMap.set(id, tab)
this.tabOrdering.value.push(id)
if (switchToIt) {
this.setActiveTab(id)
}
return tab
}
public getTabs(): HoppTab<Doc>[] {
return Array.from(this.tabMap.values())
}
public getActiveTab(): HoppTab<Doc> | null {
return this.tabMap.get(this.currentTabID.value) ?? null
}
public setActiveTab(tabID: string): void {
this.currentTabID.value = tabID
}
public loadTabsFromPersistedState(data: PersistableTabState<Doc>): void {
if (data) {
this.tabMap.clear()
this.tabOrdering.value = []
for (const doc of data.orderedDocs) {
this.tabMap.set(doc.tabID, {
id: doc.tabID,
document: doc.doc,
})
this.tabOrdering.value.push(doc.tabID)
}
this.setActiveTab(data.lastActiveTabID)
}
}
public getActiveTabs(): Readonly<ComputedRef<HoppTab<Doc>[]>> {
return shallowReadonly(
computed(() => this.tabOrdering.value.map((x) => this.tabMap.get(x)!))
)
}
public getTabRef(tabID: string) {
return computed({
get: () => {
const result = this.tabMap.get(tabID)
if (result === undefined) throw new Error(`Invalid tab id: ${tabID}`)
return result
},
set: (value) => {
return this.tabMap.set(tabID, value)
},
})
}
public updateTab(tabUpdate: HoppTab<Doc>) {
if (!this.tabMap.has(tabUpdate.id)) {
console.warn(
`Cannot update tab as tab with that tab id does not exist (id: ${tabUpdate.id})`
)
}
this.tabMap.set(tabUpdate.id, tabUpdate)
}
public updateTabOrdering(fromIndex: number, toIndex: number) {
this.tabOrdering.value.splice(
toIndex,
0,
this.tabOrdering.value.splice(fromIndex, 1)[0]
)
return this.tabOrdering.value
}
public closeTab(tabID: string) {
if (!this.tabMap.has(tabID)) {
console.warn(
`Tried to close a tab which does not exist (tab id: ${tabID})`
)
return
}
if (this.tabOrdering.value.length === 1) {
console.warn(
`Tried to close the only tab open, which is not allowed. (tab id: ${tabID})`
)
return
}
this.tabOrdering.value.splice(this.tabOrdering.value.indexOf(tabID), 1)
nextTick(() => {
this.tabMap.delete(tabID)
})
}
public closeOtherTabs(tabID: string) {
if (!this.tabMap.has(tabID)) {
console.warn(
`The tab to close other tabs does not exist (tab id: ${tabID})`
)
return
}
this.tabOrdering.value = [tabID]
this.tabMap.forEach((_, id) => {
if (id !== tabID) this.tabMap.delete(id)
})
this.currentTabID.value = tabID
}
public persistableTabState = computed<PersistableTabState<Doc>>(() => ({
lastActiveTabID: this.currentTabID.value,
orderedDocs: this.tabOrdering.value.map((tabID) => {
const tab = this.tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
return {
tabID: tab.id,
doc: tab.document,
}
}),
}))
public getTabsRefTo(func: (tab: HoppTab<Doc>) => boolean) {
return Array.from(this.tabMap.values())
.filter(func)
.map((tab) => this.getTabRef(tab.id))
}
private generateNewTabID() {
while (true) {
const id = uuidV4()
if (!this.tabMap.has(id)) return id
}
}
}

View File

@@ -1,4 +1,5 @@
import { PersistableRESTTabState } from "@hoppscotch/common/helpers/rest/tab"
import { PersistableTabState } from "@hoppscotch/common/services/tab"
import { HoppRESTDocument } from "@hoppscotch/common/helpers/rest/document"
import { HoppUser } from "@hoppscotch/common/platform/auth"
import { TabStatePlatformDef } from "@hoppscotch/common/platform/tab"
import { def as platformAuth } from "@platform/auth"
@@ -8,12 +9,12 @@ import * as E from "fp-ts/Either"
async function writeCurrentTabState(
_: HoppUser,
persistableTabState: PersistableRESTTabState
persistableTabState: PersistableTabState<HoppRESTDocument>
) {
await updateUserSession(JSON.stringify(persistableTabState), SessionType.Rest)
}
async function loadTabStateFromSync(): Promise<PersistableRESTTabState | null> {
async function loadTabStateFromSync(): Promise<PersistableTabState<HoppRESTDocument> | null> {
const currentUser = platformAuth.getCurrentUser()
if (!currentUser)

View File

@@ -52,6 +52,7 @@ const {
addTabEntry,
updateTabEntry,
removeTabEntry,
isUnmounting,
} = inject<TabProvider>("tabs-system")!
const active = computed(() => activeTabID.value === props.id)
@@ -73,6 +74,7 @@ watch(tabMeta, (newMeta) => {
})
onBeforeUnmount(() => {
if (isUnmounting.value) return
removeTabEntry(props.id)
})
</script>

View File

@@ -76,8 +76,8 @@ import { pipe } from "fp-ts/function"
import { not } from "fp-ts/Predicate"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import type { Component } from "vue"
import { ref, ComputedRef, computed, provide } from "vue"
import type { Component, Ref } from "vue"
import { ref, ComputedRef, computed, provide, onBeforeUnmount } from "vue"
export type TabMeta = {
label: string | null
@@ -94,6 +94,7 @@ export type TabProvider = {
addTabEntry: (tabID: string, meta: TabMeta) => void
updateTabEntry: (tabID: string, newMeta: TabMeta) => void
removeTabEntry: (tabID: string) => void
isUnmounting: Ref<boolean>
}
const props = defineProps({
@@ -165,12 +166,19 @@ const removeTabEntry = (tabID: string) => {
if (tabEntries.value.length > 0) selectTab(tabEntries.value[0][0])
}
const isUnmounting = ref(false)
provide<TabProvider>("tabs-system", {
renderInactive: computed(() => props.renderInactiveTabs),
activeTabID: computed(() => props.modelValue),
addTabEntry,
updateTabEntry,
removeTabEntry,
isUnmounting,
})
onBeforeUnmount(() => {
isUnmounting.value = true
})
const selectTab = (id: string) => {