refactor: keep tab dirty status logic at the page level

This commit is contained in:
jamesgeorge007
2024-04-25 18:14:22 +05:30
parent cd92dfec47
commit 197d253e8b
5 changed files with 177 additions and 173 deletions

View File

@@ -772,27 +772,6 @@ const onRemoveRootCollection = async () => {
return
}
const activeTabs = tabs.getActiveTabs()
for (const tab of activeTabs.value) {
if (
tab.document.saveContext?.originLocation === "workspace-user-collection"
) {
const requestHandle = tab.document.saveContext?.requestHandle as
| HandleRef<WorkspaceRequest>["value"]
| undefined
if (requestHandle?.type === "invalid") {
continue
}
if (requestHandle!.data.requestID.startsWith(collectionIndexPath)) {
tab.document.saveContext = null
tab.document.isDirty = true
}
}
}
toast.success(t("state.deleted"))
displayConfirmModal(false)
}
@@ -1062,28 +1041,6 @@ const onRemoveChildCollection = async () => {
return
}
// TODO: Tab holding a request under the collection should be aware of the parent collection invalidation and toggle the dirty state
const activeTabs = tabs.getActiveTabs()
for (const tab of activeTabs.value) {
if (
tab.document.saveContext?.originLocation === "workspace-user-collection"
) {
const requestHandle = tab.document.saveContext?.requestHandle as
| HandleRef<WorkspaceRequest>["value"]
| undefined
if (requestHandle?.type === "invalid") {
continue
}
if (requestHandle!.data.requestID.startsWith(parentCollectionIndexPath)) {
tab.document.saveContext = null
tab.document.isDirty = true
}
}
}
toast.success(t("state.deleted"))
displayConfirmModal(false)
}

View File

@@ -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"

View File

@@ -11,6 +11,7 @@ import {
computed,
effectScope,
markRaw,
nextTick,
ref,
shallowRef,
watch,
@@ -302,15 +303,18 @@ export class PersonalWorkspaceProviderService
)
}
for (const handle of this.issuedHandles) {
for (const [idx, handle] of this.issuedHandles.entries()) {
if (handle.value.type === "invalid") continue
if ("requestID" in handle.value.data) {
if (handle.value.data.requestID.startsWith(collectionID)) {
handle.value = {
type: "invalid",
reason: "REQUEST_INVALIDATED",
}
// @ts-expect-error - We're deleting the data to invalidate the handle
delete this.issuedHandles[idx].value.data
this.issuedHandles[idx].value.type = "invalid"
// @ts-expect-error - Setting the handle invalidation reason
this.issuedHandles[idx].value.reason = "REQUEST_INVALIDATED"
}
}
}
@@ -403,15 +407,18 @@ export class PersonalWorkspaceProviderService
removeRESTRequest(collectionID, requestIndex, requestToRemove?.id)
for (const handle of this.issuedHandles) {
for (const [idx, handle] of this.issuedHandles.entries()) {
if (handle.value.type === "invalid") continue
if ("requestID" in handle.value.data) {
if (handle.value.data.requestID === requestID) {
handle.value = {
type: "invalid",
reason: "REQUEST_INVALIDATED",
}
// @ts-expect-error - We're deleting the data to invalidate the handle
delete this.issuedHandles[idx].value.data
this.issuedHandles[idx].value.type = "invalid"
// @ts-expect-error - Setting the handle invalidation reason
this.issuedHandles[idx].value.reason = "REQUEST_INVALIDATED"
}
}
}

View File

@@ -35,12 +35,11 @@ export class RESTTabService extends TabService<HoppRESTDocument> {
lastActiveTabID: this.currentTabID.value,
orderedDocs: this.tabOrdering.value.map((tabID) => {
const tab = this.tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
const resolvedTabData = this.getResolvedTabData(tab)
return {
tabID: tab.id,
doc: {
...this.getPersistedDocument(resolvedTabData.document),
...this.getPersistedDocument(tab.document),
response: null,
},
}

View File

@@ -46,10 +46,7 @@ export abstract class TabService<Doc>
})
public currentActiveTab = computed(() => {
const tab = this.tabMap.get(this.currentTabID.value)!
return this.getResolvedTabData(
tab as HoppTab<HoppRESTDocument | HoppGQLDocument>
)
return this.tabMap.get(this.currentTabID.value)!
}) // Guaranteed to not be undefined
protected watchCurrentTabID() {
@@ -87,15 +84,7 @@ export abstract class TabService<Doc>
}
public getActiveTab(): HoppTab<Doc> | null {
const tab = this.tabMap.get(this.currentTabID.value)
if (!tab) {
return null
}
return this.getResolvedTabData(
tab as HoppTab<HoppRESTDocument | HoppGQLDocument>
)
return this.tabMap.get(this.currentTabID.value) ?? null
}
public setActiveTab(tabID: string): void {
@@ -172,13 +161,7 @@ export abstract class TabService<Doc>
public getActiveTabs(): Readonly<ComputedRef<HoppTab<Doc>[]>> {
return shallowReadonly(
computed(() =>
this.tabOrdering.value.map((x) => {
const tab = this.tabMap.get(x) as HoppTab<
HoppRESTDocument | HoppGQLDocument
>
return this.getResolvedTabData(tab)
})
this.tabOrdering.value.map((x) => this.tabMap.get(x) as HoppTab<Doc>)
)
)
}
@@ -186,13 +169,11 @@ export abstract class TabService<Doc>
public getTabRef(tabID: string) {
return computed({
get: () => {
const result = this.tabMap.get(tabID) as HoppTab<
HoppRESTDocument | HoppGQLDocument
>
const result = this.tabMap.get(tabID)
if (result === undefined) throw new Error(`Invalid tab id: ${tabID}`)
return this.getResolvedTabData(result)
return result
},
set: (value) => {
return this.tabMap.set(tabID, value)
@@ -304,13 +285,10 @@ export abstract class TabService<Doc>
lastActiveTabID: this.currentTabID.value,
orderedDocs: this.tabOrdering.value.map((tabID) => {
const tab = this.tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
const resolvedTabData = this.getResolvedTabData(
tab as HoppTab<HoppRESTDocument | HoppGQLDocument>
)
return {
tabID: tab.id,
doc: this.getPersistedDocument(resolvedTabData.document),
doc: this.getPersistedDocument(tab.document),
}
}),
}))
@@ -328,32 +306,4 @@ export abstract class TabService<Doc>
if (!this.tabMap.has(id)) return id
}
}
protected getResolvedTabData(
tab: HoppTab<HoppRESTDocument | HoppGQLDocument>
): HoppTab<Doc> {
if (
tab.document.isDirty ||
!tab.document.saveContext ||
tab.document.saveContext.originLocation !== "workspace-user-collection"
) {
return tab as HoppTab<Doc>
}
const requestHandle = tab.document.saveContext.requestHandle as
| HandleRef<WorkspaceRequest>["value"]
| undefined
if (!requestHandle) {
return tab as HoppTab<Doc>
}
return {
...tab,
document: {
...tab.document,
isDirty: requestHandle.type === "invalid",
},
} as HoppTab<Doc>
}
}