Revert "refactor: minor performance improvements on teams related operations (#3348)"

This reverts commit 887dac5285.
This commit is contained in:
Andrew Bastin
2023-09-18 18:47:10 +05:30
committed by GitHub
parent 887dac5285
commit de308f9cef
16 changed files with 202 additions and 520 deletions

View File

@@ -24,6 +24,7 @@ declare module 'vue' {
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default'] AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default'] AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.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'] AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default'] AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default'] AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
@@ -143,7 +144,6 @@ declare module 'vue' {
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default'] IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'] IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default'] IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default']

View File

@@ -249,11 +249,12 @@ import { platform } from "~/platform"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { defineActionHandler, invokeAction } from "@helpers/actions" 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 { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { getPlatformSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -281,11 +282,10 @@ const currentUser = useReadonlyStream(
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>() const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
// TeamList-Adapter // TeamList-Adapter
const workspaceService = useService(WorkspaceService) const teamListAdapter = new TeamListAdapter(true)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null) const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const workspace = workspaceService.currentWorkspace const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
const workspaceName = computed(() => const workspaceName = computed(() =>
workspace.value.type === "personal" workspace.value.type === "personal"
@@ -297,18 +297,20 @@ const refetchTeams = () => {
teamListAdapter.fetchList() teamListAdapter.fetchList()
} }
onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
watch( watch(
() => myTeams.value, () => myTeams.value,
(newTeams) => { (newTeams) => {
const space = workspace.value if (newTeams && workspace.value.type === "team" && workspace.value.teamID) {
const team = newTeams.find((team) => team.id === workspace.value.teamID)
if (newTeams && space.type === "team" && space.teamID) {
const team = newTeams.find((team) => team.id === space.teamID)
if (team) { if (team) {
selectedTeam.value = team selectedTeam.value = team
// Update the workspace name if it's not the same as the updated team name // Update the workspace name if it's not the same as the updated team name
if (team.name !== space.teamName) { if (team.name !== workspace.value.teamName) {
workspaceService.updateWorkspaceTeamName(team.name) updateWorkspaceTeamName(workspace.value, team.name)
} }
} }
} }

View File

@@ -162,8 +162,10 @@ import { computed, nextTick, PropType, ref, watch } from "vue"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useReadonlyStream } from "~/composables/stream" import { useReadonlyStream } from "~/composables/stream"
import { useLocalState } from "~/newstore/localstate" import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
@@ -219,6 +221,7 @@ import {
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { platform } from "~/platform" import { platform } from "~/platform"
import { createCollectionGists } from "~/helpers/gist" import { createCollectionGists } from "~/helpers/gist"
import { workspaceStatus$ } from "~/newstore/workspace"
import { import {
createNewTab, createNewTab,
currentActiveTab, currentActiveTab,
@@ -237,8 +240,6 @@ import {
} from "~/helpers/collection/collection" } from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering" import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -315,8 +316,7 @@ const creatingGistCollection = ref(false)
const importingMyCollections = ref(false) const importingMyCollections = ref(false)
// TeamList-Adapter // TeamList-Adapter
const workspaceService = useService(WorkspaceService) const teamListAdapter = new TeamListAdapter(true)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null) const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID") const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const teamListFetched = ref(false) const teamListFetched = ref(false)
@@ -374,18 +374,17 @@ const updateSelectedTeam = (team: SelectedTeam) => {
} }
} }
const workspace = workspaceService.currentWorkspace onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// Used to switch collection type and team when user switch workspace in the global workspace switcher // 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 // 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 // If there is no teamID, switch to my environment
watch( watch(
() => { () => workspace.value.teamID,
const space = workspace.value
if (space.type === "personal") return undefined
else return space.teamID
},
(teamID) => { (teamID) => {
if (!teamID) { if (!teamID) {
switchToMyCollections() switchToMyCollections()

View File

@@ -308,6 +308,7 @@ import {
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core" import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
@@ -315,10 +316,10 @@ import { invokeAction } from "~/helpers/actions"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment" import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { Environment } from "@hoppscotch/data" import { Environment } from "@hoppscotch/data"
import { onMounted } from "vue" import { onMounted } from "vue"
import { onLoggedIn } from "~/composables/auth"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate" import { useLocalState } from "~/newstore/localstate"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
type Scope = type Scope =
| { | {
@@ -352,18 +353,21 @@ type EnvironmentType = "my-environments" | "team-environments"
const myEnvironments = useReadonlyStream(environments$, []) const myEnvironments = useReadonlyStream(environments$, [])
const workspaceService = useService(WorkspaceService) const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
const workspace = workspaceService.currentWorkspace
// TeamList-Adapter // TeamList-Adapter
const teamListAdapter = workspaceService.acquireTeamListAdapter(null) const teamListAdapter = new TeamListAdapter(true)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null) const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false) const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID") const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => { const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
REMEMBERED_TEAM_ID.value = team.id REMEMBERED_TEAM_ID.value = team.id
workspaceService.changeWorkspace({ changeWorkspace({
teamID: team.id, teamID: team.id,
teamName: team.name, teamName: team.name,
type: "team", type: "team",

View File

@@ -58,15 +58,16 @@ import {
} from "~/newstore/environments" } from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate" import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { deleteEnvironment } from "~/newstore/environments" import { deleteEnvironment } from "~/newstore/environments"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment" import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -98,8 +99,7 @@ const currentUser = useReadonlyStream(
) )
// TeamList-Adapter // TeamList-Adapter
const workspaceService = useService(WorkspaceService) const teamListAdapter = new TeamListAdapter(true)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null) const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false) const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID") const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
@@ -152,7 +152,11 @@ watch(
} }
) )
const workspace = workspaceService.currentWorkspace onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// Switch to my environments if workspace is personal and to team environments if workspace is team // 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 // also resets selected environment if workspace is personal and the previous selected environment was a team environment

View File

@@ -216,8 +216,7 @@ import IconClose from "~icons/lucide/x"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { workspaceStatus$ } from "~/newstore/workspace"
import { WorkspaceService } from "~/services/workspace.service"
const props = defineProps<{ const props = defineProps<{
modelValue: HoppTestResult | null | undefined modelValue: HoppTestResult | null | undefined
@@ -232,8 +231,7 @@ const testResults = useVModel(props, "modelValue", emit)
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const workspaceService = useService(WorkspaceService) const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
const workspace = workspaceService.currentWorkspace
const showMyEnvironmentDetailsModal = ref(false) const showMyEnvironmentDetailsModal = ref(false)
const showTeamEnvironmentDetailsModal = ref(false) const showTeamEnvironmentDetailsModal = ref(false)

View File

@@ -69,11 +69,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { onLoggedIn } from "@composables/auth"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
@@ -89,8 +89,7 @@ const showModalInvite = ref(false)
const editingTeam = ref<any>({}) // TODO: Check this out const editingTeam = ref<any>({}) // TODO: Check this out
const editingTeamID = ref<any>("") const editingTeamID = ref<any>("")
const workspaceService = useService(WorkspaceService) const adapter = new TeamListAdapter(true)
const adapter = workspaceService.acquireTeamListAdapter(10000)
const adapterLoading = useReadonlyStream(adapter.loading$, false) const adapterLoading = useReadonlyStream(adapter.loading$, false)
const adapterError = useReadonlyStream(adapter.error$, null) const adapterError = useReadonlyStream(adapter.error$, null)
const myTeams = useReadonlyStream(adapter.teamList$, []) const myTeams = useReadonlyStream(adapter.teamList$, [])
@@ -99,6 +98,12 @@ const loading = computed(
() => adapterLoading.value && myTeams.value.length === 0 () => adapterLoading.value && myTeams.value.length === 0
) )
onLoggedIn(() => {
try {
adapter.initialize()
} catch (e) {}
})
const displayModalAdd = (shouldDisplay: boolean) => { const displayModalAdd = (shouldDisplay: boolean) => {
showModalAdd.value = shouldDisplay showModalAdd.value = shouldDisplay
adapter.fetchList() adapter.fetchList()

View File

@@ -16,9 +16,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue" import { computed } from "vue"
import { useReadonlyStream } from "~/composables/stream"
import { workspaceStatus$ } from "~/newstore/workspace"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
defineProps<{ defineProps<{
section?: string section?: string
@@ -26,8 +26,7 @@ defineProps<{
const t = useI18n() const t = useI18n()
const workspaceService = useService(WorkspaceService) const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
const workspace = workspaceService.currentWorkspace
const teamWorkspaceName = computed(() => { const teamWorkspaceName = computed(() => {
if (workspace.value.type === "team" && workspace.value.teamName) { if (workspace.value.type === "team" && workspace.value.teamName) {

View File

@@ -1,5 +1,5 @@
<template> <template>
<div ref="rootEl"> <div>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <div class="flex flex-col">
<HoppSmartItem <HoppSmartItem
@@ -69,20 +69,20 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { onLoggedIn } from "~/composables/auth"
import { useReadonlyStream } from "~/composables/stream" import { useReadonlyStream } from "~/composables/stream"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import IconUser from "~icons/lucide/user" import IconUser from "~icons/lucide/user"
import IconUsers from "~icons/lucide/users" import IconUsers from "~icons/lucide/users"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconDone from "~icons/lucide/check" import IconDone from "~icons/lucide/check"
import { useLocalState } from "~/newstore/localstate" import { useLocalState } from "~/newstore/localstate"
import { defineActionHandler } from "~/helpers/actions" 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 t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -94,37 +94,13 @@ const currentUser = useReadonlyStream(
platform.auth.getProbableUser() platform.auth.getProbableUser()
) )
const workspaceService = useService(WorkspaceService) const teamListadapter = new TeamListAdapter(true)
const teamListadapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListadapter.teamList$, []) const myTeams = useReadonlyStream(teamListadapter.teamList$, [])
const isTeamListLoading = useReadonlyStream(teamListadapter.loading$, false) const isTeamListLoading = useReadonlyStream(teamListadapter.loading$, false)
const teamListAdapterError = useReadonlyStream(teamListadapter.error$, null) const teamListAdapterError = useReadonlyStream(teamListadapter.error$, null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID") const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const teamListFetched = ref(false) 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) => { watch(myTeams, (teams) => {
if (teams && !teamListFetched.value) { if (teams && !teamListFetched.value) {
teamListFetched.value = true teamListFetched.value = true
@@ -139,7 +115,7 @@ const loading = computed(
() => isTeamListLoading.value && myTeams.value.length === 0 () => isTeamListLoading.value && myTeams.value.length === 0
) )
const workspace = workspaceService.currentWorkspace const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
const isActiveWorkspace = computed(() => (id: string) => { const isActiveWorkspace = computed(() => (id: string) => {
if (workspace.value.type === "personal") return false if (workspace.value.type === "personal") return false
@@ -148,7 +124,7 @@ const isActiveWorkspace = computed(() => (id: string) => {
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => { const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
REMEMBERED_TEAM_ID.value = team.id REMEMBERED_TEAM_ID.value = team.id
workspaceService.changeWorkspace({ changeWorkspace({
teamID: team.id, teamID: team.id,
teamName: team.name, teamName: team.name,
type: "team", type: "team",
@@ -157,11 +133,15 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
const switchToPersonalWorkspace = () => { const switchToPersonalWorkspace = () => {
REMEMBERED_TEAM_ID.value = undefined REMEMBERED_TEAM_ID.value = undefined
workspaceService.changeWorkspace({ changeWorkspace({
type: "personal", type: "personal",
}) })
} }
onLoggedIn(() => {
teamListadapter.initialize()
})
watch( watch(
() => currentUser.value, () => currentUser.value,
(user) => { (user) => {

View File

@@ -902,16 +902,36 @@ export default class NewTeamCollectionAdapter {
) )
} }
private async getCollectionChildren( /**
collection: TeamCollection * Expands a collection on the tree
): Promise<TeamCollection[]> { *
* 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
const collections: TeamCollection[] = [] const collections: TeamCollection[] = []
this.loadingCollections$.next([
...this.loadingCollections$.getValue(),
collectionID,
])
while (true) { while (true) {
const data = await runGQLQuery({ const data = await runGQLQuery({
query: GetCollectionChildrenDocument, query: GetCollectionChildrenDocument,
variables: { variables: {
collectionID: collection.id, collectionID,
cursor: cursor:
collections.length > 0 collections.length > 0
? collections[collections.length - 1].id ? collections[collections.length - 1].id
@@ -920,8 +940,12 @@ export default class NewTeamCollectionAdapter {
}) })
if (E.isLeft(data)) { if (E.isLeft(data)) {
this.loadingCollections$.next(
this.loadingCollections$.getValue().filter((x) => x !== collectionID)
)
throw new Error( throw new Error(
`Child Collection Fetch Error for ${collection.id}: ${data.left}` `Child Collection Fetch Error for ${collectionID}: ${data.left}`
) )
} }
@@ -941,25 +965,23 @@ export default class NewTeamCollectionAdapter {
break break
} }
return collections
}
private async getCollectionRequests(
collection: TeamCollection
): Promise<TeamRequest[]> {
const requests: TeamRequest[] = [] const requests: TeamRequest[] = []
while (true) { while (true) {
const data = await runGQLQuery({ const data = await runGQLQuery({
query: GetCollectionRequestsDocument, query: GetCollectionRequestsDocument,
variables: { variables: {
collectionID: collection.id, collectionID,
cursor: cursor:
requests.length > 0 ? requests[requests.length - 1].id : undefined, requests.length > 0 ? requests[requests.length - 1].id : undefined,
}, },
}) })
if (E.isLeft(data)) { 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}`) throw new Error(`Child Request Fetch Error for ${data}: ${data.left}`)
} }
@@ -967,7 +989,7 @@ export default class NewTeamCollectionAdapter {
...data.right.requestsInCollection.map<TeamRequest>((el) => { ...data.right.requestsInCollection.map<TeamRequest>((el) => {
return { return {
id: el.id, id: el.id,
collectionID: collection.id, collectionID,
title: el.title, title: el.title,
request: translateToNewRequest(JSON.parse(el.request)), request: translateToNewRequest(JSON.parse(el.request)),
} }
@@ -978,50 +1000,17 @@ export default class NewTeamCollectionAdapter {
break break
} }
return requests collection.children = collections
} collection.requests = requests
/** // Add to the entity ids set
* Expands a collection on the tree collections.forEach((coll) => this.entityIDs.add(`collection-${coll.id}`))
* requests.forEach((req) => this.entityIDs.add(`request-${req.id}`))
* 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) this.loadingCollections$.next(
this.loadingCollections$.getValue().filter((x) => x !== collectionID)
)
if (!collection) return this.collections$.next(tree)
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)
)
}
} }
} }

View File

@@ -17,10 +17,7 @@ export default class TeamListAdapter {
public isInitialized: boolean public isInitialized: boolean
constructor( constructor(deferInit = false) {
deferInit = false,
private doPolling = true
) {
this.error$ = new BehaviorSubject<GQLError<string> | null>(null) this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
this.loading$ = new BehaviorSubject<boolean>(false) this.loading$ = new BehaviorSubject<boolean>(false)
this.teamList$ = new BehaviorSubject<GetMyTeamsQuery["myTeams"]>([]) this.teamList$ = new BehaviorSubject<GetMyTeamsQuery["myTeams"]>([])
@@ -41,7 +38,7 @@ export default class TeamListAdapter {
const func = async () => { const func = async () => {
await this.fetchList() await this.fetchList()
if (!this.isDispose && this.doPolling) { if (!this.isDispose) {
this.timeoutHandle = setTimeout(() => func(), POLL_DURATION) this.timeoutHandle = setTimeout(() => func(), POLL_DURATION)
} }
} }

View File

@@ -0,0 +1,67 @@
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 },
})
}

View File

@@ -1,238 +0,0 @@
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()
})
})
})

View File

@@ -25,7 +25,8 @@ import {
HoppGQLRequest, HoppGQLRequest,
HoppRESTRequest, HoppRESTRequest,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import { WorkspaceService } from "~/services/workspace.service" import { hoppWorkspaceStore } from "~/newstore/workspace"
import { changeWorkspace } from "~/newstore/workspace"
import { invokeAction } from "~/helpers/actions" import { invokeAction } from "~/helpers/actions"
/** /**
@@ -45,7 +46,6 @@ export class CollectionsSpotlightSearcherService
public searcherSectionTitle = this.t("collection.my_collections") public searcherSectionTitle = this.t("collection.my_collections")
private readonly spotlight = this.bind(SpotlightService) private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
constructor() { constructor() {
super() super()
@@ -284,8 +284,8 @@ export class CollectionsSpotlightSearcherService
const folderPath = path.split("/").map((x) => parseInt(x)) const folderPath = path.split("/").map((x) => parseInt(x))
const reqIndex = folderPath.pop()! const reqIndex = folderPath.pop()!
if (this.workspaceService.currentWorkspace.value.type !== "personal") { if (hoppWorkspaceStore.value.workspace.type !== "personal") {
this.workspaceService.changeWorkspace({ changeWorkspace({
type: "personal", type: "personal",
}) })
} }

View File

@@ -25,15 +25,16 @@ import { Service } from "dioc"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import MiniSearch from "minisearch" import MiniSearch from "minisearch"
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue" import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
import { useStreamStatic } from "~/composables/stream"
import { runGQLQuery } from "~/helpers/backend/GQLClient" import { runGQLQuery } from "~/helpers/backend/GQLClient"
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { workspaceStatus$ } from "~/newstore/workspace"
import { platform } from "~/platform" import { platform } from "~/platform"
import IconEdit from "~icons/lucide/edit" import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconUser from "~icons/lucide/user" import IconUser from "~icons/lucide/user"
import IconUserPlus from "~icons/lucide/user-plus" import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users" import IconUsers from "~icons/lucide/users"
import { WorkspaceService } from "~/services/workspace.service"
type Doc = { type Doc = {
text: string | string[] text: string | string[]
@@ -57,9 +58,14 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
public searcherSectionTitle = this.t("spotlight.workspace.title") public searcherSectionTitle = this.t("spotlight.workspace.title")
private readonly spotlight = this.bind(SpotlightService) private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
private workspace = this.workspaceService.currentWorkspace private workspace = useStreamStatic(
workspaceStatus$,
{ type: "personal" },
() => {
/* noop */
}
)[0]
private isTeamSelected = computed( private isTeamSelected = computed(
() => () =>
@@ -164,7 +170,6 @@ export class SwitchWorkspaceSpotlightSearcherService
public searcherSectionTitle = this.t("workspace.title") public searcherSectionTitle = this.t("workspace.title")
private readonly spotlight = this.bind(SpotlightService) private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
constructor() { constructor() {
super() super()
@@ -192,7 +197,13 @@ export class SwitchWorkspaceSpotlightSearcherService
}) })
} }
private workspace = this.workspaceService.currentWorkspace private workspace = useStreamStatic(
workspaceStatus$,
{ type: "personal" },
() => {
/* noop */
}
)[0]
createSearchSession( createSearchSession(
query: Readonly<Ref<string>> query: Readonly<Ref<string>>

View File

@@ -1,135 +0,0 @@
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
}
}