feat: tippy menu for history and tab (#3220)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
"open_workspace": "Open workspace",
|
"open_workspace": "Open workspace",
|
||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"prettify": "Prettify",
|
"prettify": "Prettify",
|
||||||
|
"rename": "Rename",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -132,6 +133,7 @@
|
|||||||
"renamed": "Collection renamed",
|
"renamed": "Collection renamed",
|
||||||
"request_in_use": "Request in use",
|
"request_in_use": "Request in use",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
|
"save_to_collection": "Save to Collection",
|
||||||
"select": "Select a Collection",
|
"select": "Select a Collection",
|
||||||
"select_location": "Select location",
|
"select_location": "Select location",
|
||||||
"select_team": "Select a team",
|
"select_team": "Select a team",
|
||||||
@@ -149,6 +151,7 @@
|
|||||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||||
|
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||||
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
||||||
},
|
},
|
||||||
"context_menu": {
|
"context_menu": {
|
||||||
@@ -432,6 +435,7 @@
|
|||||||
"payload": "Payload",
|
"payload": "Payload",
|
||||||
"query": "Query",
|
"query": "Query",
|
||||||
"raw_body": "Raw Request Body",
|
"raw_body": "Raw Request Body",
|
||||||
|
"rename": "Rename Request",
|
||||||
"renamed": "Request renamed",
|
"renamed": "Request renamed",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -658,8 +662,11 @@
|
|||||||
"tab": {
|
"tab": {
|
||||||
"authorization": "Authorization",
|
"authorization": "Authorization",
|
||||||
"body": "Body",
|
"body": "Body",
|
||||||
|
"close": "Close Tab",
|
||||||
|
"close_others": "Close other Tabs",
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
|
"duplicate": "Duplicate Tab",
|
||||||
"environments": "Environments",
|
"environments": "Environments",
|
||||||
"headers": "Headers",
|
"headers": "Headers",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
|
|||||||
41
packages/hoppscotch-common/src/components.d.ts
vendored
41
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -7,7 +7,6 @@ export {}
|
|||||||
|
|
||||||
declare module "@vue/runtime-core" {
|
declare module "@vue/runtime-core" {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
<<<<<<< HEAD
|
|
||||||
AppActionHandler: typeof import("./components/app/ActionHandler.vue")["default"]
|
AppActionHandler: typeof import("./components/app/ActionHandler.vue")["default"]
|
||||||
AppAnnouncement: typeof import("./components/app/Announcement.vue")["default"]
|
AppAnnouncement: typeof import("./components/app/Announcement.vue")["default"]
|
||||||
AppDeveloperOptions: typeof import("./components/app/DeveloperOptions.vue")["default"]
|
AppDeveloperOptions: typeof import("./components/app/DeveloperOptions.vue")["default"]
|
||||||
@@ -200,7 +199,6 @@ declare module "@vue/runtime-core" {
|
|||||||
Tippy: typeof import("vue-tippy")["Tippy"]
|
Tippy: typeof import("vue-tippy")["Tippy"]
|
||||||
WorkspaceCurrent: typeof import("./components/workspace/Current.vue")["default"]
|
WorkspaceCurrent: typeof import("./components/workspace/Current.vue")["default"]
|
||||||
WorkspaceSelector: typeof import("./components/workspace/Selector.vue")["default"]
|
WorkspaceSelector: typeof import("./components/workspace/Selector.vue")["default"]
|
||||||
=======
|
|
||||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||||
@@ -268,6 +266,29 @@ declare module "@vue/runtime-core" {
|
|||||||
History: typeof import('./components/history/index.vue')['default']
|
History: typeof import('./components/history/index.vue')['default']
|
||||||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||||
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||||
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||||
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||||
|
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||||
|
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||||
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
|
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||||
|
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||||
|
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
|
||||||
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
|
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||||
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||||
|
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||||
|
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
||||||
|
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||||
|
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||||
|
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||||
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
|
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||||
|
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
||||||
|
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||||
|
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||||
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||||
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
||||||
@@ -287,12 +308,27 @@ declare module "@vue/runtime-core" {
|
|||||||
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
HttpResponse: typeof import('./components/http/Response.vue')['default']
|
||||||
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
|
||||||
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
|
||||||
|
HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
|
||||||
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
|
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
|
||||||
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
|
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
|
||||||
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
|
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
|
||||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||||
|
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||||
|
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||||
|
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||||
|
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
|
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||||
|
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
||||||
|
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||||
|
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
||||||
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
|
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||||
@@ -352,6 +388,5 @@ declare module "@vue/runtime-core" {
|
|||||||
Tippy: typeof import('vue-tippy')['Tippy']
|
Tippy: typeof import('vue-tippy')['Tippy']
|
||||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||||
>>>>>>> upstream/main
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, reactive, ref, watch } from "vue"
|
import { computed, nextTick, reactive, ref, watch } from "vue"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import {
|
import {
|
||||||
HoppGQLRequest,
|
HoppGQLRequest,
|
||||||
@@ -101,10 +101,12 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
mode: "rest" | "graphql"
|
mode: "rest" | "graphql"
|
||||||
|
request?: HoppRESTRequest | HoppGQLRequest | null
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
show: false,
|
show: false,
|
||||||
mode: "rest",
|
mode: "rest",
|
||||||
|
request: null,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,9 +128,17 @@ const restRequestName = computedWithControl(
|
|||||||
() => currentActiveTab.value.document.request.name
|
() => currentActiveTab.value.document.request.name
|
||||||
)
|
)
|
||||||
|
|
||||||
const requestName = ref(
|
const reqName = computed(() => {
|
||||||
props.mode === "rest" ? restRequestName.value : gqlRequestName.value
|
if (props.request) {
|
||||||
)
|
return props.request.name
|
||||||
|
} else if (props.mode === "rest") {
|
||||||
|
return restRequestName.value
|
||||||
|
} else {
|
||||||
|
return gqlRequestName.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const requestName = ref(reqName.value)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [currentActiveTab.value, gqlRequestName.value],
|
() => [currentActiveTab.value, gqlRequestName.value],
|
||||||
@@ -192,10 +202,15 @@ const saveRequestAs = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestUpdated =
|
let requestUpdated
|
||||||
props.mode === "rest"
|
|
||||||
? cloneDeep(currentActiveTab.value.document.request)
|
if (props.request) {
|
||||||
: cloneDeep(getGQLSession().request)
|
requestUpdated = cloneDeep(props.request)
|
||||||
|
} else if (props.mode === "rest") {
|
||||||
|
requestUpdated = cloneDeep(currentActiveTab.value.document.request)
|
||||||
|
} else {
|
||||||
|
requestUpdated = cloneDeep(getGQLSession().request)
|
||||||
|
}
|
||||||
|
|
||||||
requestUpdated.name = requestName.value
|
requestUpdated.name = requestName.value
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
@toggle-star="toggleStar(entry.entry)"
|
@toggle-star="toggleStar(entry.entry)"
|
||||||
@delete-entry="deleteHistory(entry.entry)"
|
@delete-entry="deleteHistory(entry.entry)"
|
||||||
@use-entry="useHistory(toRaw(entry.entry))"
|
@use-entry="useHistory(toRaw(entry.entry))"
|
||||||
|
@add-to-collection="addToCollection(entry.entry)"
|
||||||
/>
|
/>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,7 +177,7 @@ import {
|
|||||||
import HistoryRestCard from "./rest/Card.vue"
|
import HistoryRestCard from "./rest/Card.vue"
|
||||||
import HistoryGraphqlCard from "./graphql/Card.vue"
|
import HistoryGraphqlCard from "./graphql/Card.vue"
|
||||||
import { createNewTab } from "~/helpers/rest/tab"
|
import { createNewTab } from "~/helpers/rest/tab"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
|
|
||||||
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
||||||
|
|
||||||
@@ -324,6 +325,14 @@ const deleteHistory = (entry: HistoryEntry) => {
|
|||||||
toast.success(`${t("state.deleted")}`)
|
toast.success(`${t("state.deleted")}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addToCollection = (entry: HistoryEntry) => {
|
||||||
|
if (props.page === "rest") {
|
||||||
|
invokeAction("request.save-as", {
|
||||||
|
request: entry.request,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toggleStar = (entry: HistoryEntry) => {
|
const toggleStar = (entry: HistoryEntry) => {
|
||||||
// History entry type specified because function does not know the type
|
// History entry type specified because function does not know the type
|
||||||
if (props.page === "rest")
|
if (props.page === "rest")
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-stretch group">
|
<div
|
||||||
|
class="flex items-stretch group"
|
||||||
|
@contextmenu.prevent="options!.tippy.show()"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||||
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
||||||
@@ -26,6 +29,39 @@
|
|||||||
{{ entry.request.endpoint }}
|
{{ entry.request.endpoint }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
<tippy
|
||||||
|
ref="options"
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => tippyActions!.focus()"
|
||||||
|
>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="tippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
role="menu"
|
||||||
|
@keyup.s="addToCollectionAction?.$el.click()"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="addToCollectionAction"
|
||||||
|
:icon="IconSave"
|
||||||
|
:label="`${t('collection.save_to_collection')}`"
|
||||||
|
:shortcut="['S']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('add-to-collection')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:icon="IconTrash"
|
:icon="IconTrash"
|
||||||
@@ -48,15 +84,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import findStatusGroup from "~/helpers/findStatusGroup"
|
import findStatusGroup from "~/helpers/findStatusGroup"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { RESTHistoryEntry } from "~/newstore/history"
|
import { RESTHistoryEntry } from "~/newstore/history"
|
||||||
import { shortDateTime } from "~/helpers/utils/date"
|
import { shortDateTime } from "~/helpers/utils/date"
|
||||||
|
import IconSave from "~icons/lucide/save"
|
||||||
import IconStar from "~icons/lucide/star"
|
import IconStar from "~icons/lucide/star"
|
||||||
import IconStarOff from "~icons/hopp/star-off"
|
import IconStarOff from "~icons/hopp/star-off"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
|
import { TippyComponent } from "vue-tippy"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
entry: RESTHistoryEntry
|
entry: RESTHistoryEntry
|
||||||
@@ -67,8 +104,13 @@ const emit = defineEmits<{
|
|||||||
(e: "use-entry"): void
|
(e: "use-entry"): void
|
||||||
(e: "delete-entry"): void
|
(e: "delete-entry"): void
|
||||||
(e: "toggle-star"): void
|
(e: "toggle-star"): void
|
||||||
|
(e: "add-to-collection"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
|
const options = ref<TippyComponent | null>(null)
|
||||||
|
const addToCollectionAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const duration = computed(() => {
|
const duration = computed(() => {
|
||||||
|
|||||||
@@ -221,6 +221,7 @@
|
|||||||
v-if="showSaveRequestModal"
|
v-if="showSaveRequestModal"
|
||||||
mode="rest"
|
mode="rest"
|
||||||
:show="showSaveRequestModal"
|
:show="showSaveRequestModal"
|
||||||
|
:request="request"
|
||||||
@hide-modal="showSaveRequestModal = false"
|
@hide-modal="showSaveRequestModal = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,6 +264,7 @@ import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
|||||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { getCurrentStrategyID } from "~/helpers/network"
|
import { getCurrentStrategyID } from "~/helpers/network"
|
||||||
|
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -578,6 +580,8 @@ const saveRequest = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const request = ref<HoppRESTRequest | null>(null)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (loading.value) cancelRequest()
|
if (loading.value) cancelRequest()
|
||||||
})
|
})
|
||||||
@@ -593,7 +597,22 @@ defineActionHandler("request.method.prev", cycleUpMethod)
|
|||||||
defineActionHandler("request.save", saveRequest)
|
defineActionHandler("request.save", saveRequest)
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"request.save-as",
|
"request.save-as",
|
||||||
() => (showSaveRequestModal.value = true)
|
(
|
||||||
|
req:
|
||||||
|
| {
|
||||||
|
requestType: "rest"
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
requestType: "gql"
|
||||||
|
request: HoppGQLRequest
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
showSaveRequestModal.value = true
|
||||||
|
if (req && req.requestType === "rest") {
|
||||||
|
request.value = req.request
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
||||||
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
||||||
|
|||||||
126
packages/hoppscotch-common/src/components/http/TabHead.vue
Normal file
126
packages/hoppscotch-common/src/components/http/TabHead.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<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')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="font-semibold text-tiny"
|
||||||
|
:class="getMethodLabelColorClassOf(tab.document.request)"
|
||||||
|
>
|
||||||
|
{{ tab.document.request.method }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<tippy
|
||||||
|
ref="options"
|
||||||
|
trigger="manual"
|
||||||
|
interactive
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => tippyActions!.focus()"
|
||||||
|
>
|
||||||
|
<span class="leading-8 px-2">
|
||||||
|
{{ 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 { 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"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tab: HoppRESTTab
|
||||||
|
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>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
|
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
|
||||||
import { BehaviorSubject } from "rxjs"
|
import { BehaviorSubject } from "rxjs"
|
||||||
import { HoppRESTDocument } from "./rest/document"
|
import { HoppRESTDocument } from "./rest/document"
|
||||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
|
||||||
export type HoppAction =
|
export type HoppAction =
|
||||||
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
||||||
@@ -76,6 +76,15 @@ type HoppActionArgsMap = {
|
|||||||
"rest.request.open": {
|
"rest.request.open": {
|
||||||
doc: HoppRESTDocument
|
doc: HoppRESTDocument
|
||||||
}
|
}
|
||||||
|
"request.save-as":
|
||||||
|
| {
|
||||||
|
requestType: "rest"
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
requestType: "gql"
|
||||||
|
request: HoppGQLRequest
|
||||||
|
}
|
||||||
"gql.request.open": {
|
"gql.request.open": {
|
||||||
request: HoppGQLRequest
|
request: HoppGQLRequest
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,33 @@ export function closeTab(tabID: string) {
|
|||||||
tabMap.delete(tabID)
|
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) {
|
export function getTabRefWithSaveContext(ctx: HoppRESTSaveContext) {
|
||||||
for (const tab of tabMap.values()) {
|
for (const tab of tabMap.values()) {
|
||||||
// For `team-collection` request id can be considered unique
|
// For `team-collection` request id can be considered unique
|
||||||
|
|||||||
@@ -19,22 +19,14 @@
|
|||||||
:close-visibility="'hover'"
|
:close-visibility="'hover'"
|
||||||
>
|
>
|
||||||
<template #tabhead>
|
<template #tabhead>
|
||||||
<div
|
<HttpTabHead
|
||||||
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.id)"
|
||||||
@dblclick="openReqRenameModal()"
|
@close-tab="removeTab(tab.id)"
|
||||||
>
|
@close-other-tabs="closeOtherTabsAction(tab.id)"
|
||||||
<span
|
@duplicate-tab="duplicateTab(tab.id)"
|
||||||
class="font-semibold text-tiny"
|
/>
|
||||||
:class="getMethodLabelColorClassOf(tab.document.request)"
|
|
||||||
>
|
|
||||||
{{ tab.document.request.method }}
|
|
||||||
</span>
|
|
||||||
<span class="leading-8 px-2">
|
|
||||||
{{ tab.document.request.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<span
|
<span
|
||||||
@@ -78,6 +70,13 @@
|
|||||||
@hide-modal="onCloseConfirmSaveTab"
|
@hide-modal="onCloseConfirmSaveTab"
|
||||||
@resolve="onResolveConfirmSaveTab"
|
@resolve="onResolveConfirmSaveTab"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="confirmingCloseAllTabs"
|
||||||
|
:confirm="t('modal.close_unsaved_tab')"
|
||||||
|
:title="t('confirm.close_unsaved_tabs', { count: unsavedTabsCount })"
|
||||||
|
@hide-modal="confirmingCloseAllTabs = false"
|
||||||
|
@resolve="onResolveConfirmCloseAllTabs"
|
||||||
|
/>
|
||||||
<CollectionsSaveRequest
|
<CollectionsSaveRequest
|
||||||
v-if="savingRequest"
|
v-if="savingRequest"
|
||||||
mode="rest"
|
mode="rest"
|
||||||
@@ -99,10 +98,10 @@ import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
|
|||||||
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
||||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import {
|
import {
|
||||||
closeTab,
|
closeTab,
|
||||||
|
closeOtherTabs,
|
||||||
createNewTab,
|
createNewTab,
|
||||||
currentActiveTab,
|
currentActiveTab,
|
||||||
currentTabID,
|
currentTabID,
|
||||||
@@ -113,6 +112,7 @@ import {
|
|||||||
persistableTabState,
|
persistableTabState,
|
||||||
updateTab,
|
updateTab,
|
||||||
updateTabOrdering,
|
updateTabOrdering,
|
||||||
|
getDirtyTabsCount,
|
||||||
} from "~/helpers/rest/tab"
|
} from "~/helpers/rest/tab"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
@@ -139,8 +139,11 @@ import {
|
|||||||
|
|
||||||
const savingRequest = ref(false)
|
const savingRequest = ref(false)
|
||||||
const confirmingCloseForTabID = ref<string | null>(null)
|
const confirmingCloseForTabID = ref<string | null>(null)
|
||||||
|
const confirmingCloseAllTabs = ref(false)
|
||||||
const showRenamingReqNameModal = ref(false)
|
const showRenamingReqNameModal = ref(false)
|
||||||
const reqName = ref<string>("")
|
const reqName = ref<string>("")
|
||||||
|
const unsavedTabsCount = ref(0)
|
||||||
|
const exceptedTabID = ref<string | null>(null)
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -215,9 +218,42 @@ const removeTab = (tabID: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openReqRenameModal = () => {
|
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 duplicateTab = (tabID: string) => {
|
||||||
|
const tab = getTabRef(tabID)
|
||||||
|
if (tab.value) {
|
||||||
|
const newTab = createNewTab({
|
||||||
|
request: tab.value.document.request,
|
||||||
|
isDirty: true,
|
||||||
|
})
|
||||||
|
currentTabID.value = newTab.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResolveConfirmCloseAllTabs = () => {
|
||||||
|
if (exceptedTabID.value) closeOtherTabs(exceptedTabID.value)
|
||||||
|
confirmingCloseAllTabs.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const openReqRenameModal = (tabID?: string) => {
|
||||||
|
if (tabID) {
|
||||||
|
const tab = getTabRef(tabID)
|
||||||
|
reqName.value = tab.value.document.request.name
|
||||||
|
} else {
|
||||||
|
reqName.value = currentActiveTab.value.document.request.name
|
||||||
|
}
|
||||||
showRenamingReqNameModal.value = true
|
showRenamingReqNameModal.value = true
|
||||||
reqName.value = currentActiveTab.value.document.request.name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renameReqName = () => {
|
const renameReqName = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user