Skip to content

Commit

Permalink
refactor: introduce writable handles to signify updates to handle ref…
Browse files Browse the repository at this point in the history
…erences

A special list of writable handles is compiled in a list while issuing handles (request/collection creation, etc). Instead of manually computing the tab and toggling the dirty state, the writable handle is updated (changing the type to invalid on request deletion) and the tab with the request open can infer it via the update reflected in the request handle under the tab save context (reactive update trigger).
  • Loading branch information
jamesgeorge007 committed Apr 24, 2024
1 parent 854ffa2 commit ee1e98e
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 114 deletions.
Expand Up @@ -1118,15 +1118,6 @@ const onRemoveRequest = async () => {
return
}
const { providerID, requestID, workspaceID } = requestHandle.value.data
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "workspace-user-collection",
workspaceID,
providerID,
requestID,
})
if (
isSelected({
requestIndex: parseInt(requestIndexPath.split("/").pop() ?? ""),
Expand All @@ -1143,12 +1134,6 @@ const onRemoveRequest = async () => {
return
}
// If there is a tab attached to this request, dissociate its state and mark it dirty
if (possibleTab) {
possibleTab.value.document.saveContext = null
possibleTab.value.document.isDirty = true
}
toast.success(t("state.deleted"))
displayConfirmModal(false)
}
Expand Down
70 changes: 43 additions & 27 deletions packages/hoppscotch-common/src/pages/index.vue
Expand Up @@ -13,7 +13,7 @@
<HoppSmartWindow
v-for="tab in activeTabs"
:id="tab.id"
:key="tab.id"
:key="`${tab.id}-${tab.document.isDirty}`"
:label="tab.document.request.name"
:is-removable="activeTabs.length > 1"
:close-visibility="'hover'"
Expand Down Expand Up @@ -94,40 +94,42 @@
</template>

<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from "vue"
import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { useRoute } from "vue-router"
import { useI18n } from "@composables/i18n"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { onLoggedIn } from "~/composables/auth"
import { platform } from "~/platform"
import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { watchDebounced } from "@vueuse/core"
import { useService } from "dioc/vue"
import { cloneDeep } from "lodash-es"
import {
audit,
BehaviorSubject,
combineLatest,
EMPTY,
Subscription,
audit,
combineLatest,
from,
map,
Subscription,
} from "rxjs"
import { useToast } from "~/composables/toast"
import { watchDebounced } from "@vueuse/core"
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 { useService } from "dioc/vue"
import { platform } from "~/platform"
import { InspectionService } from "~/services/inspection"
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
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 { cloneDeep } from "lodash-es"
import { RESTTabService } from "~/services/tab/rest"
import { HandleRef } from "~/services/new-workspace/handle"
import { WorkspaceRequest } from "~/services/new-workspace/workspace"
import { HoppTab, PersistableTabState } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { RESTTabService } from "~/services/tab/rest"
const savingRequest = ref(false)
const confirmingCloseForTabID = ref<string | null>(null)
Expand Down Expand Up @@ -285,15 +287,29 @@ const onCloseConfirmSaveTab = () => {
* Called when the user confirms they want to save the tab
*/
const onResolveConfirmSaveTab = () => {
if (tabs.currentActiveTab.value.document.saveContext) {
invokeAction("request.save")
const { saveContext } = tabs.currentActiveTab.value.document
// There're two cases the save request under a collection modal should open
// 1. Attempting to save a request that is not under a collection (When the save context is not available)
// 2. Deleting a request from the collection tree and attempting to save it while closing the respective tab (When the request handle is invalid)
if (
!saveContext ||
(saveContext.originLocation === "workspace-user-collection" &&
// `requestHandle` gets unwrapped here
(
saveContext.requestHandle as
| HandleRef<WorkspaceRequest>["value"]
| undefined
)?.type === "invalid")
) {
return (savingRequest.value = true)
}
if (confirmingCloseForTabID.value) {
tabs.closeTab(confirmingCloseForTabID.value)
confirmingCloseForTabID.value = null
}
} else {
savingRequest.value = true
invokeAction("request.save")
if (confirmingCloseForTabID.value) {
tabs.closeTab(confirmingCloseForTabID.value)
confirmingCloseForTabID.value = null
}
}
Expand Down
5 changes: 5 additions & 0 deletions packages/hoppscotch-common/src/services/inspection/index.ts
Expand Up @@ -123,6 +123,11 @@ export class InspectionService extends Service {
}

private initializeListeners() {
console.log(
`Current active tab from inspection service is `,
this.restTab.currentActiveTab.value
)

watch(
() => [this.inspectors.entries(), this.restTab.currentActiveTab.value.id],
() => {
Expand Down
@@ -1,5 +1,12 @@
import { Ref } from "vue"
import { Ref, WritableComputedRef } from "vue"

export type HandleRef<T, InvalidateReason = unknown> = Ref<
{ type: "ok"; data: T } | { type: "invalid"; reason: InvalidateReason }
>

export type WritableHandleRef<
T,
InvalidateReason = unknown,
> = WritableComputedRef<
{ type: "ok"; data: T } | { type: "invalid"; reason: InvalidateReason }
>
Expand Up @@ -39,7 +39,7 @@ import {
} from "~/newstore/collections"
import { platform } from "~/platform"

import { HandleRef } from "~/services/new-workspace/handle"
import { HandleRef, WritableHandleRef } from "~/services/new-workspace/handle"
import { WorkspaceProvider } from "~/services/new-workspace/provider"
import {
RESTCollectionChildrenView,
Expand Down Expand Up @@ -87,6 +87,10 @@ export class PersonalWorkspaceProviderService

private restCollectionState: Ref<{ state: HoppCollection[] }>

private issuedHandles: WritableHandleRef<
WorkspaceCollection | WorkspaceRequest
>[] = []

public constructor() {
super()

Expand Down Expand Up @@ -329,35 +333,44 @@ export class PersonalWorkspaceProviderService
platform: "rest",
})

return Promise.resolve(
E.right(
computed(() => {
if (
!isValidCollectionHandle(
parentCollectionHandle,
this.providerID,
"personal"
)
) {
return {
type: "invalid" as const,
reason: "COLLECTION_INVALIDATED" as const,
}
}
const handle: HandleRef<WorkspaceRequest> = computed(() => {
if (
!isValidCollectionHandle(
parentCollectionHandle,
this.providerID,
"personal"
)
) {
return {
type: "invalid" as const,
reason: "COLLECTION_INVALIDATED" as const,
}
}

return {
type: "ok",
data: {
providerID,
workspaceID,
collectionID,
requestID,
request: newRequest,
},
}
})
)
)
return {
type: "ok",
data: {
providerID,
workspaceID,
collectionID,
requestID,
request: newRequest,
},
}
})

const writableHandle = computed({
get() {
return handle.value
},
set(newValue) {
handle.value = newValue
},
})

this.issuedHandles.push(writableHandle)

return Promise.resolve(E.right(handle))
}

public removeRESTRequest(
Expand All @@ -377,6 +390,19 @@ export class PersonalWorkspaceProviderService

removeRESTRequest(collectionID, requestIndex, requestToRemove?.id)

for (const handle of this.issuedHandles) {
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",
}
}
}
}

return Promise.resolve(E.right(undefined))
}

Expand Down Expand Up @@ -647,35 +673,42 @@ export class PersonalWorkspaceProviderService
return Promise.resolve(E.left("REQUEST_NOT_FOUND" as const))
}

return Promise.resolve(
E.right(
computed(() => {
if (
!isValidWorkspaceHandle(
workspaceHandle,
this.providerID,
"personal"
)
) {
return {
type: "invalid" as const,
reason: "WORKSPACE_INVALIDATED" as const,
}
}
const handleRefData = ref({
type: "ok" as const,
data: {
providerID,
workspaceID,
collectionID,
requestID,
request,
},
})

return {
type: "ok",
data: {
providerID,
workspaceID,
collectionID,
requestID,
request,
},
}
})
)
)
const handle: HandleRef<WorkspaceRequest> = computed(() => {
if (
!isValidWorkspaceHandle(workspaceHandle, this.providerID, "personal")
) {
return {
type: "invalid" as const,
reason: "WORKSPACE_INVALIDATED" as const,
}
}

return handleRefData.value
})

const writableHandle = computed({
get() {
return handleRefData.value
},
set(newValue) {
handleRefData.value = newValue
},
})

this.issuedHandles.push(writableHandle)

return Promise.resolve(E.right(handle))
}

public getRESTCollectionChildrenView(
Expand Down
6 changes: 4 additions & 2 deletions packages/hoppscotch-common/src/services/tab/rest.ts
Expand Up @@ -29,17 +29,19 @@ 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: {
...tab.document,
...this.getPersistedDocument(resolvedTabData.document),
response: null,
},
}
}),
}))

public getTabRefWithSaveContext(ctx: HoppRESTSaveContext) {
public getTabRefWithSaveContext(ctx: Partial<HoppRESTSaveContext>) {
for (const tab of this.tabMap.values()) {
// For `team-collection` request id can be considered unique
if (ctx?.originLocation === "team-collection") {
Expand Down

0 comments on commit ee1e98e

Please sign in to comment.