feat: cleaner save context handling for graphql (#3282)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -80,6 +80,7 @@ declare module 'vue' {
|
|||||||
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
||||||
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
||||||
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
|
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
|
||||||
|
GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
|
||||||
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
|
||||||
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
|
||||||
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
|
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
|
||||||
@@ -89,7 +90,7 @@ declare module 'vue' {
|
|||||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||||
HoppSmartAutoComplete: typeof import("@hoppscotch/ui")["HoppSmartAutoComplete"]
|
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||||
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||||
@@ -151,6 +152,7 @@ declare module 'vue' {
|
|||||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
|
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
@click="
|
@click="
|
||||||
emit('add-request', {
|
emit('add-request', {
|
||||||
path: `${collectionIndex}`,
|
path: `${collectionIndex}`,
|
||||||
|
index: collection.requests.length,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -219,6 +220,7 @@ import {
|
|||||||
moveGraphqlRequest,
|
moveGraphqlRequest,
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
import { Picked } from "~/helpers/types/HoppPicked"
|
import { Picked } from "~/helpers/types/HoppPicked"
|
||||||
|
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
picked: { type: Object, default: null },
|
picked: { type: Object, default: null },
|
||||||
@@ -293,6 +295,22 @@ const removeCollection = () => {
|
|||||||
emit("select", null)
|
emit("select", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const possibleTabs = getTabsRefTo((tab) => {
|
||||||
|
const ctx = tab.document.saveContext
|
||||||
|
|
||||||
|
if (!ctx) return false
|
||||||
|
|
||||||
|
return (
|
||||||
|
ctx.originLocation === "user-collection" &&
|
||||||
|
ctx.folderPath.startsWith(props.collectionIndex.toString())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const tab of possibleTabs) {
|
||||||
|
tab.value.document.saveContext = undefined
|
||||||
|
tab.value.document.isDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
removeGraphqlCollection(props.collectionIndex, props.collection.id)
|
removeGraphqlCollection(props.collectionIndex, props.collection.id)
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,12 @@
|
|||||||
:icon="IconFilePlus"
|
:icon="IconFilePlus"
|
||||||
:title="t('request.new')"
|
:title="t('request.new')"
|
||||||
class="hidden group-hover:inline-flex"
|
class="hidden group-hover:inline-flex"
|
||||||
@click="emit('add-request', { path: folderPath })"
|
@click="
|
||||||
|
emit('add-request', {
|
||||||
|
path: folderPath,
|
||||||
|
index: folder.requests.length,
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -198,6 +203,7 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
||||||
import { computed, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
|
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -249,10 +255,8 @@ const collectionIcon = computed(() => {
|
|||||||
|
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
emit("select", {
|
emit("select", {
|
||||||
picked: {
|
pickedType: "gql-my-folder",
|
||||||
pickedType: "gql-my-folder",
|
folderPath: props.folderPath,
|
||||||
folderPath: props.folderPath,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +277,22 @@ const removeFolder = () => {
|
|||||||
emit("select", { picked: null })
|
emit("select", { picked: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const possibleTabs = getTabsRefTo((tab) => {
|
||||||
|
const ctx = tab.document.saveContext
|
||||||
|
|
||||||
|
if (!ctx) return false
|
||||||
|
|
||||||
|
return (
|
||||||
|
ctx.originLocation === "user-collection" &&
|
||||||
|
ctx.folderPath.startsWith(props.folderPath)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const tab of possibleTabs) {
|
||||||
|
tab.value.document.saveContext = undefined
|
||||||
|
tab.value.document.isDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
removeGraphqlFolder(props.folderPath, props.folder.id)
|
removeGraphqlFolder(props.folderPath, props.folder.id)
|
||||||
toast.success(t("state.deleted"))
|
toast.success(t("state.deleted"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,22 +20,28 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
||||||
@click="selectRequest()"
|
@click="selectRequest()"
|
||||||
>
|
>
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
{{ request.name }}
|
{{ request.name }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isActive"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
||||||
|
:title="`${t('collection.request_in_use')}`"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="!saveRequest"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:icon="IconRotateCCW"
|
|
||||||
:title="t('action.restore')"
|
|
||||||
class="hidden group-hover:inline-flex"
|
|
||||||
@click="selectRequest()"
|
|
||||||
/>
|
|
||||||
<span>
|
<span>
|
||||||
<tippy
|
<tippy
|
||||||
ref="options"
|
ref="options"
|
||||||
@@ -121,7 +127,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
import IconFile from "~icons/lucide/file"
|
import IconFile from "~icons/lucide/file"
|
||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||||
import IconEdit from "~icons/lucide/edit"
|
import IconEdit from "~icons/lucide/edit"
|
||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
@@ -132,7 +137,12 @@ import { useToast } from "@composables/toast"
|
|||||||
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import { removeGraphqlRequest } from "~/newstore/collections"
|
import { removeGraphqlRequest } from "~/newstore/collections"
|
||||||
import { createNewTab } from "~/helpers/graphql/tab"
|
import {
|
||||||
|
createNewTab,
|
||||||
|
getTabRefWithSaveContext,
|
||||||
|
currentTabID,
|
||||||
|
currentActiveTab,
|
||||||
|
} from "~/helpers/graphql/tab"
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
@@ -154,6 +164,18 @@ const props = defineProps({
|
|||||||
requestIndex: { type: Number, default: null },
|
requestIndex: { type: Number, default: null },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isActive = computed(() => {
|
||||||
|
const saveCtx = currentActiveTab.value?.document.saveContext
|
||||||
|
|
||||||
|
if (!saveCtx) return false
|
||||||
|
|
||||||
|
return (
|
||||||
|
saveCtx.originLocation === "user-collection" &&
|
||||||
|
saveCtx.folderPath === props.folderPath &&
|
||||||
|
saveCtx.requestIndex === props.requestIndex
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: Better types please
|
// TODO: Better types please
|
||||||
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
||||||
|
|
||||||
@@ -179,7 +201,24 @@ const selectRequest = () => {
|
|||||||
if (props.saveRequest) {
|
if (props.saveRequest) {
|
||||||
pick()
|
pick()
|
||||||
} else {
|
} else {
|
||||||
|
const possibleTab = getTabRefWithSaveContext({
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath: props.folderPath,
|
||||||
|
requestIndex: props.requestIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Switch to that request if that request is open
|
||||||
|
if (possibleTab) {
|
||||||
|
currentTabID.value = possibleTab.value.id
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
createNewTab({
|
createNewTab({
|
||||||
|
saveContext: {
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath: props.folderPath,
|
||||||
|
requestIndex: props.requestIndex,
|
||||||
|
},
|
||||||
request: cloneDeep(
|
request: cloneDeep(
|
||||||
makeGQLRequest({
|
makeGQLRequest({
|
||||||
name: props.request.name,
|
name: props.request.name,
|
||||||
@@ -213,6 +252,18 @@ const removeRequest = () => {
|
|||||||
emit("select", null)
|
emit("select", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detach the request from any of the tabs
|
||||||
|
const possibleTab = getTabRefWithSaveContext({
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath: props.folderPath,
|
||||||
|
requestIndex: props.requestIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (possibleTab) {
|
||||||
|
possibleTab.value.document.saveContext = undefined
|
||||||
|
possibleTab.value.document.isDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
|
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
|
||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ export default defineComponent({
|
|||||||
this.$data.editingCollectionIndex = collectionIndex
|
this.$data.editingCollectionIndex = collectionIndex
|
||||||
this.displayModalEdit(true)
|
this.displayModalEdit(true)
|
||||||
},
|
},
|
||||||
onAddRequest({ name, path }) {
|
onAddRequest({ name, path, index }) {
|
||||||
const newRequest = {
|
const newRequest = {
|
||||||
...currentActiveTab.value.document.request,
|
...currentActiveTab.value.document.request,
|
||||||
name,
|
name,
|
||||||
@@ -274,6 +274,11 @@ export default defineComponent({
|
|||||||
saveGraphqlRequestAs(path, newRequest)
|
saveGraphqlRequestAs(path, newRequest)
|
||||||
|
|
||||||
createNewTab({
|
createNewTab({
|
||||||
|
saveContext: {
|
||||||
|
originLocation: "user-collection",
|
||||||
|
folderPath: path,
|
||||||
|
requestIndex: index,
|
||||||
|
},
|
||||||
request: newRequest,
|
request: newRequest,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
|
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
||||||
>
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<label class="font-semibold truncate text-secondaryLight">
|
<label class="font-semibold truncate text-secondaryLight">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
|
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
||||||
>
|
>
|
||||||
<label class="font-semibold text-secondaryLight">
|
<label class="font-semibold text-secondaryLight">
|
||||||
{{ t("tab.headers") }}
|
{{ t("tab.headers") }}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight gqlRunQuery"
|
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
||||||
>
|
>
|
||||||
<label class="font-semibold text-secondaryLight">
|
<label class="font-semibold text-secondaryLight">
|
||||||
{{ t("request.query") }}
|
{{ t("request.query") }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="flex flex-col flex-1 h-full">
|
<div class="flex flex-col flex-1 h-full">
|
||||||
<HoppSmartTabs
|
<HoppSmartTabs
|
||||||
v-model="selectedOptionTab"
|
v-model="selectedOptionTab"
|
||||||
styles="sticky bg-primary z-10"
|
styles="sticky top-0 bg-primary z-10 border-b-0"
|
||||||
:render-inactive-tabs="true"
|
:render-inactive-tabs="true"
|
||||||
>
|
>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
@@ -67,6 +67,7 @@ import {
|
|||||||
} from "~/helpers/graphql/connection"
|
} from "~/helpers/graphql/connection"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
|
import { editGraphqlRequest } from "~/newstore/collections"
|
||||||
|
|
||||||
type OptionTabs = "query" | "headers" | "variables" | "authorization"
|
type OptionTabs = "query" | "headers" | "variables" | "authorization"
|
||||||
const selectedOptionTab = ref<OptionTabs>("query")
|
const selectedOptionTab = ref<OptionTabs>("query")
|
||||||
@@ -180,12 +181,29 @@ const hideRequestModal = () => {
|
|||||||
showSaveRequestModal.value = false
|
showSaveRequestModal.value = false
|
||||||
}
|
}
|
||||||
const saveRequest = () => {
|
const saveRequest = () => {
|
||||||
showSaveRequestModal.value = true
|
if (
|
||||||
|
currentActiveTab.value.document.saveContext &&
|
||||||
|
currentActiveTab.value.document.saveContext.originLocation ===
|
||||||
|
"user-collection"
|
||||||
|
) {
|
||||||
|
editGraphqlRequest(
|
||||||
|
currentActiveTab.value.document.saveContext.folderPath,
|
||||||
|
currentActiveTab.value.document.saveContext.requestIndex,
|
||||||
|
currentActiveTab.value.document.request
|
||||||
|
)
|
||||||
|
|
||||||
|
currentActiveTab.value.document.isDirty = false
|
||||||
|
} else {
|
||||||
|
showSaveRequestModal.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const clearGQLQuery = () => {
|
const clearGQLQuery = () => {
|
||||||
request.value.query = ""
|
request.value.query = ""
|
||||||
}
|
}
|
||||||
defineActionHandler("request.send-cancel", runQuery)
|
defineActionHandler("request.send-cancel", runQuery)
|
||||||
defineActionHandler("request.save", saveRequest)
|
defineActionHandler("request.save", saveRequest)
|
||||||
|
defineActionHandler("request.save-as", () => {
|
||||||
|
showSaveRequestModal.value = true
|
||||||
|
})
|
||||||
defineActionHandler("request.reset", clearGQLQuery)
|
defineActionHandler("request.reset", clearGQLQuery)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
118
packages/hoppscotch-common/src/components/graphql/TabHead.vue
Normal file
118
packages/hoppscotch-common/src/components/graphql/TabHead.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||||
|
:title="tab.document.request.name"
|
||||||
|
class="truncate px-2 flex items-center"
|
||||||
|
@dblclick="emit('open-rename-modal')"
|
||||||
|
@contextmenu.prevent="options?.tippy?.show()"
|
||||||
|
@click.middle="emit('close-tab')"
|
||||||
|
>
|
||||||
|
<tippy
|
||||||
|
ref="options"
|
||||||
|
trigger="manual"
|
||||||
|
interactive
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => tippyActions!.focus()"
|
||||||
|
>
|
||||||
|
<span class="leading-8 px-2 truncate">
|
||||||
|
{{ tab.document.request.name }}
|
||||||
|
</span>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="tippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.r="renameAction?.$el.click()"
|
||||||
|
@keyup.d="duplicateAction?.$el.click()"
|
||||||
|
@keyup.w="closeAction?.$el.click()"
|
||||||
|
@keyup.x="closeOthersAction?.$el.click()"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="renameAction"
|
||||||
|
:icon="IconFileEdit"
|
||||||
|
:label="t('request.rename')"
|
||||||
|
:shortcut="['R']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('open-rename-modal')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="duplicateAction"
|
||||||
|
:icon="IconCopy"
|
||||||
|
:label="t('tab.duplicate')"
|
||||||
|
:shortcut="['D']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('duplicate-tab')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="isRemovable"
|
||||||
|
ref="closeAction"
|
||||||
|
:icon="IconXCircle"
|
||||||
|
:label="t('tab.close')"
|
||||||
|
:shortcut="['W']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('close-tab')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="isRemovable"
|
||||||
|
ref="closeOthersAction"
|
||||||
|
:icon="IconXSquare"
|
||||||
|
:label="t('tab.close_others')"
|
||||||
|
:shortcut="['X']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('close-other-tabs')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { TippyComponent } from "vue-tippy"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
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"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tab: HoppGQLTab
|
||||||
|
isRemovable: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "open-rename-modal"): void
|
||||||
|
(event: "close-tab"): void
|
||||||
|
(event: "close-other-tabs"): void
|
||||||
|
(event: "duplicate-tab"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
|
const options = ref<TippyComponent | null>(null)
|
||||||
|
|
||||||
|
const renameAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const closeAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const closeOthersAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const duplicateAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
|
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
|
||||||
>
|
>
|
||||||
<label class="font-semibold text-secondaryLight">
|
<label class="font-semibold text-secondaryLight">
|
||||||
{{ t("request.variables") }}
|
{{ t("request.variables") }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
:title="tab.document.request.name"
|
:title="tab.document.request.name"
|
||||||
class="truncate px-2 flex items-center"
|
class="truncate px-2 flex items-center"
|
||||||
@dblclick="emit('open-rename-modal')"
|
@dblclick="emit('open-rename-modal')"
|
||||||
@contextmenu.prevent="options?.tippy.show()"
|
@contextmenu.prevent="options?.tippy?.show()"
|
||||||
@click.middle="emit('close-tab')"
|
@click.middle="emit('close-tab')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { BehaviorSubject } from "rxjs"
|
|||||||
import { HoppRESTDocument } from "./rest/document"
|
import { HoppRESTDocument } from "./rest/document"
|
||||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||||
|
import { HoppGQLSaveContext } from "./graphql/document"
|
||||||
|
|
||||||
export type HoppAction =
|
export type HoppAction =
|
||||||
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
||||||
@@ -109,6 +110,7 @@ type HoppActionArgsMap = {
|
|||||||
|
|
||||||
"gql.request.open": {
|
"gql.request.open": {
|
||||||
request: HoppGQLRequest
|
request: HoppGQLRequest
|
||||||
|
saveContext?: HoppGQLSaveContext
|
||||||
}
|
}
|
||||||
"modals.environment.add": {
|
"modals.environment.add": {
|
||||||
envName: string
|
envName: string
|
||||||
|
|||||||
@@ -197,3 +197,30 @@ export function getTabsRefTo(func: (tab: HoppGQLTab) => boolean) {
|
|||||||
.filter(func)
|
.filter(func)
|
||||||
.map((tab) => getTabRef(tab.id))
|
.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
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,15 +21,14 @@
|
|||||||
:close-visibility="'hover'"
|
:close-visibility="'hover'"
|
||||||
>
|
>
|
||||||
<template #tabhead>
|
<template #tabhead>
|
||||||
<div
|
<GraphqlTabHead
|
||||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
:tab="tab"
|
||||||
:title="tab.document.request.name"
|
:is-removable="tabs.length > 1"
|
||||||
class="truncate px-2"
|
@open-rename-modal="openReqRenameModal(tab)"
|
||||||
>
|
@close-tab="removeTab(tab.id)"
|
||||||
<span class="leading-8 px-2">
|
@close-other-tabs="closeOtherTabsAction(tab.id)"
|
||||||
{{ tab.document.request.name }}
|
@duplicate-tab="duplicateTab(tab)"
|
||||||
</span>
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
@@ -59,7 +58,12 @@
|
|||||||
<GraphqlSidebar />
|
<GraphqlSidebar />
|
||||||
</template>
|
</template>
|
||||||
</AppPaneLayout>
|
</AppPaneLayout>
|
||||||
|
<CollectionsEditRequest
|
||||||
|
v-model="editReqModalReqName"
|
||||||
|
:show="showRenamingReqNameModalForTabID !== undefined"
|
||||||
|
@submit="renameReqName"
|
||||||
|
@hide-modal="showRenamingReqNameModalForTabID = undefined"
|
||||||
|
/>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmingCloseForTabID !== null"
|
:show="confirmingCloseForTabID !== null"
|
||||||
:confirm="t('modal.close_unsaved_tab')"
|
:confirm="t('modal.close_unsaved_tab')"
|
||||||
@@ -67,6 +71,13 @@
|
|||||||
@hide-modal="onCloseConfirm"
|
@hide-modal="onCloseConfirm"
|
||||||
@resolve="onResolveConfirm"
|
@resolve="onResolveConfirm"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="confirmingCloseAllTabs"
|
||||||
|
:confirm="t('modal.close_unsaved_tab')"
|
||||||
|
:title="t('confirm.close_unsaved_tabs', { count: unsavedTabsCount })"
|
||||||
|
@hide-modal="confirmingCloseAllTabs = false"
|
||||||
|
@resolve="onResolveConfirmCloseAllTabs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,10 +91,12 @@ import { connection, disconnect } from "~/helpers/graphql/connection"
|
|||||||
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
|
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
|
||||||
import {
|
import {
|
||||||
HoppGQLTab,
|
HoppGQLTab,
|
||||||
|
closeOtherTabs,
|
||||||
closeTab,
|
closeTab,
|
||||||
createNewTab,
|
createNewTab,
|
||||||
currentTabID,
|
currentTabID,
|
||||||
getActiveTabs,
|
getActiveTabs,
|
||||||
|
getDirtyTabsCount,
|
||||||
getTabRef,
|
getTabRef,
|
||||||
updateTab,
|
updateTab,
|
||||||
updateTabOrdering,
|
updateTabOrdering,
|
||||||
@@ -142,6 +155,27 @@ const onResolveConfirm = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmingCloseAllTabs = ref(false)
|
||||||
|
const unsavedTabsCount = ref(0)
|
||||||
|
const exceptedTabID = ref<string | null>(null)
|
||||||
|
|
||||||
|
const closeOtherTabsAction = (tabID: string) => {
|
||||||
|
const dirtyTabCount = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResolveConfirmCloseAllTabs = () => {
|
||||||
|
if (exceptedTabID.value) closeOtherTabs(exceptedTabID.value)
|
||||||
|
confirmingCloseAllTabs.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const onTabUpdate = (tab: HoppGQLTab) => {
|
const onTabUpdate = (tab: HoppGQLTab) => {
|
||||||
updateTab(tab)
|
updateTab(tab)
|
||||||
}
|
}
|
||||||
@@ -152,8 +186,34 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("gql.request.open", ({ request }) => {
|
const editReqModalReqName = ref("")
|
||||||
|
const showRenamingReqNameModalForTabID = ref<string>()
|
||||||
|
|
||||||
|
const openReqRenameModal = (tab: HoppGQLTab) => {
|
||||||
|
editReqModalReqName.value = tab.document.request.name
|
||||||
|
showRenamingReqNameModalForTabID.value = tab.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameReqName = () => {
|
||||||
|
const tab = getTabRef(showRenamingReqNameModalForTabID.value!)
|
||||||
|
if (tab.value) {
|
||||||
|
tab.value.document.request.name = editReqModalReqName.value
|
||||||
|
updateTab(tab.value)
|
||||||
|
}
|
||||||
|
showRenamingReqNameModalForTabID.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateTab = (tab: HoppGQLTab) => {
|
||||||
|
const newTab = createNewTab({
|
||||||
|
request: tab.document.request,
|
||||||
|
isDirty: true,
|
||||||
|
})
|
||||||
|
currentTabID.value = newTab.id
|
||||||
|
}
|
||||||
|
|
||||||
|
defineActionHandler("gql.request.open", ({ request, saveContext }) => {
|
||||||
createNewTab({
|
createNewTab({
|
||||||
|
saveContext,
|
||||||
request: request,
|
request: request,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -304,13 +304,15 @@ export class CollectionsSpotlightSearcherService
|
|||||||
|
|
||||||
if (!req) return
|
if (!req) return
|
||||||
|
|
||||||
createNewGQLTab(
|
createNewGQLTab({
|
||||||
{
|
saveContext: {
|
||||||
request: req,
|
originLocation: "user-collection",
|
||||||
isDirty: false,
|
folderPath: folderPath.join("/"),
|
||||||
|
requestIndex: reqIndex,
|
||||||
},
|
},
|
||||||
true
|
request: req,
|
||||||
)
|
isDirty: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user