refactor: minor performance improvements on teams related operations
This commit is contained in:
@@ -24,7 +24,6 @@ declare module 'vue' {
|
||||
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
||||
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
||||
AppSocial: typeof import('./components/app/Social.vue')['default']
|
||||
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
|
||||
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
|
||||
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
|
||||
|
||||
@@ -249,12 +249,11 @@ import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
||||
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -282,10 +281,11 @@ const currentUser = useReadonlyStream(
|
||||
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
|
||||
|
||||
// TeamList-Adapter
|
||||
const teamListAdapter = new TeamListAdapter(true)
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
const workspaceName = computed(() =>
|
||||
workspace.value.type === "personal"
|
||||
@@ -297,20 +297,18 @@ const refetchTeams = () => {
|
||||
teamListAdapter.fetchList()
|
||||
}
|
||||
|
||||
onLoggedIn(() => {
|
||||
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => myTeams.value,
|
||||
(newTeams) => {
|
||||
if (newTeams && workspace.value.type === "team" && workspace.value.teamID) {
|
||||
const team = newTeams.find((team) => team.id === workspace.value.teamID)
|
||||
const space = workspace.value
|
||||
|
||||
if (newTeams && space.type === "team" && space.teamID) {
|
||||
const team = newTeams.find((team) => team.id === space.teamID)
|
||||
if (team) {
|
||||
selectedTeam.value = team
|
||||
// Update the workspace name if it's not the same as the updated team name
|
||||
if (team.name !== workspace.value.teamName) {
|
||||
updateWorkspaceTeamName(workspace.value, team.name)
|
||||
if (team.name !== space.teamName) {
|
||||
workspaceService.updateWorkspaceTeamName(team.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,10 +162,8 @@ import { computed, nextTick, PropType, ref, watch } from "vue"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
@@ -221,7 +219,6 @@ import {
|
||||
import * as E from "fp-ts/Either"
|
||||
import { platform } from "~/platform"
|
||||
import { createCollectionGists } from "~/helpers/gist"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import {
|
||||
createNewTab,
|
||||
currentActiveTab,
|
||||
@@ -240,6 +237,8 @@ import {
|
||||
} from "~/helpers/collection/collection"
|
||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -316,7 +315,8 @@ const creatingGistCollection = ref(false)
|
||||
const importingMyCollections = ref(false)
|
||||
|
||||
// TeamList-Adapter
|
||||
const teamListAdapter = new TeamListAdapter(true)
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
const teamListFetched = ref(false)
|
||||
@@ -374,17 +374,18 @@ const updateSelectedTeam = (team: SelectedTeam) => {
|
||||
}
|
||||
}
|
||||
|
||||
onLoggedIn(() => {
|
||||
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
||||
})
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
// Used to switch collection type and team when user switch workspace in the global workspace switcher
|
||||
// Check if there is a teamID in the workspace, if yes, switch to team collection and select the team
|
||||
// If there is no teamID, switch to my environment
|
||||
watch(
|
||||
() => workspace.value.teamID,
|
||||
() => {
|
||||
const space = workspace.value
|
||||
|
||||
if (space.type === "personal") return undefined
|
||||
else return space.teamID
|
||||
},
|
||||
(teamID) => {
|
||||
if (!teamID) {
|
||||
switchToMyCollections()
|
||||
|
||||
@@ -308,7 +308,6 @@ import {
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||
@@ -316,10 +315,10 @@ import { invokeAction } from "~/helpers/actions"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { onMounted } from "vue"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { useService } from "dioc/vue"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
type Scope =
|
||||
| {
|
||||
@@ -353,21 +352,18 @@ type EnvironmentType = "my-environments" | "team-environments"
|
||||
|
||||
const myEnvironments = useReadonlyStream(environments$, [])
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
// TeamList-Adapter
|
||||
const teamListAdapter = new TeamListAdapter(true)
|
||||
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||
const teamListFetched = ref(false)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
|
||||
onLoggedIn(() => {
|
||||
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
||||
})
|
||||
|
||||
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
|
||||
REMEMBERED_TEAM_ID.value = team.id
|
||||
changeWorkspace({
|
||||
workspaceService.changeWorkspace({
|
||||
teamID: team.id,
|
||||
teamName: team.name,
|
||||
type: "team",
|
||||
|
||||
@@ -58,16 +58,15 @@ import {
|
||||
} from "~/newstore/environments"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { deleteEnvironment } from "~/newstore/environments"
|
||||
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -99,7 +98,8 @@ const currentUser = useReadonlyStream(
|
||||
)
|
||||
|
||||
// TeamList-Adapter
|
||||
const teamListAdapter = new TeamListAdapter(true)
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||
const teamListFetched = ref(false)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
@@ -152,11 +152,7 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
||||
})
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
// Switch to my environments if workspace is personal and to team environments if workspace is team
|
||||
// also resets selected environment if workspace is personal and the previous selected environment was a team environment
|
||||
|
||||
@@ -216,7 +216,8 @@ import IconClose from "~icons/lucide/x"
|
||||
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import { useService } from "dioc/vue"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppTestResult | null | undefined
|
||||
@@ -231,7 +232,8 @@ const testResults = useVModel(props, "modelValue", emit)
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
const showMyEnvironmentDetailsModal = ref(false)
|
||||
const showTeamEnvironmentDetailsModal = ref(false)
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { onLoggedIn } from "@composables/auth"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -89,7 +89,8 @@ const showModalInvite = ref(false)
|
||||
const editingTeam = ref<any>({}) // TODO: Check this out
|
||||
const editingTeamID = ref<any>("")
|
||||
|
||||
const adapter = new TeamListAdapter(true)
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const adapter = workspaceService.acquireTeamListAdapter(10000)
|
||||
const adapterLoading = useReadonlyStream(adapter.loading$, false)
|
||||
const adapterError = useReadonlyStream(adapter.error$, null)
|
||||
const myTeams = useReadonlyStream(adapter.teamList$, [])
|
||||
@@ -98,12 +99,6 @@ const loading = computed(
|
||||
() => adapterLoading.value && myTeams.value.length === 0
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
try {
|
||||
adapter.initialize()
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
showModalAdd.value = shouldDisplay
|
||||
adapter.fetchList()
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useService } from "dioc/vue"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
defineProps<{
|
||||
section?: string
|
||||
@@ -26,7 +26,8 @@ defineProps<{
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
const teamWorkspaceName = computed(() => {
|
||||
if (workspace.value.type === "team" && workspace.value.teamName) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="rootEl">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartItem
|
||||
@@ -69,20 +69,20 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import IconUsers from "~icons/lucide/users"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { useElementVisibility, useIntervalFn } from "@vueuse/core"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
@@ -94,13 +94,37 @@ const currentUser = useReadonlyStream(
|
||||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
const teamListadapter = new TeamListAdapter(true)
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const teamListadapter = workspaceService.acquireTeamListAdapter(null)
|
||||
const myTeams = useReadonlyStream(teamListadapter.teamList$, [])
|
||||
const isTeamListLoading = useReadonlyStream(teamListadapter.loading$, false)
|
||||
const teamListAdapterError = useReadonlyStream(teamListadapter.error$, null)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
const teamListFetched = ref(false)
|
||||
|
||||
const rootEl = ref<HTMLElement>()
|
||||
const elVisible = useElementVisibility(rootEl)
|
||||
|
||||
const { pause: pauseListPoll, resume: resumeListPoll } = useIntervalFn(() => {
|
||||
if (teamListadapter.isInitialized) {
|
||||
teamListadapter.fetchList()
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
watch(
|
||||
elVisible,
|
||||
() => {
|
||||
if (elVisible.value) {
|
||||
teamListadapter.fetchList()
|
||||
|
||||
resumeListPoll()
|
||||
} else {
|
||||
pauseListPoll()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(myTeams, (teams) => {
|
||||
if (teams && !teamListFetched.value) {
|
||||
teamListFetched.value = true
|
||||
@@ -115,7 +139,7 @@ const loading = computed(
|
||||
() => isTeamListLoading.value && myTeams.value.length === 0
|
||||
)
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
const isActiveWorkspace = computed(() => (id: string) => {
|
||||
if (workspace.value.type === "personal") return false
|
||||
@@ -124,7 +148,7 @@ const isActiveWorkspace = computed(() => (id: string) => {
|
||||
|
||||
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
|
||||
REMEMBERED_TEAM_ID.value = team.id
|
||||
changeWorkspace({
|
||||
workspaceService.changeWorkspace({
|
||||
teamID: team.id,
|
||||
teamName: team.name,
|
||||
type: "team",
|
||||
@@ -133,15 +157,11 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
|
||||
|
||||
const switchToPersonalWorkspace = () => {
|
||||
REMEMBERED_TEAM_ID.value = undefined
|
||||
changeWorkspace({
|
||||
workspaceService.changeWorkspace({
|
||||
type: "personal",
|
||||
})
|
||||
}
|
||||
|
||||
onLoggedIn(() => {
|
||||
teamListadapter.initialize()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentUser.value,
|
||||
(user) => {
|
||||
|
||||
@@ -902,36 +902,16 @@ export default class NewTeamCollectionAdapter {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a collection on the tree
|
||||
*
|
||||
* When a collection is loaded initially in the adapter, children and requests are not loaded (they will be set to null)
|
||||
* Upon expansion those two fields will be populated
|
||||
*
|
||||
* @param {string} collectionID - The ID of the collection to expand
|
||||
*/
|
||||
async expandCollection(collectionID: string): Promise<void> {
|
||||
// TODO: While expanding one collection, block (or queue) the expansion of the other, to avoid race conditions
|
||||
const tree = this.collections$.value
|
||||
|
||||
const collection = findCollInTree(tree, collectionID)
|
||||
|
||||
if (!collection) return
|
||||
|
||||
if (collection.children != null) return
|
||||
|
||||
private async getCollectionChildren(
|
||||
collection: TeamCollection
|
||||
): Promise<TeamCollection[]> {
|
||||
const collections: TeamCollection[] = []
|
||||
|
||||
this.loadingCollections$.next([
|
||||
...this.loadingCollections$.getValue(),
|
||||
collectionID,
|
||||
])
|
||||
|
||||
while (true) {
|
||||
const data = await runGQLQuery({
|
||||
query: GetCollectionChildrenDocument,
|
||||
variables: {
|
||||
collectionID,
|
||||
collectionID: collection.id,
|
||||
cursor:
|
||||
collections.length > 0
|
||||
? collections[collections.length - 1].id
|
||||
@@ -940,12 +920,8 @@ export default class NewTeamCollectionAdapter {
|
||||
})
|
||||
|
||||
if (E.isLeft(data)) {
|
||||
this.loadingCollections$.next(
|
||||
this.loadingCollections$.getValue().filter((x) => x !== collectionID)
|
||||
)
|
||||
|
||||
throw new Error(
|
||||
`Child Collection Fetch Error for ${collectionID}: ${data.left}`
|
||||
`Child Collection Fetch Error for ${collection.id}: ${data.left}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -965,23 +941,25 @@ export default class NewTeamCollectionAdapter {
|
||||
break
|
||||
}
|
||||
|
||||
return collections
|
||||
}
|
||||
|
||||
private async getCollectionRequests(
|
||||
collection: TeamCollection
|
||||
): Promise<TeamRequest[]> {
|
||||
const requests: TeamRequest[] = []
|
||||
|
||||
while (true) {
|
||||
const data = await runGQLQuery({
|
||||
query: GetCollectionRequestsDocument,
|
||||
variables: {
|
||||
collectionID,
|
||||
collectionID: collection.id,
|
||||
cursor:
|
||||
requests.length > 0 ? requests[requests.length - 1].id : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
if (E.isLeft(data)) {
|
||||
this.loadingCollections$.next(
|
||||
this.loadingCollections$.getValue().filter((x) => x !== collectionID)
|
||||
)
|
||||
|
||||
throw new Error(`Child Request Fetch Error for ${data}: ${data.left}`)
|
||||
}
|
||||
|
||||
@@ -989,7 +967,7 @@ export default class NewTeamCollectionAdapter {
|
||||
...data.right.requestsInCollection.map<TeamRequest>((el) => {
|
||||
return {
|
||||
id: el.id,
|
||||
collectionID,
|
||||
collectionID: collection.id,
|
||||
title: el.title,
|
||||
request: translateToNewRequest(JSON.parse(el.request)),
|
||||
}
|
||||
@@ -1000,17 +978,50 @@ export default class NewTeamCollectionAdapter {
|
||||
break
|
||||
}
|
||||
|
||||
collection.children = collections
|
||||
collection.requests = requests
|
||||
return requests
|
||||
}
|
||||
|
||||
// Add to the entity ids set
|
||||
collections.forEach((coll) => this.entityIDs.add(`collection-${coll.id}`))
|
||||
requests.forEach((req) => this.entityIDs.add(`request-${req.id}`))
|
||||
/**
|
||||
* Expands a collection on the tree
|
||||
*
|
||||
* When a collection is loaded initially in the adapter, children and requests are not loaded (they will be set to null)
|
||||
* Upon expansion those two fields will be populated
|
||||
*
|
||||
* @param {string} collectionID - The ID of the collection to expand
|
||||
*/
|
||||
async expandCollection(collectionID: string): Promise<void> {
|
||||
// TODO: While expanding one collection, block (or queue) the expansion of the other, to avoid race conditions
|
||||
const tree = this.collections$.value
|
||||
|
||||
this.loadingCollections$.next(
|
||||
this.loadingCollections$.getValue().filter((x) => x !== collectionID)
|
||||
)
|
||||
const collection = findCollInTree(tree, collectionID)
|
||||
|
||||
this.collections$.next(tree)
|
||||
if (!collection) return
|
||||
|
||||
if (collection.children != null) return
|
||||
|
||||
this.loadingCollections$.next([
|
||||
...this.loadingCollections$.getValue(),
|
||||
collectionID,
|
||||
])
|
||||
|
||||
try {
|
||||
const [collections, requests] = await Promise.all([
|
||||
this.getCollectionChildren(collection),
|
||||
this.getCollectionRequests(collection),
|
||||
])
|
||||
|
||||
collection.children = collections
|
||||
collection.requests = requests
|
||||
|
||||
// Add to the entity ids set
|
||||
collections.forEach((coll) => this.entityIDs.add(`collection-${coll.id}`))
|
||||
requests.forEach((req) => this.entityIDs.add(`request-${req.id}`))
|
||||
|
||||
this.collections$.next(tree)
|
||||
} finally {
|
||||
this.loadingCollections$.next(
|
||||
this.loadingCollections$.getValue().filter((x) => x !== collectionID)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ export default class TeamListAdapter {
|
||||
|
||||
public isInitialized: boolean
|
||||
|
||||
constructor(deferInit = false) {
|
||||
constructor(
|
||||
deferInit = false,
|
||||
private doPolling = true
|
||||
) {
|
||||
this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
|
||||
this.loading$ = new BehaviorSubject<boolean>(false)
|
||||
this.teamList$ = new BehaviorSubject<GetMyTeamsQuery["myTeams"]>([])
|
||||
@@ -38,7 +41,7 @@ export default class TeamListAdapter {
|
||||
const func = async () => {
|
||||
await this.fetchList()
|
||||
|
||||
if (!this.isDispose) {
|
||||
if (!this.isDispose && this.doPolling) {
|
||||
this.timeoutHandle = setTimeout(() => func(), POLL_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { distinctUntilChanged, pluck } from "rxjs"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
|
||||
type Workspace =
|
||||
| { type: "personal" }
|
||||
| { type: "team"; teamID: string; teamName: string }
|
||||
|
||||
type WorkspaceState = {
|
||||
workspace: Workspace
|
||||
}
|
||||
|
||||
const initialState: WorkspaceState = {
|
||||
workspace: {
|
||||
type: "personal",
|
||||
},
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
changeWorkspace(_, { workspace }: { workspace: Workspace }) {
|
||||
return {
|
||||
workspace,
|
||||
}
|
||||
},
|
||||
updateWorkspaceTeamName(
|
||||
_,
|
||||
{ workspace, newTeamName }: { workspace: Workspace; newTeamName: string }
|
||||
) {
|
||||
if (workspace.type === "team") {
|
||||
return {
|
||||
workspace: {
|
||||
...workspace,
|
||||
teamName: newTeamName,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
workspace,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const hoppWorkspaceStore = new DispatchingStore(
|
||||
initialState,
|
||||
dispatchers
|
||||
)
|
||||
|
||||
export const workspaceStatus$ = hoppWorkspaceStore.subject$.pipe(
|
||||
pluck("workspace"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export function changeWorkspace(workspace: Workspace) {
|
||||
hoppWorkspaceStore.dispatch({
|
||||
dispatcher: "changeWorkspace",
|
||||
payload: { workspace },
|
||||
})
|
||||
}
|
||||
|
||||
export function updateWorkspaceTeamName(
|
||||
workspace: Workspace,
|
||||
newTeamName: string
|
||||
) {
|
||||
hoppWorkspaceStore.dispatch({
|
||||
dispatcher: "updateWorkspaceTeamName",
|
||||
payload: { workspace, newTeamName },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import { describe, expect, vi, it, beforeEach, afterEach } from "vitest"
|
||||
import { TestContainer } from "dioc/testing"
|
||||
import { WorkspaceService } from "../workspace.service"
|
||||
import { setPlatformDef } from "~/platform"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { effectScope, nextTick } from "vue"
|
||||
|
||||
const listAdapterMock = vi.hoisted(() => ({
|
||||
isInitialized: false,
|
||||
initialize: vi.fn(() => {
|
||||
listAdapterMock.isInitialized = true
|
||||
}),
|
||||
dispose: vi.fn(() => {
|
||||
listAdapterMock.isInitialized = false
|
||||
}),
|
||||
fetchList: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("~/helpers/teams/TeamListAdapter", () => ({
|
||||
default: class {
|
||||
isInitialized = listAdapterMock.isInitialized
|
||||
initialize = listAdapterMock.initialize
|
||||
dispose = listAdapterMock.dispose
|
||||
fetchList = listAdapterMock.fetchList
|
||||
},
|
||||
}))
|
||||
|
||||
describe("WorkspaceService", () => {
|
||||
const platformMock = {
|
||||
auth: {
|
||||
getCurrentUserStream: vi.fn(),
|
||||
getCurrentUser: vi.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error - We're mocking the platform
|
||||
setPlatformDef(platformMock)
|
||||
|
||||
platformMock.auth.getCurrentUserStream.mockReturnValue(
|
||||
new BehaviorSubject(null)
|
||||
)
|
||||
|
||||
platformMock.auth.getCurrentUser.mockReturnValue(null)
|
||||
})
|
||||
|
||||
describe("Initialization", () => {
|
||||
it("should initialize with the personal workspace selected", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(WorkspaceService)
|
||||
|
||||
expect(service.currentWorkspace.value).toEqual({ type: "personal" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateWorkspaceTeamName", () => {
|
||||
it("should update the workspace team name if the current workspace is a team workspace", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(WorkspaceService)
|
||||
|
||||
service.changeWorkspace({
|
||||
type: "team",
|
||||
teamID: "test",
|
||||
teamName: "before update",
|
||||
})
|
||||
|
||||
service.updateWorkspaceTeamName("test")
|
||||
|
||||
expect(service.currentWorkspace.value).toEqual({
|
||||
type: "team",
|
||||
teamID: "test",
|
||||
teamName: "test",
|
||||
})
|
||||
})
|
||||
|
||||
it("should not update the workspace team name if the current workspace is a personal workspace", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(WorkspaceService)
|
||||
|
||||
service.changeWorkspace({
|
||||
type: "personal",
|
||||
})
|
||||
|
||||
service.updateWorkspaceTeamName("test")
|
||||
|
||||
expect(service.currentWorkspace.value).toEqual({ type: "personal" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("changeWorkspace", () => {
|
||||
it("updates the current workspace value to the given workspace", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(WorkspaceService)
|
||||
|
||||
service.changeWorkspace({
|
||||
type: "team",
|
||||
teamID: "test",
|
||||
teamName: "test",
|
||||
})
|
||||
|
||||
expect(service.currentWorkspace.value).toEqual({
|
||||
type: "team",
|
||||
teamID: "test",
|
||||
teamName: "test",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("acquireTeamListAdapter", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
listAdapterMock.fetchList.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers()
|
||||
})
|
||||
|
||||
it("should not poll if the polling time is null", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
listAdapterMock.isInitialized = true // We need to initialize the list adapter before we can use it
|
||||
const service = container.bind(WorkspaceService)
|
||||
|
||||
service.acquireTeamListAdapter(null)
|
||||
vi.advanceTimersByTime(100000)
|
||||
|
||||
expect(listAdapterMock.fetchList).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not poll if the polling time is not null and user not logged in", async () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(WorkspaceService)
|
||||
|
||||
service.acquireTeamListAdapter(100)
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(110)
|
||||
|
||||
platformMock.auth.getCurrentUser.mockReturnValue(null)
|
||||
platformMock.auth.getCurrentUserStream.mockReturnValue(
|
||||
new BehaviorSubject(null)
|
||||
)
|
||||
|
||||
expect(listAdapterMock.fetchList).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should poll if the polling time is not null and the user is logged in", async () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
listAdapterMock.isInitialized = true // We need to initialize the list adapter before we can use it
|
||||
|
||||
platformMock.auth.getCurrentUser.mockReturnValue({
|
||||
id: "test",
|
||||
})
|
||||
platformMock.auth.getCurrentUserStream.mockReturnValue(
|
||||
new BehaviorSubject({ id: "test" })
|
||||
)
|
||||
|
||||
const service = container.bind(WorkspaceService)
|
||||
|
||||
const adapter = service.acquireTeamListAdapter(100)
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(100)
|
||||
|
||||
expect(adapter!.fetchList).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it("emits 'managed-team-list-adapter-polled' when the service polls the adapter", async () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
listAdapterMock.isInitialized = true
|
||||
|
||||
platformMock.auth.getCurrentUser.mockReturnValue({
|
||||
id: "test",
|
||||
})
|
||||
|
||||
platformMock.auth.getCurrentUserStream.mockReturnValue(
|
||||
new BehaviorSubject({ id: "test" })
|
||||
)
|
||||
|
||||
const service = container.bind(WorkspaceService)
|
||||
|
||||
const eventFn = vi.fn()
|
||||
const sub = service.getEventStream().subscribe(eventFn)
|
||||
|
||||
service.acquireTeamListAdapter(100)
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(100)
|
||||
|
||||
expect(eventFn).toHaveBeenCalledOnce()
|
||||
expect(eventFn).toHaveBeenCalledWith({
|
||||
type: "managed-team-list-adapter-polled",
|
||||
})
|
||||
|
||||
sub.unsubscribe()
|
||||
})
|
||||
|
||||
it("stops polling when the Vue effect scope is disposed and there is no more polling locks", async () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
listAdapterMock.isInitialized = true
|
||||
|
||||
platformMock.auth.getCurrentUser.mockReturnValue({
|
||||
id: "test",
|
||||
})
|
||||
|
||||
platformMock.auth.getCurrentUserStream.mockReturnValue(
|
||||
new BehaviorSubject({ id: "test" })
|
||||
)
|
||||
|
||||
const service = container.bind(WorkspaceService)
|
||||
listAdapterMock.fetchList.mockClear() // Reset the counters
|
||||
|
||||
const scopeHandle = effectScope()
|
||||
scopeHandle.run(() => {
|
||||
service.acquireTeamListAdapter(100)
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(100)
|
||||
|
||||
expect(listAdapterMock.fetchList).toHaveBeenCalledOnce()
|
||||
listAdapterMock.fetchList.mockClear()
|
||||
|
||||
scopeHandle.stop()
|
||||
|
||||
await nextTick()
|
||||
vi.advanceTimersByTime(100)
|
||||
|
||||
expect(listAdapterMock.fetchList).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -25,8 +25,7 @@ import {
|
||||
HoppGQLRequest,
|
||||
HoppRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { hoppWorkspaceStore } from "~/newstore/workspace"
|
||||
import { changeWorkspace } from "~/newstore/workspace"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
/**
|
||||
@@ -46,6 +45,7 @@ export class CollectionsSpotlightSearcherService
|
||||
public searcherSectionTitle = this.t("collection.my_collections")
|
||||
|
||||
private readonly spotlight = this.bind(SpotlightService)
|
||||
private readonly workspaceService = this.bind(WorkspaceService)
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
@@ -284,8 +284,8 @@ export class CollectionsSpotlightSearcherService
|
||||
const folderPath = path.split("/").map((x) => parseInt(x))
|
||||
const reqIndex = folderPath.pop()!
|
||||
|
||||
if (hoppWorkspaceStore.value.workspace.type !== "personal") {
|
||||
changeWorkspace({
|
||||
if (this.workspaceService.currentWorkspace.value.type !== "personal") {
|
||||
this.workspaceService.changeWorkspace({
|
||||
type: "personal",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,16 +25,15 @@ import { Service } from "dioc"
|
||||
import * as E from "fp-ts/Either"
|
||||
import MiniSearch from "minisearch"
|
||||
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
|
||||
import { useStreamStatic } from "~/composables/stream"
|
||||
import { runGQLQuery } from "~/helpers/backend/GQLClient"
|
||||
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import { platform } from "~/platform"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconUsers from "~icons/lucide/users"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
type Doc = {
|
||||
text: string | string[]
|
||||
@@ -58,14 +57,9 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
|
||||
public searcherSectionTitle = this.t("spotlight.workspace.title")
|
||||
|
||||
private readonly spotlight = this.bind(SpotlightService)
|
||||
private readonly workspaceService = this.bind(WorkspaceService)
|
||||
|
||||
private workspace = useStreamStatic(
|
||||
workspaceStatus$,
|
||||
{ type: "personal" },
|
||||
() => {
|
||||
/* noop */
|
||||
}
|
||||
)[0]
|
||||
private workspace = this.workspaceService.currentWorkspace
|
||||
|
||||
private isTeamSelected = computed(
|
||||
() =>
|
||||
@@ -170,6 +164,7 @@ export class SwitchWorkspaceSpotlightSearcherService
|
||||
public searcherSectionTitle = this.t("workspace.title")
|
||||
|
||||
private readonly spotlight = this.bind(SpotlightService)
|
||||
private readonly workspaceService = this.bind(WorkspaceService)
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
@@ -197,13 +192,7 @@ export class SwitchWorkspaceSpotlightSearcherService
|
||||
})
|
||||
}
|
||||
|
||||
private workspace = useStreamStatic(
|
||||
workspaceStatus$,
|
||||
{ type: "personal" },
|
||||
() => {
|
||||
/* noop */
|
||||
}
|
||||
)[0]
|
||||
private workspace = this.workspaceService.currentWorkspace
|
||||
|
||||
createSearchSession(
|
||||
query: Readonly<Ref<string>>
|
||||
|
||||
135
packages/hoppscotch-common/src/services/workspace.service.ts
Normal file
135
packages/hoppscotch-common/src/services/workspace.service.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { tryOnScopeDispose, useIntervalFn } from "@vueuse/core"
|
||||
import { Service } from "dioc"
|
||||
import { computed, reactive, ref, watch, readonly } from "vue"
|
||||
import { useStreamStatic } from "~/composables/stream"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { platform } from "~/platform"
|
||||
import { min } from "lodash-es"
|
||||
|
||||
/**
|
||||
* Defines a workspace and its information
|
||||
*/
|
||||
export type Workspace =
|
||||
| { type: "personal" }
|
||||
| { type: "team"; teamID: string; teamName: string }
|
||||
|
||||
export type WorkspaceServiceEvent = {
|
||||
type: "managed-team-list-adapter-polled"
|
||||
}
|
||||
|
||||
/**
|
||||
* This services manages workspace related data and actions in Hoppscotch.
|
||||
*/
|
||||
export class WorkspaceService extends Service<WorkspaceServiceEvent> {
|
||||
public static readonly ID = "WORKSPACE_SERVICE"
|
||||
|
||||
private _currentWorkspace = ref<Workspace>({ type: "personal" })
|
||||
|
||||
/**
|
||||
* A readonly reference to the currently selected workspace
|
||||
*/
|
||||
public currentWorkspace = readonly(this._currentWorkspace)
|
||||
|
||||
private teamListAdapterLocks = reactive(new Map<number, number | null>())
|
||||
private teamListAdapterLockTicker = 0 // Used to generate unique lock IDs
|
||||
private managedTeamListAdapter = new TeamListAdapter(true, false)
|
||||
|
||||
private currentUser = useStreamStatic(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser(),
|
||||
() => {
|
||||
/* noop */
|
||||
}
|
||||
)[0]
|
||||
|
||||
private readonly pollingTime = computed(
|
||||
() =>
|
||||
min(Array.from(this.teamListAdapterLocks.values()).filter((x) => !!x)) ??
|
||||
-1
|
||||
)
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
// Dispose the managed team list adapter when the user logs out
|
||||
// and initialize it when the user logs in
|
||||
watch(
|
||||
this.currentUser,
|
||||
(user) => {
|
||||
if (!user && this.managedTeamListAdapter.isInitialized) {
|
||||
this.managedTeamListAdapter.dispose()
|
||||
}
|
||||
|
||||
if (user && !this.managedTeamListAdapter.isInitialized) {
|
||||
this.managedTeamListAdapter.initialize()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Poll the managed team list adapter if the polling time is defined
|
||||
const { pause: pauseListPoll, resume: resumeListPoll } = useIntervalFn(
|
||||
() => {
|
||||
if (this.managedTeamListAdapter.isInitialized) {
|
||||
this.managedTeamListAdapter.fetchList()
|
||||
|
||||
this.emit({ type: "managed-team-list-adapter-polled" })
|
||||
}
|
||||
},
|
||||
this.pollingTime,
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Pause and resume the polling when the polling time changes
|
||||
watch(
|
||||
this.pollingTime,
|
||||
(pollingTime) => {
|
||||
if (pollingTime === -1) {
|
||||
pauseListPoll()
|
||||
} else {
|
||||
resumeListPoll()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Update this function, its existence is pretty weird
|
||||
/**
|
||||
* Updates the name of the current workspace if it is a team workspace.
|
||||
* @param newTeamName The new name of the team
|
||||
*/
|
||||
public updateWorkspaceTeamName(newTeamName: string) {
|
||||
if (this._currentWorkspace.value.type === "team") {
|
||||
this._currentWorkspace.value = {
|
||||
...this._currentWorkspace.value,
|
||||
teamName: newTeamName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current workspace to the given workspace.
|
||||
* @param workspace The new workspace
|
||||
*/
|
||||
public changeWorkspace(workspace: Workspace) {
|
||||
this._currentWorkspace.value = workspace
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a team list adapter that is managed by the workspace service.
|
||||
* The team list adapter is associated with a Vue Scope and will be disposed
|
||||
* when the scope is disposed.
|
||||
* @param pollDuration The duration between polls in milliseconds. If null, the team list adapter will not poll.
|
||||
*/
|
||||
public acquireTeamListAdapter(pollDuration: number | null) {
|
||||
const lockID = this.teamListAdapterLockTicker++
|
||||
|
||||
this.teamListAdapterLocks.set(lockID, pollDuration)
|
||||
|
||||
tryOnScopeDispose(() => {
|
||||
this.teamListAdapterLocks.delete(lockID)
|
||||
})
|
||||
|
||||
return this.managedTeamListAdapter
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user