diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index cb30e4ebe..3b38789b1 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -31,6 +31,7 @@ "open_workspace": "Open workspace", "paste": "Paste", "prettify": "Prettify", + "rename": "Rename", "remove": "Remove", "restore": "Restore", "save": "Save", @@ -132,6 +133,7 @@ "renamed": "Collection renamed", "request_in_use": "Request in use", "save_as": "Save as", + "save_to_collection": "Save to Collection", "select": "Select a Collection", "select_location": "Select location", "select_team": "Select a team", @@ -149,6 +151,7 @@ "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.", "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." }, "context_menu": { @@ -432,6 +435,7 @@ "payload": "Payload", "query": "Query", "raw_body": "Raw Request Body", + "rename": "Rename Request", "renamed": "Request renamed", "run": "Run", "save": "Save", @@ -658,8 +662,11 @@ "tab": { "authorization": "Authorization", "body": "Body", + "close": "Close Tab", + "close_others": "Close other Tabs", "collections": "Collections", "documentation": "Documentation", + "duplicate": "Duplicate Tab", "environments": "Environments", "headers": "Headers", "history": "History", diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index 7ceddf987..d8037387c 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -7,7 +7,6 @@ export {} declare module "@vue/runtime-core" { export interface GlobalComponents { -<<<<<<< HEAD AppActionHandler: typeof import("./components/app/ActionHandler.vue")["default"] AppAnnouncement: typeof import("./components/app/Announcement.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"] WorkspaceCurrent: typeof import("./components/workspace/Current.vue")["default"] WorkspaceSelector: typeof import("./components/workspace/Selector.vue")["default"] -======= AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default'] AppAnnouncement: typeof import('./components/app/Announcement.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'] HistoryGraphqlCard: typeof import('./components/history/graphql/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'] HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.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'] HttpResponseMeta: typeof import('./components/http/ResponseMeta.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'] HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default'] HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default'] HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default'] HttpTests: typeof import('./components/http/Tests.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'] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.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'] WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default'] WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default'] ->>>>>>> upstream/main } } diff --git a/packages/hoppscotch-common/src/components/collections/SaveRequest.vue b/packages/hoppscotch-common/src/components/collections/SaveRequest.vue index 4484c93c6..5dd260d95 100644 --- a/packages/hoppscotch-common/src/components/collections/SaveRequest.vue +++ b/packages/hoppscotch-common/src/components/collections/SaveRequest.vue @@ -56,7 +56,7 @@ diff --git a/packages/hoppscotch-common/src/helpers/actions.ts b/packages/hoppscotch-common/src/helpers/actions.ts index 00ec882e4..2c85347fc 100644 --- a/packages/hoppscotch-common/src/helpers/actions.ts +++ b/packages/hoppscotch-common/src/helpers/actions.ts @@ -5,7 +5,7 @@ import { Ref, onBeforeUnmount, onMounted, watch } from "vue" import { BehaviorSubject } from "rxjs" import { HoppRESTDocument } from "./rest/document" -import { HoppGQLRequest } from "@hoppscotch/data" +import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data" export type HoppAction = | "contextmenu.open" // Send/Cancel a Hoppscotch Request @@ -76,6 +76,15 @@ type HoppActionArgsMap = { "rest.request.open": { doc: HoppRESTDocument } + "request.save-as": + | { + requestType: "rest" + request: HoppRESTRequest + } + | { + requestType: "gql" + request: HoppGQLRequest + } "gql.request.open": { request: HoppGQLRequest } diff --git a/packages/hoppscotch-common/src/helpers/rest/tab.ts b/packages/hoppscotch-common/src/helpers/rest/tab.ts index 734dd0cbd..612a6e93e 100644 --- a/packages/hoppscotch-common/src/helpers/rest/tab.ts +++ b/packages/hoppscotch-common/src/helpers/rest/tab.ts @@ -181,6 +181,33 @@ export function closeTab(tabID: string) { 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 diff --git a/packages/hoppscotch-common/src/pages/index.vue b/packages/hoppscotch-common/src/pages/index.vue index dcb8cfd35..b1d2fac74 100644 --- a/packages/hoppscotch-common/src/pages/index.vue +++ b/packages/hoppscotch-common/src/pages/index.vue @@ -19,22 +19,14 @@ :close-visibility="'hover'" > - - - {{ tab.document.request.method }} - - - {{ tab.document.request.name }} - - + + (null) +const confirmingCloseAllTabs = ref(false) const showRenamingReqNameModal = ref(false) const reqName = ref("") +const unsavedTabsCount = ref(0) +const exceptedTabID = ref(null) const t = useI18n() 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 - reqName.value = currentActiveTab.value.document.request.name } const renameReqName = () => {