feat: tab service added (#3367)

This commit is contained in:
Anwarul Islam
2023-10-11 18:51:07 +06:00
committed by GitHub
parent 51510566bc
commit ba31cdabea
60 changed files with 1112 additions and 841 deletions

View File

@@ -0,0 +1,137 @@
import { describe, expect, it } from "vitest"
import { TestContainer } from "dioc/testing"
import { TabService } from "../tab"
import { reactive } from "vue"
class MockTabService extends TabService<{ request: string }> {
public static readonly ID = "MOCK_TAB_SERVICE"
constructor() {
super()
this.tabMap = reactive(
new Map([
[
"test",
{
id: "test",
document: {
request: "test request",
},
},
],
])
)
this.watchCurrentTabID()
}
}
describe("TabService", () => {
it("initially only one tab is defined", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
expect(service.getActiveTabs().value.length).toEqual(1)
})
it("initially the only tab is the active tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
expect(service.getActiveTab()).not.toBeNull()
})
it("initially active tab id is test", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
expect(service.getActiveTab()?.id).toEqual("test")
})
it("add new tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.createNewTab({
request: "new request",
})
expect(service.getActiveTabs().value.length).toEqual(2)
})
it("get active tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
console.log(service.getActiveTab())
expect(service.getActiveTab()?.id).toEqual("test")
})
it("sort tabs", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
const currentOrder = service.updateTabOrdering(1, 0)
expect(currentOrder[1]).toEqual("test")
})
it("update tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.updateTab({
id: service.currentTabID.value,
document: {
request: "updated request",
},
})
expect(service.getActiveTab()?.document.request).toEqual("updated request")
})
it("set new active tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.setActiveTab("test")
expect(service.getActiveTab()?.id).toEqual("test")
})
it("close other tabs", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.closeOtherTabs("test")
expect(service.getActiveTabs().value.length).toEqual(1)
})
it("close tab", () => {
const container = new TestContainer()
const service = container.bind(MockTabService)
service.createNewTab({
request: "new request",
})
expect(service.getActiveTabs().value.length).toEqual(2)
service.closeTab("test")
expect(service.getActiveTabs().value.length).toEqual(1)
})
})

View File

@@ -0,0 +1,66 @@
import { isEqual } from "lodash-es"
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
import { HoppGQLDocument, HoppGQLSaveContext } from "~/helpers/graphql/document"
import { TabService } from "./tab"
import { computed } from "vue"
export class GQLTabService extends TabService<HoppGQLDocument> {
public static readonly ID = "GQL_TAB_SERVICE"
constructor() {
super()
this.tabMap.set("test", {
id: "test",
document: {
request: getDefaultGQLRequest(),
isDirty: false,
optionTabPreference: "query",
},
})
this.watchCurrentTabID()
}
// override persistableTabState to remove response from the document
public override persistableTabState = computed(() => ({
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
return {
tabID: tab.id,
doc: {
...tab.document,
response: null,
},
}
}),
}))
public getTabRefWithSaveContext(ctx: HoppGQLSaveContext) {
for (const tab of this.tabMap.values()) {
// For `team-collection` request id can be considered unique
if (ctx?.originLocation === "team-collection") {
if (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.requestID === ctx.requestID
) {
return this.getTabRef(tab.id)
}
} else if (isEqual(ctx, tab.document.saveContext))
return this.getTabRef(tab.id)
}
return null
}
public getDirtyTabsCount() {
let count = 0
for (const tab of this.tabMap.values()) {
if (tab.document.isDirty) count++
}
return count
}
}

View File

@@ -0,0 +1,116 @@
import { ComputedRef, WritableComputedRef } from "vue"
/**
* Represents a tab in HoppScotch.
* @template Doc The type of the document associated with the tab.
*/
export type HoppTab<Doc> = {
/** The unique identifier of the tab. */
id: string
/** The document associated with the tab. */
document: Doc
}
export type PersistableTabState<Doc> = {
lastActiveTabID: string
orderedDocs: Array<{
tabID: string
doc: Doc
}>
}
/**
* Represents a service for managing tabs with documents.
* @template Doc - The type of document associated with each tab.
*/
export interface TabService<Doc> {
/**
* Gets the current active tab.
*/
currentActiveTab: ComputedRef<HoppTab<Doc>>
/**
* Creates a new tab with the given document and sets it as the active tab.
* @param document - The document to associate with the new tab.
* @returns The newly created tab.
*/
createNewTab(document: Doc): HoppTab<Doc>
/**
* Gets an array of all tabs.
* @returns An array of all tabs.
*/
getTabs(): HoppTab<Doc>[]
/**
* Gets the currently active tab.
* @returns The active tab or null if no tab is active.
*/
getActiveTab(): HoppTab<Doc> | null
/**
* Sets the active tab by its ID.
* @param tabID - The ID of the tab to set as active.
*/
setActiveTab(tabID: string): void
/**
* Loads tabs and their ordering from a persisted state.
* @param data - The persisted tab state to load.
*/
loadTabsFromPersistedState(data: PersistableTabState<Doc>): void
/**
* Gets a read-only computed reference to the active tabs.
* @returns A computed reference to the active tabs.
*/
getActiveTabs(): Readonly<ComputedRef<HoppTab<Doc>[]>>
/**
* Gets a computed reference to a specific tab by its ID.
* @param tabID - The ID of the tab to retrieve.
* @returns A computed reference to the specified tab.
* @throws An error if the tab with the specified ID does not exist.
*/
getTabRef(tabID: string): WritableComputedRef<HoppTab<Doc>>
/**
* Updates the properties of a tab.
* @param tabUpdate - The updated tab object.
*/
updateTab(tabUpdate: HoppTab<Doc>): void
/**
* Updates the ordering of tabs by moving a tab from one index to another.
* @param fromIndex - The current index of the tab to move.
* @param toIndex - The target index where the tab should be moved to.
*/
updateTabOrdering(fromIndex: number, toIndex: number): void
/**
* Closes the tab with the specified ID.
* @param tabID - The ID of the tab to close.
*/
closeTab(tabID: string): void
/**
* Closes all tabs except the one with the specified ID.
* @param tabID - The ID of the tab to keep open.
*/
closeOtherTabs(tabID: string): void
/**
* Gets a computed reference to a persistable tab state.
* @returns A computed reference to a persistable tab state object.
*/
persistableTabState: ComputedRef<PersistableTabState<Doc>>
/**
* Gets computed references to tabs that match a specified condition.
* @param func - A function that defines the condition for selecting tabs.
* @returns An array of computed references to matching tabs.
*/
getTabsRefTo(
func: (tab: HoppTab<Doc>) => boolean
): WritableComputedRef<HoppTab<Doc>>[]
}

View File

@@ -0,0 +1,67 @@
import { isEqual } from "lodash-es"
import { computed } from "vue"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { HoppRESTDocument, HoppRESTSaveContext } from "~/helpers/rest/document"
import { TabService } from "./tab"
export class RESTTabService extends TabService<HoppRESTDocument> {
public static readonly ID = "REST_TAB_SERVICE"
constructor() {
super()
this.tabMap.set("test", {
id: "test",
document: {
request: getDefaultRESTRequest(),
isDirty: false,
optionTabPreference: "params",
},
})
this.watchCurrentTabID()
}
// override persistableTabState to remove response from the document
public override persistableTabState = computed(() => ({
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
return {
tabID: tab.id,
doc: {
...tab.document,
response: null,
},
}
}),
}))
public getTabRefWithSaveContext(ctx: HoppRESTSaveContext) {
for (const tab of this.tabMap.values()) {
// For `team-collection` request id can be considered unique
if (ctx?.originLocation === "team-collection") {
if (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.requestID === ctx.requestID
) {
return this.getTabRef(tab.id)
}
} else if (isEqual(ctx, tab.document.saveContext)) {
return this.getTabRef(tab.id)
}
}
return null
}
public getDirtyTabsCount() {
let count = 0
for (const tab of this.tabMap.values()) {
if (tab.document.isDirty) count++
}
return count
}
}

View File

@@ -0,0 +1,207 @@
import { refWithControl } from "@vueuse/core"
import { Service } from "dioc"
import { v4 as uuidV4 } from "uuid"
import {
ComputedRef,
computed,
nextTick,
reactive,
ref,
shallowReadonly,
watch,
} from "vue"
import {
HoppTab,
PersistableTabState,
TabService as TabServiceInterface,
} from "."
export abstract class TabService<Doc>
extends Service
implements TabServiceInterface<Doc>
{
protected tabMap = reactive(new Map<string, HoppTab<Doc>>())
protected tabOrdering = ref<string[]>(["test"])
public currentTabID = refWithControl("test", {
onBeforeChange: (newTabID) => {
if (!newTabID || !this.tabMap.has(newTabID)) {
console.warn(
`Tried to set current tab id to an invalid value. (value: ${newTabID})`
)
// Don't allow change
return false
}
},
})
public currentActiveTab = computed(
() => this.tabMap.get(this.currentTabID.value)!
) // Guaranteed to not be undefined
protected watchCurrentTabID() {
watch(
this.tabOrdering,
(newOrdering) => {
if (
!this.currentTabID.value ||
!newOrdering.includes(this.currentTabID.value)
) {
this.setActiveTab(newOrdering[newOrdering.length - 1]) // newOrdering should always be non-empty
}
},
{ deep: true }
)
}
public createNewTab(document: Doc, switchToIt = true): HoppTab<Doc> {
const id = this.generateNewTabID()
const tab: HoppTab<Doc> = { id, document }
this.tabMap.set(id, tab)
this.tabOrdering.value.push(id)
if (switchToIt) {
this.setActiveTab(id)
}
return tab
}
public getTabs(): HoppTab<Doc>[] {
return Array.from(this.tabMap.values())
}
public getActiveTab(): HoppTab<Doc> | null {
return this.tabMap.get(this.currentTabID.value) ?? null
}
public setActiveTab(tabID: string): void {
this.currentTabID.value = tabID
}
public loadTabsFromPersistedState(data: PersistableTabState<Doc>): void {
if (data) {
this.tabMap.clear()
this.tabOrdering.value = []
for (const doc of data.orderedDocs) {
this.tabMap.set(doc.tabID, {
id: doc.tabID,
document: doc.doc,
})
this.tabOrdering.value.push(doc.tabID)
}
this.setActiveTab(data.lastActiveTabID)
}
}
public getActiveTabs(): Readonly<ComputedRef<HoppTab<Doc>[]>> {
return shallowReadonly(
computed(() => this.tabOrdering.value.map((x) => this.tabMap.get(x)!))
)
}
public getTabRef(tabID: string) {
return computed({
get: () => {
const result = this.tabMap.get(tabID)
if (result === undefined) throw new Error(`Invalid tab id: ${tabID}`)
return result
},
set: (value) => {
return this.tabMap.set(tabID, value)
},
})
}
public updateTab(tabUpdate: HoppTab<Doc>) {
if (!this.tabMap.has(tabUpdate.id)) {
console.warn(
`Cannot update tab as tab with that tab id does not exist (id: ${tabUpdate.id})`
)
}
this.tabMap.set(tabUpdate.id, tabUpdate)
}
public updateTabOrdering(fromIndex: number, toIndex: number) {
this.tabOrdering.value.splice(
toIndex,
0,
this.tabOrdering.value.splice(fromIndex, 1)[0]
)
return this.tabOrdering.value
}
public closeTab(tabID: string) {
if (!this.tabMap.has(tabID)) {
console.warn(
`Tried to close a tab which does not exist (tab id: ${tabID})`
)
return
}
if (this.tabOrdering.value.length === 1) {
console.warn(
`Tried to close the only tab open, which is not allowed. (tab id: ${tabID})`
)
return
}
this.tabOrdering.value.splice(this.tabOrdering.value.indexOf(tabID), 1)
nextTick(() => {
this.tabMap.delete(tabID)
})
}
public closeOtherTabs(tabID: string) {
if (!this.tabMap.has(tabID)) {
console.warn(
`The tab to close other tabs does not exist (tab id: ${tabID})`
)
return
}
this.tabOrdering.value = [tabID]
this.tabMap.forEach((_, id) => {
if (id !== tabID) this.tabMap.delete(id)
})
this.currentTabID.value = tabID
}
public persistableTabState = computed<PersistableTabState<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
return {
tabID: tab.id,
doc: tab.document,
}
}),
}))
public getTabsRefTo(func: (tab: HoppTab<Doc>) => boolean) {
return Array.from(this.tabMap.values())
.filter(func)
.map((tab) => this.getTabRef(tab.id))
}
private generateNewTabID() {
while (true) {
const id = uuidV4()
if (!this.tabMap.has(id)) return id
}
}
}