feat: cleaner save context handling for graphql (#3282)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
@click="
|
||||
emit('add-request', {
|
||||
path: `${collectionIndex}`,
|
||||
index: collection.requests.length,
|
||||
})
|
||||
"
|
||||
/>
|
||||
@@ -219,6 +220,7 @@ import {
|
||||
moveGraphqlRequest,
|
||||
} from "~/newstore/collections"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
||||
|
||||
const props = defineProps({
|
||||
picked: { type: Object, default: null },
|
||||
@@ -293,6 +295,22 @@ const removeCollection = () => {
|
||||
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)
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,12 @@
|
||||
:icon="IconFilePlus"
|
||||
:title="t('request.new')"
|
||||
class="hidden group-hover:inline-flex"
|
||||
@click="emit('add-request', { path: folderPath })"
|
||||
@click="
|
||||
emit('add-request', {
|
||||
path: folderPath,
|
||||
index: folder.requests.length,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -198,6 +203,7 @@ 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"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -249,10 +255,8 @@ const collectionIcon = computed(() => {
|
||||
|
||||
const pick = () => {
|
||||
emit("select", {
|
||||
picked: {
|
||||
pickedType: "gql-my-folder",
|
||||
folderPath: props.folderPath,
|
||||
},
|
||||
pickedType: "gql-my-folder",
|
||||
folderPath: props.folderPath,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -273,6 +277,22 @@ const removeFolder = () => {
|
||||
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)
|
||||
toast.success(t("state.deleted"))
|
||||
}
|
||||
|
||||
@@ -20,22 +20,28 @@
|
||||
/>
|
||||
</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()"
|
||||
>
|
||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||
{{ request.name }}
|
||||
</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>
|
||||
<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>
|
||||
<tippy
|
||||
ref="options"
|
||||
@@ -121,7 +127,6 @@
|
||||
<script setup lang="ts">
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconFile from "~icons/lucide/file"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
@@ -132,7 +137,12 @@ import { useToast } from "@composables/toast"
|
||||
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { removeGraphqlRequest } from "~/newstore/collections"
|
||||
import { createNewTab } from "~/helpers/graphql/tab"
|
||||
import {
|
||||
createNewTab,
|
||||
getTabRefWithSaveContext,
|
||||
currentTabID,
|
||||
currentActiveTab,
|
||||
} from "~/helpers/graphql/tab"
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
@@ -154,6 +164,18 @@ const props = defineProps({
|
||||
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
|
||||
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
|
||||
|
||||
@@ -179,7 +201,24 @@ const selectRequest = () => {
|
||||
if (props.saveRequest) {
|
||||
pick()
|
||||
} 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({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: props.folderPath,
|
||||
requestIndex: props.requestIndex,
|
||||
},
|
||||
request: cloneDeep(
|
||||
makeGQLRequest({
|
||||
name: props.request.name,
|
||||
@@ -213,6 +252,18 @@ const removeRequest = () => {
|
||||
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)
|
||||
toast.success(`${t("state.deleted")}`)
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ export default defineComponent({
|
||||
this.$data.editingCollectionIndex = collectionIndex
|
||||
this.displayModalEdit(true)
|
||||
},
|
||||
onAddRequest({ name, path }) {
|
||||
onAddRequest({ name, path, index }) {
|
||||
const newRequest = {
|
||||
...currentActiveTab.value.document.request,
|
||||
name,
|
||||
@@ -274,6 +274,11 @@ export default defineComponent({
|
||||
saveGraphqlRequestAs(path, newRequest)
|
||||
|
||||
createNewTab({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
requestIndex: index,
|
||||
},
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<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">
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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">
|
||||
{{ t("tab.headers") }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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">
|
||||
{{ t("request.query") }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex flex-col flex-1 h-full">
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky bg-primary z-10"
|
||||
styles="sticky top-0 bg-primary z-10 border-b-0"
|
||||
:render-inactive-tabs="true"
|
||||
>
|
||||
<HoppSmartTab
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { editGraphqlRequest } from "~/newstore/collections"
|
||||
|
||||
type OptionTabs = "query" | "headers" | "variables" | "authorization"
|
||||
const selectedOptionTab = ref<OptionTabs>("query")
|
||||
@@ -180,12 +181,29 @@ const hideRequestModal = () => {
|
||||
showSaveRequestModal.value = false
|
||||
}
|
||||
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 = () => {
|
||||
request.value.query = ""
|
||||
}
|
||||
defineActionHandler("request.send-cancel", runQuery)
|
||||
defineActionHandler("request.save", saveRequest)
|
||||
defineActionHandler("request.save-as", () => {
|
||||
showSaveRequestModal.value = true
|
||||
})
|
||||
defineActionHandler("request.reset", clearGQLQuery)
|
||||
</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>
|
||||
<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">
|
||||
{{ t("request.variables") }}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:title="tab.document.request.name"
|
||||
class="truncate px-2 flex items-center"
|
||||
@dblclick="emit('open-rename-modal')"
|
||||
@contextmenu.prevent="options?.tippy.show()"
|
||||
@contextmenu.prevent="options?.tippy?.show()"
|
||||
@click.middle="emit('close-tab')"
|
||||
>
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user