|
@@ -13,7 +13,7 @@
|
|
|
<HoppSmartWindow
|
|
|
v-for="tab in activeTabs"
|
|
|
:id="tab.id"
|
|
|
- :key="`${tab.id}-${tab.document.isDirty}`"
|
|
|
+ :key="tab.id"
|
|
|
:label="tab.document.request.name"
|
|
|
:is-removable="activeTabs.length > 1"
|
|
|
:close-visibility="'hover'"
|
|
@@ -26,12 +26,11 @@
|
|
|
@close-tab="removeTab(tab.id)"
|
|
|
@close-other-tabs="closeOtherTabsAction(tab.id)"
|
|
|
@duplicate-tab="duplicateTab(tab.id)"
|
|
|
- @share-tab-request="shareTabRequest(tab.id)"
|
|
|
/>
|
|
|
</template>
|
|
|
<template #suffix>
|
|
|
<span
|
|
|
- v-if="tab.document.isDirty"
|
|
|
+ v-if="getTabDirtyStatus(tab)"
|
|
|
class="flex w-4 items-center justify-center text-secondary group-hover:hidden"
|
|
|
>
|
|
|
<svg
|
|
@@ -64,6 +63,13 @@
|
|
|
@submit="renameReqName"
|
|
|
@hide-modal="showRenamingReqNameModal = false"
|
|
|
/>
|
|
|
+ <HoppSmartConfirmModal
|
|
|
+ :show="confirmingCloseForTabID !== null"
|
|
|
+ :confirm="t('modal.close_unsaved_tab')"
|
|
|
+ :title="t('confirm.save_unsaved_tab')"
|
|
|
+ @hide-modal="onCloseConfirmSaveTab"
|
|
|
+ @resolve="onResolveConfirmSaveTab"
|
|
|
+ />
|
|
|
<HoppSmartConfirmModal
|
|
|
:show="confirmingCloseAllTabs"
|
|
|
:confirm="t('modal.close_unsaved_tab')"
|
|
@@ -71,36 +77,6 @@
|
|
|
@hide-modal="confirmingCloseAllTabs = false"
|
|
|
@resolve="onResolveConfirmCloseAllTabs"
|
|
|
/>
|
|
|
- <HoppSmartModal
|
|
|
- v-if="confirmingCloseForTabID !== null"
|
|
|
- dialog
|
|
|
- role="dialog"
|
|
|
- aria-modal="true"
|
|
|
- :title="t('modal.close_unsaved_tab')"
|
|
|
- @close="confirmingCloseForTabID = null"
|
|
|
- >
|
|
|
- <template #body>
|
|
|
- <div class="text-center">
|
|
|
- {{ t("confirm.save_unsaved_tab") }}
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <template #footer>
|
|
|
- <span class="flex space-x-2">
|
|
|
- <HoppButtonPrimary
|
|
|
- v-focus
|
|
|
- :label="t?.('action.yes')"
|
|
|
- outline
|
|
|
- @click="onResolveConfirmSaveTab"
|
|
|
- />
|
|
|
- <HoppButtonSecondary
|
|
|
- :label="t?.('action.no')"
|
|
|
- filled
|
|
|
- outline
|
|
|
- @click="onCloseConfirmSaveTab"
|
|
|
- />
|
|
|
- </span>
|
|
|
- </template>
|
|
|
- </HoppSmartModal>
|
|
|
<CollectionsSaveRequest
|
|
|
v-if="savingRequest"
|
|
|
mode="rest"
|
|
@@ -120,23 +96,37 @@
|
|
|
<script lang="ts" setup>
|
|
|
import { useI18n } from "@composables/i18n"
|
|
|
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
|
|
+import { watchDebounced } from "@vueuse/core"
|
|
|
import { useService } from "dioc/vue"
|
|
|
import { cloneDeep } from "lodash-es"
|
|
|
-import { onMounted, ref } from "vue"
|
|
|
+import {
|
|
|
+ BehaviorSubject,
|
|
|
+ EMPTY,
|
|
|
+ Subscription,
|
|
|
+ audit,
|
|
|
+ combineLatest,
|
|
|
+ from,
|
|
|
+ map,
|
|
|
+} from "rxjs"
|
|
|
+import { onBeforeUnmount, onMounted, ref } from "vue"
|
|
|
import { useRoute } from "vue-router"
|
|
|
+import { onLoggedIn } from "~/composables/auth"
|
|
|
import { useReadonlyStream } from "~/composables/stream"
|
|
|
+import { useToast } from "~/composables/toast"
|
|
|
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
|
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
|
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
|
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
|
|
+import {
|
|
|
+ changeCurrentSyncStatus,
|
|
|
+ currentSyncingStatus$,
|
|
|
+} from "~/newstore/syncing"
|
|
|
import { platform } from "~/platform"
|
|
|
import { InspectionService } from "~/services/inspection"
|
|
|
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
|
|
|
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
|
|
|
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
|
|
|
-import { HandleRef } from "~/services/new-workspace/handle"
|
|
|
-import { WorkspaceRequest } from "~/services/new-workspace/workspace"
|
|
|
-import { HoppTab } from "~/services/tab"
|
|
|
+import { HoppTab, PersistableTabState } from "~/services/tab"
|
|
|
import { RESTTabService } from "~/services/tab/rest"
|
|
|
|
|
|
const savingRequest = ref(false)
|
|
@@ -149,16 +139,12 @@ const exceptedTabID = ref<string | null>(null)
|
|
|
const renameTabID = ref<string | null>(null)
|
|
|
|
|
|
const t = useI18n()
|
|
|
+const toast = useToast()
|
|
|
|
|
|
const tabs = useService(RESTTabService)
|
|
|
|
|
|
const currentTabID = tabs.currentTabID
|
|
|
|
|
|
-const currentUser = useReadonlyStream(
|
|
|
- platform.auth.getCurrentUserStream(),
|
|
|
- platform.auth.getCurrentUser()
|
|
|
-)
|
|
|
-
|
|
|
type PopupDetails = {
|
|
|
show: boolean
|
|
|
position: {
|
|
@@ -179,6 +165,12 @@ const contextMenu = ref<PopupDetails>({
|
|
|
|
|
|
const activeTabs = tabs.getActiveTabs()
|
|
|
|
|
|
+const confirmSync = useReadonlyStream(currentSyncingStatus$, {
|
|
|
+ isInitialSync: false,
|
|
|
+ shouldSync: true,
|
|
|
+})
|
|
|
+const tabStateForSync = ref<PersistableTabState<HoppRESTDocument> | null>(null)
|
|
|
+
|
|
|
function bindRequestToURLParams() {
|
|
|
const route = useRoute()
|
|
|
// Get URL parameters and set that as the request
|
|
@@ -218,7 +210,7 @@ const inspectionService = useService(InspectionService)
|
|
|
const removeTab = (tabID: string) => {
|
|
|
const tabState = tabs.getTabRef(tabID).value
|
|
|
|
|
|
- if (tabState.document.isDirty) {
|
|
|
+ if (getTabDirtyStatus(tabState)) {
|
|
|
confirmingCloseForTabID.value = tabID
|
|
|
} else {
|
|
|
tabs.closeTab(tabState.id)
|
|
@@ -227,8 +219,10 @@ const removeTab = (tabID: string) => {
|
|
|
}
|
|
|
|
|
|
const closeOtherTabsAction = (tabID: string) => {
|
|
|
- const isTabDirty = tabs.getTabRef(tabID).value?.document.isDirty
|
|
|
const dirtyTabCount = tabs.getDirtyTabsCount()
|
|
|
+
|
|
|
+ const isTabDirty = getTabDirtyStatus(tabs.getTabRef(tabID).value)
|
|
|
+
|
|
|
// If current tab is dirty, so we need to subtract 1 from the dirty tab count
|
|
|
const balanceDirtyTabCount = isTabDirty ? dirtyTabCount - 1 : dirtyTabCount
|
|
|
|
|
@@ -301,12 +295,7 @@ const onResolveConfirmSaveTab = () => {
|
|
|
if (
|
|
|
!saveContext ||
|
|
|
(saveContext.originLocation === "workspace-user-collection" &&
|
|
|
- // `requestHandle` gets unwrapped here
|
|
|
- (
|
|
|
- saveContext.requestHandle as
|
|
|
- | HandleRef<WorkspaceRequest>["value"]
|
|
|
- | undefined
|
|
|
- )?.type === "invalid")
|
|
|
+ saveContext.requestHandle?.value.type === "invalid")
|
|
|
) {
|
|
|
return (savingRequest.value = true)
|
|
|
}
|
|
@@ -330,17 +319,120 @@ const onSaveModalClose = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-const shareTabRequest = (tabID: string) => {
|
|
|
- const tab = tabs.getTabRef(tabID)
|
|
|
- if (tab.value) {
|
|
|
- if (currentUser.value) {
|
|
|
- invokeAction("share.request", {
|
|
|
- request: tab.value.document.request,
|
|
|
- })
|
|
|
- } else {
|
|
|
- invokeAction("modals.login.toggle")
|
|
|
- }
|
|
|
+const syncTabState = () => {
|
|
|
+ if (tabStateForSync.value)
|
|
|
+ tabs.loadTabsFromPersistedState(tabStateForSync.value)
|
|
|
+}
|
|
|
+
|
|
|
+const getTabDirtyStatus = (tab: HoppTab<HoppRESTDocument>) => {
|
|
|
+ if (tab.document.isDirty) {
|
|
|
+ return true
|
|
|
}
|
|
|
+
|
|
|
+ return (
|
|
|
+ tab.document.saveContext?.originLocation === "workspace-user-collection" &&
|
|
|
+ tab.document.saveContext.requestHandle?.value.type === "invalid"
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Performs sync of the REST Tab session with Firestore.
|
|
|
+ *
|
|
|
+ * @returns A subscription to the sync observable stream.
|
|
|
+ * Unsubscribe to stop syncing.
|
|
|
+ */
|
|
|
+function startTabStateSync(): Subscription {
|
|
|
+ const currentUser$ = platform.auth.getCurrentUserStream()
|
|
|
+ const tabState$ =
|
|
|
+ new BehaviorSubject<PersistableTabState<HoppRESTDocument> | null>(null)
|
|
|
+
|
|
|
+ watchDebounced(
|
|
|
+ tabs.persistableTabState,
|
|
|
+ (state) => {
|
|
|
+ tabState$.next(state)
|
|
|
+ },
|
|
|
+ { debounce: 500, deep: true }
|
|
|
+ )
|
|
|
+
|
|
|
+ const sub = combineLatest([currentUser$, tabState$])
|
|
|
+ .pipe(
|
|
|
+ map(([user, tabState]) =>
|
|
|
+ user && tabState
|
|
|
+ ? from(platform.sync.tabState.writeCurrentTabState(user, tabState))
|
|
|
+ : EMPTY
|
|
|
+ ),
|
|
|
+ audit((x) => x)
|
|
|
+ )
|
|
|
+ .subscribe(() => {
|
|
|
+ // NOTE: This subscription should be kept
|
|
|
+ })
|
|
|
+
|
|
|
+ return sub
|
|
|
+}
|
|
|
+
|
|
|
+const showSyncToast = () => {
|
|
|
+ toast.show(t("confirm.sync"), {
|
|
|
+ duration: 0,
|
|
|
+ action: [
|
|
|
+ {
|
|
|
+ text: `${t("action.yes")}`,
|
|
|
+ onClick: (_, toastObject) => {
|
|
|
+ syncTabState()
|
|
|
+ changeCurrentSyncStatus({
|
|
|
+ isInitialSync: true,
|
|
|
+ shouldSync: true,
|
|
|
+ })
|
|
|
+ toastObject.goAway(0)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ text: `${t("action.no")}`,
|
|
|
+ onClick: (_, toastObject) => {
|
|
|
+ changeCurrentSyncStatus({
|
|
|
+ isInitialSync: true,
|
|
|
+ shouldSync: false,
|
|
|
+ })
|
|
|
+ toastObject.goAway(0)
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function setupTabStateSync() {
|
|
|
+ const route = useRoute()
|
|
|
+
|
|
|
+ // Subscription to request sync
|
|
|
+ let sub: Subscription | null = null
|
|
|
+
|
|
|
+ // Load request on login resolve and start sync
|
|
|
+ onLoggedIn(async () => {
|
|
|
+ if (
|
|
|
+ Object.keys(route.query).length === 0 &&
|
|
|
+ !(route.query.code || route.query.error)
|
|
|
+ ) {
|
|
|
+ const tabStateFromSync =
|
|
|
+ await platform.sync.tabState.loadTabStateFromSync()
|
|
|
+
|
|
|
+ if (tabStateFromSync && !confirmSync.value.isInitialSync) {
|
|
|
+ tabStateForSync.value = tabStateFromSync
|
|
|
+ showSyncToast()
|
|
|
+ // Have to set isInitialSync to true here because the toast is shown
|
|
|
+ // and the user does not click on any of the actions
|
|
|
+ changeCurrentSyncStatus({
|
|
|
+ isInitialSync: true,
|
|
|
+ shouldSync: false,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ sub = startTabStateSync()
|
|
|
+ })
|
|
|
+
|
|
|
+ // Stop subscription to stop syncing
|
|
|
+ onBeforeUnmount(() => {
|
|
|
+ sub?.unsubscribe()
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
defineActionHandler("contextmenu.open", ({ position, text }) => {
|
|
@@ -359,6 +451,7 @@ defineActionHandler("contextmenu.open", ({ position, text }) => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+setupTabStateSync()
|
|
|
bindRequestToURLParams()
|
|
|
|
|
|
defineActionHandler("rest.request.open", ({ doc }) => {
|
|
@@ -384,5 +477,3 @@ for (const inspectorDef of platform.additionalInspectors ?? []) {
|
|
|
useService(inspectorDef.service)
|
|
|
}
|
|
|
</script>
|
|
|
-import { HandleRef } from "~/services/new-workspace/handle" import {
|
|
|
-WorkspaceRequest } from "~/services/new-workspace/workspace"
|