From a568610c28209423bc57c8cae8dd85c3fd170f3f Mon Sep 17 00:00:00 2001 From: Nivedin <53208152+nivedin@users.noreply.github.com> Date: Fri, 7 Oct 2022 22:05:39 +0530 Subject: [PATCH] feat: team environments (#2512) Co-authored-by: amk-dev Co-authored-by: liyasthomas Co-authored-by: islamzeki Co-authored-by: Jesvin Jose Co-authored-by: Andrew Bastin --- .../hoppscotch-app/assets/scss/styles.scss | 9 + packages/hoppscotch-app/locales/en.json | 7 + packages/hoppscotch-app/src/components.d.ts | 10 +- .../components/environments/ChooseType.vue | 149 +++++++ .../components/environments/ImportExport.vue | 107 ++++- .../src/components/environments/index.vue | 373 ++++++++++++------ .../environments/{ => my}/Details.vue | 42 +- .../environments/{ => my}/Environment.vue | 26 +- .../src/components/environments/my/index.vue | 121 ++++++ .../components/environments/teams/Details.vue | 340 ++++++++++++++++ .../environments/teams/Environment.vue | 176 +++++++++ .../components/environments/teams/index.vue | 177 +++++++++ .../src/components/http/TestResult.vue | 16 +- .../src/helpers/RequestRunner.ts | 31 +- .../CreateDuplicateEnvironment.graphql | 8 + .../mutations/CreateTeamEnvironment.graphql | 7 + .../mutations/DeleteTeamEnvironment.graphql | 3 + .../mutations/UpdateTeamEnvironment.graphql | 7 + .../gql/queries/GetTeamEnvironments.graphql | 10 + .../TeamEnvironmentCreated.graphql | 8 + .../TeamEnvironmentDeleted.graphql | 5 + .../TeamEnvironmentUpdated.graphql | 8 + .../backend/mutations/TeamEnvironment.ts | 69 ++++ .../editor/extensions/HoppEnvironment.ts | 10 + .../src/helpers/teams/TeamEnvironment.ts | 10 + .../helpers/teams/TeamEnvironmentAdapter.ts | 238 +++++++++++ .../src/newstore/environments.ts | 121 ++++-- .../src/newstore/localpersistence.ts | 25 +- 28 files changed, 1886 insertions(+), 227 deletions(-) create mode 100644 packages/hoppscotch-app/src/components/environments/ChooseType.vue rename packages/hoppscotch-app/src/components/environments/{ => my}/Details.vue (91%) rename packages/hoppscotch-app/src/components/environments/{ => my}/Environment.vue (84%) create mode 100644 packages/hoppscotch-app/src/components/environments/my/index.vue create mode 100644 packages/hoppscotch-app/src/components/environments/teams/Details.vue create mode 100644 packages/hoppscotch-app/src/components/environments/teams/Environment.vue create mode 100644 packages/hoppscotch-app/src/components/environments/teams/index.vue create mode 100644 packages/hoppscotch-app/src/helpers/backend/gql/mutations/CreateDuplicateEnvironment.graphql create mode 100644 packages/hoppscotch-app/src/helpers/backend/gql/mutations/CreateTeamEnvironment.graphql create mode 100644 packages/hoppscotch-app/src/helpers/backend/gql/mutations/DeleteTeamEnvironment.graphql create mode 100644 packages/hoppscotch-app/src/helpers/backend/gql/mutations/UpdateTeamEnvironment.graphql create mode 100644 packages/hoppscotch-app/src/helpers/backend/gql/queries/GetTeamEnvironments.graphql create mode 100644 packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentCreated.graphql create mode 100644 packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentDeleted.graphql create mode 100644 packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentUpdated.graphql create mode 100644 packages/hoppscotch-app/src/helpers/backend/mutations/TeamEnvironment.ts create mode 100644 packages/hoppscotch-app/src/helpers/teams/TeamEnvironment.ts create mode 100644 packages/hoppscotch-app/src/helpers/teams/TeamEnvironmentAdapter.ts diff --git a/packages/hoppscotch-app/assets/scss/styles.scss b/packages/hoppscotch-app/assets/scss/styles.scss index eb6ca56cf..a49805754 100644 --- a/packages/hoppscotch-app/assets/scss/styles.scss +++ b/packages/hoppscotch-app/assets/scss/styles.scss @@ -122,6 +122,7 @@ a { .cm-tooltip { .tippy-box { @apply fixed; + @apply inline-flex; @apply -mt-6; } } @@ -135,6 +136,7 @@ a { @apply truncate; @apply shadow; @apply leading-body; + @apply items-center; font-size: 86%; kbd { @@ -152,6 +154,13 @@ a { .tippy-svg-arrow svg { @apply fill-tooltip; } + + .env-icon { + @apply inline-flex; + @apply items-center; + @apply mr-1; + @apply text-accentDark; + } } .tippy-box[data-theme="popover"] { diff --git a/packages/hoppscotch-app/locales/en.json b/packages/hoppscotch-app/locales/en.json index 8edb4b346..11dc1af47 100644 --- a/packages/hoppscotch-app/locales/en.json +++ b/packages/hoppscotch-app/locales/en.json @@ -184,11 +184,13 @@ "deleted": "Environment deletion", "edit": "Edit Environment", "invalid_name": "Please provide a name for the environment", + "my_environments":"My Environments", "nested_overflow": "nested environment variables are limited to 10 levels", "new": "New Environment", "no_environment": "No environment", "no_environment_description": "No environments were selected. Choose what to do with the following variables.", "select": "Select environment", + "team_environments": "Team Environments", "title": "Environments", "updated": "Environment updated", "variable_list": "Variable List" @@ -653,6 +655,11 @@ "we_sent_invite_link": "We sent an invite link to all invitees!", "we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team." }, + "team_environment": { + "deleted": "Environment Deleted", + "duplicate": "Environment Duplicated", + "not_found": "Environment not found." + }, "test": { "failed": "test failed", "javascript_code": "JavaScript Code", diff --git a/packages/hoppscotch-app/src/components.d.ts b/packages/hoppscotch-app/src/components.d.ts index 2daecbc6c..61eff4281 100644 --- a/packages/hoppscotch-app/src/components.d.ts +++ b/packages/hoppscotch-app/src/components.d.ts @@ -55,9 +55,14 @@ declare module '@vue/runtime-core' { CollectionsTeamsFolder: typeof import('./components/collections/teams/Folder.vue')['default'] CollectionsTeamsRequest: typeof import('./components/collections/teams/Request.vue')['default'] Environments: typeof import('./components/environments/index.vue')['default'] - EnvironmentsDetails: typeof import('./components/environments/Details.vue')['default'] - EnvironmentsEnvironment: typeof import('./components/environments/Environment.vue')['default'] + EnvironmentsChooseType: typeof import('./components/environments/ChooseType.vue')['default'] EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default'] + EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default'] + EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default'] + EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default'] + EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default'] + EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default'] + EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default'] FirebaseLogin: typeof import('./components/firebase/Login.vue')['default'] FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default'] GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default'] @@ -104,7 +109,6 @@ declare module '@vue/runtime-core' { IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideUser: typeof import('~icons/lucide/user')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default'] - IconLucideVerified: typeof import('~icons/lucide/verified')['default'] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default'] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default'] LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default'] diff --git a/packages/hoppscotch-app/src/components/environments/ChooseType.vue b/packages/hoppscotch-app/src/components/environments/ChooseType.vue new file mode 100644 index 000000000..01b96a764 --- /dev/null +++ b/packages/hoppscotch-app/src/components/environments/ChooseType.vue @@ -0,0 +1,149 @@ + + + diff --git a/packages/hoppscotch-app/src/components/environments/ImportExport.vue b/packages/hoppscotch-app/src/components/environments/ImportExport.vue index 48cb463a9..0eaefd97b 100644 --- a/packages/hoppscotch-app/src/components/environments/ImportExport.vue +++ b/packages/hoppscotch-app/src/components/environments/ImportExport.vue @@ -12,7 +12,7 @@ interactive trigger="click" theme="popover" - :on-shown="() => tippyActions.focus()" + :on-shown="() => tippyActions!.focus()" > -
- -
+ + -
-
- -
- - + + +
-
- - - {{ t("empty.environments") }} - - -
- - + diff --git a/packages/hoppscotch-app/src/components/environments/Details.vue b/packages/hoppscotch-app/src/components/environments/my/Details.vue similarity index 91% rename from packages/hoppscotch-app/src/components/environments/Details.vue rename to packages/hoppscotch-app/src/components/environments/my/Details.vue index 5cb8ff41d..92be74f7b 100644 --- a/packages/hoppscotch-app/src/components/environments/Details.vue +++ b/packages/hoppscotch-app/src/components/environments/my/Details.vue @@ -101,18 +101,12 @@ @@ -120,7 +114,7 @@ diff --git a/packages/hoppscotch-app/src/components/environments/teams/Details.vue b/packages/hoppscotch-app/src/components/environments/teams/Details.vue new file mode 100644 index 000000000..e4a83943e --- /dev/null +++ b/packages/hoppscotch-app/src/components/environments/teams/Details.vue @@ -0,0 +1,340 @@ + + + diff --git a/packages/hoppscotch-app/src/components/environments/teams/Environment.vue b/packages/hoppscotch-app/src/components/environments/teams/Environment.vue new file mode 100644 index 000000000..cc4796451 --- /dev/null +++ b/packages/hoppscotch-app/src/components/environments/teams/Environment.vue @@ -0,0 +1,176 @@ + + + diff --git a/packages/hoppscotch-app/src/components/environments/teams/index.vue b/packages/hoppscotch-app/src/components/environments/teams/index.vue new file mode 100644 index 000000000..f37b9a386 --- /dev/null +++ b/packages/hoppscotch-app/src/components/environments/teams/index.vue @@ -0,0 +1,177 @@ + + + diff --git a/packages/hoppscotch-app/src/components/http/TestResult.vue b/packages/hoppscotch-app/src/components/http/TestResult.vue index c3d5d2317..29a459682 100644 --- a/packages/hoppscotch-app/src/components/http/TestResult.vue +++ b/packages/hoppscotch-app/src/components/http/TestResult.vue @@ -194,7 +194,7 @@ class="my-4" /> - { }) const selectedEnvironmentIndex = useStream( - selectedEnvIndex$, - -1, - setCurrentEnvironment + selectedEnvironmentIndex$, + { type: "NO_ENV_SELECTED" }, + setSelectedEnvironmentIndex ) const globalEnvVars = useReadonlyStream(globalEnv$, []) as Ref< @@ -275,7 +275,9 @@ const globalEnvVars = useReadonlyStream(globalEnv$, []) as Ref< }> > -const noEnvSelected = computed(() => selectedEnvironmentIndex.value === -1) +const noEnvSelected = computed( + () => selectedEnvironmentIndex.value.type === "NO_ENV_SELECTED" +) const globalHasAdditions = computed(() => { if (!testResults.value?.envDiff.selected.additions) return false diff --git a/packages/hoppscotch-app/src/helpers/RequestRunner.ts b/packages/hoppscotch-app/src/helpers/RequestRunner.ts index c571ca5f7..4b6144d81 100644 --- a/packages/hoppscotch-app/src/helpers/RequestRunner.ts +++ b/packages/hoppscotch-app/src/helpers/RequestRunner.ts @@ -21,6 +21,7 @@ import { HoppRESTResponse } from "./types/HoppRESTResponse" import { createRESTNetworkRequestStream } from "./network" import { HoppTestData, HoppTestResult } from "./types/HoppTestResult" import { isJSONContentType } from "./utils/contenttypes" +import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment" import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession" import { environmentsStore, @@ -96,17 +97,35 @@ export const runRESTRequest$ = (): TaskEither< setGlobalEnvVariables(runResult.right.envs.global) - if (environmentsStore.value.currentEnvironmentIndex !== -1) { - const env = getEnvironment( - environmentsStore.value.currentEnvironmentIndex - ) + if ( + environmentsStore.value.selectedEnvironmentIndex.type === + "MY_ENV" + ) { + const env = getEnvironment({ + type: "MY_ENV", + index: environmentsStore.value.selectedEnvironmentIndex.index, + }) updateEnvironment( - environmentsStore.value.currentEnvironmentIndex, + environmentsStore.value.selectedEnvironmentIndex.index, { name: env.name, variables: runResult.right.envs.selected, } ) + } else if ( + environmentsStore.value.selectedEnvironmentIndex.type === + "TEAM_ENV" + ) { + const env = getEnvironment({ + type: "TEAM_ENV", + }) + pipe( + updateTeamEnvironment( + JSON.stringify(runResult.right.envs.selected), + environmentsStore.value.selectedEnvironmentIndex.teamEnvID, + env.name + ) + )() } } else { setRESTTestResults({ @@ -188,7 +207,7 @@ function translateToSandboxTestResults( } const globals = cloneDeep(getGlobalVariables()) - const env = cloneDeep(getCurrentEnvironment()) + const env = getCurrentEnvironment() return { description: "", diff --git a/packages/hoppscotch-app/src/helpers/backend/gql/mutations/CreateDuplicateEnvironment.graphql b/packages/hoppscotch-app/src/helpers/backend/gql/mutations/CreateDuplicateEnvironment.graphql new file mode 100644 index 000000000..674cd75ce --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/gql/mutations/CreateDuplicateEnvironment.graphql @@ -0,0 +1,8 @@ +mutation CreateDuplicateEnvironment($id: ID!){ + createDuplicateEnvironment (id: $id ){ + id + teamID + name + variables + } +} \ No newline at end of file diff --git a/packages/hoppscotch-app/src/helpers/backend/gql/mutations/CreateTeamEnvironment.graphql b/packages/hoppscotch-app/src/helpers/backend/gql/mutations/CreateTeamEnvironment.graphql new file mode 100644 index 000000000..efaba9855 --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/gql/mutations/CreateTeamEnvironment.graphql @@ -0,0 +1,7 @@ +mutation CreateTeamEnvironment($variables: String!,$teamID: ID!,$name: String!){ + createTeamEnvironment( variables: $variables ,teamID: $teamID ,name: $name){ + variables + name + teamID + } +} \ No newline at end of file diff --git a/packages/hoppscotch-app/src/helpers/backend/gql/mutations/DeleteTeamEnvironment.graphql b/packages/hoppscotch-app/src/helpers/backend/gql/mutations/DeleteTeamEnvironment.graphql new file mode 100644 index 000000000..f012c832f --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/gql/mutations/DeleteTeamEnvironment.graphql @@ -0,0 +1,3 @@ +mutation DeleteTeamEnvironment($id: ID!){ + deleteTeamEnvironment (id: $id ) +} \ No newline at end of file diff --git a/packages/hoppscotch-app/src/helpers/backend/gql/mutations/UpdateTeamEnvironment.graphql b/packages/hoppscotch-app/src/helpers/backend/gql/mutations/UpdateTeamEnvironment.graphql new file mode 100644 index 000000000..d2f2e3f16 --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/gql/mutations/UpdateTeamEnvironment.graphql @@ -0,0 +1,7 @@ +mutation UpdateTeamEnvironment($variables: String!,$id: ID!,$name: String!){ + updateTeamEnvironment( variables: $variables ,id: $id ,name: $name){ + variables + name + id + } +} \ No newline at end of file diff --git a/packages/hoppscotch-app/src/helpers/backend/gql/queries/GetTeamEnvironments.graphql b/packages/hoppscotch-app/src/helpers/backend/gql/queries/GetTeamEnvironments.graphql new file mode 100644 index 000000000..5f9b8250e --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/gql/queries/GetTeamEnvironments.graphql @@ -0,0 +1,10 @@ +query GetTeamEnvironments($teamID: ID!){ + team(teamID: $teamID){ + teamEnvironments{ + id + name + variables + teamID + } + } +} diff --git a/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentCreated.graphql b/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentCreated.graphql new file mode 100644 index 000000000..51aecdd21 --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentCreated.graphql @@ -0,0 +1,8 @@ +subscription TeamEnvironmentCreated ($teamID: ID!) { + teamEnvironmentCreated(teamID: $teamID) { + id + teamID + name + variables + } +} diff --git a/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentDeleted.graphql b/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentDeleted.graphql new file mode 100644 index 000000000..545a50f61 --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentDeleted.graphql @@ -0,0 +1,5 @@ +subscription TeamEnvironmentDeleted ($teamID: ID!) { + teamEnvironmentDeleted(teamID: $teamID) { + id + } +} diff --git a/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentUpdated.graphql b/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentUpdated.graphql new file mode 100644 index 000000000..561054b86 --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/gql/subscriptions/TeamEnvironmentUpdated.graphql @@ -0,0 +1,8 @@ +subscription TeamEnvironmentUpdated ($teamID: ID!) { + teamEnvironmentUpdated(teamID: $teamID) { + id + teamID + name + variables + } +} diff --git a/packages/hoppscotch-app/src/helpers/backend/mutations/TeamEnvironment.ts b/packages/hoppscotch-app/src/helpers/backend/mutations/TeamEnvironment.ts new file mode 100644 index 000000000..380c2c141 --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/backend/mutations/TeamEnvironment.ts @@ -0,0 +1,69 @@ +import { runMutation } from "../GQLClient" +import { + CreateDuplicateEnvironmentDocument, + CreateDuplicateEnvironmentMutation, + CreateDuplicateEnvironmentMutationVariables, + CreateTeamEnvironmentDocument, + CreateTeamEnvironmentMutation, + CreateTeamEnvironmentMutationVariables, + DeleteTeamEnvironmentDocument, + DeleteTeamEnvironmentMutation, + DeleteTeamEnvironmentMutationVariables, + UpdateTeamEnvironmentDocument, + UpdateTeamEnvironmentMutation, + UpdateTeamEnvironmentMutationVariables, +} from "../graphql" + +type DeleteTeamEnvironmentError = "team_environment/not_found" + +type UpdateTeamEnvironmentError = "team_environment/not_found" + +type DuplicateTeamEnvironmentError = "team_environment/not_found" + +export const createTeamEnvironment = ( + variables: string, + teamID: string, + name: string +) => + runMutation< + CreateTeamEnvironmentMutation, + CreateTeamEnvironmentMutationVariables, + "" + >(CreateTeamEnvironmentDocument, { + variables, + teamID, + name, + }) + +export const deleteTeamEnvironment = (id: string) => + runMutation< + DeleteTeamEnvironmentMutation, + DeleteTeamEnvironmentMutationVariables, + DeleteTeamEnvironmentError + >(DeleteTeamEnvironmentDocument, { + id, + }) + +export const updateTeamEnvironment = ( + variables: string, + id: string, + name: string +) => + runMutation< + UpdateTeamEnvironmentMutation, + UpdateTeamEnvironmentMutationVariables, + UpdateTeamEnvironmentError + >(UpdateTeamEnvironmentDocument, { + variables, + id, + name, + }) + +export const createDuplicateEnvironment = (id: string) => + runMutation< + CreateDuplicateEnvironmentMutation, + CreateDuplicateEnvironmentMutationVariables, + DuplicateTeamEnvironmentError + >(CreateDuplicateEnvironmentDocument, { + id, + }) diff --git a/packages/hoppscotch-app/src/helpers/editor/extensions/HoppEnvironment.ts b/packages/hoppscotch-app/src/helpers/editor/extensions/HoppEnvironment.ts index 87d89010f..cffe815e7 100644 --- a/packages/hoppscotch-app/src/helpers/editor/extensions/HoppEnvironment.ts +++ b/packages/hoppscotch-app/src/helpers/editor/extensions/HoppEnvironment.ts @@ -14,6 +14,7 @@ import { AggregateEnvironment, aggregateEnvs$, getAggregateEnvs, + getSelectedEnvironmentType, } from "~/newstore/environments" const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g @@ -73,6 +74,11 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) => const finalEnv = E.isLeft(result) ? "error" : result.right + const selectedEnvType = getSelectedEnvironmentType() + + const envTypeIcon = `${ + selectedEnvType === "TEAM_ENV" ? "people" : "person" + }` return { pos: start, end: to, @@ -81,11 +87,15 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) => create() { const dom = document.createElement("span") const kbd = document.createElement("kbd") + const icon = document.createElement("span") + icon.innerHTML = envTypeIcon kbd.textContent = finalEnv + dom.appendChild(icon) dom.appendChild(document.createTextNode(`${envName} `)) dom.appendChild(kbd) dom.className = "tippy-box" dom.dataset.theme = "tooltip" + icon.className = "env-icon" return { dom } }, } diff --git a/packages/hoppscotch-app/src/helpers/teams/TeamEnvironment.ts b/packages/hoppscotch-app/src/helpers/teams/TeamEnvironment.ts new file mode 100644 index 000000000..b48880f70 --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/teams/TeamEnvironment.ts @@ -0,0 +1,10 @@ +import { Environment } from "@hoppscotch/data" + +/** + * Defines how a Team Environment is represented in the TeamEnvironmentAdapter + */ +export interface TeamEnvironment { + id: string + teamID: string + environment: Environment +} diff --git a/packages/hoppscotch-app/src/helpers/teams/TeamEnvironmentAdapter.ts b/packages/hoppscotch-app/src/helpers/teams/TeamEnvironmentAdapter.ts new file mode 100644 index 000000000..55ab6b57c --- /dev/null +++ b/packages/hoppscotch-app/src/helpers/teams/TeamEnvironmentAdapter.ts @@ -0,0 +1,238 @@ +import * as E from "fp-ts/Either" +import { BehaviorSubject, Subscription } from "rxjs" +import { Subscription as WSubscription } from "wonka" +import { pipe } from "fp-ts/function" +import { GQLError, runGQLQuery, runGQLSubscription } from "../backend/GQLClient" +import { + GetTeamEnvironmentsDocument, + TeamEnvironmentCreatedDocument, + TeamEnvironmentDeletedDocument, + TeamEnvironmentUpdatedDocument, +} from "../backend/graphql" +import { TeamEnvironment } from "./TeamEnvironment" + +export default class TeamEnvironmentAdapter { + error$: BehaviorSubject | null> + loading$: BehaviorSubject + teamEnvironmentList$: BehaviorSubject + + private isDispose: boolean + + private teamEnvironmentCreated$: Subscription | null + private teamEnvironmentUpdated$: Subscription | null + private teamEnvironmentDeleted$: Subscription | null + + private teamEnvironmentCreatedSub: WSubscription | null + private teamEnvironmentUpdatedSub: WSubscription | null + private teamEnvironmentDeletedSub: WSubscription | null + + constructor(private teamID: string | undefined) { + this.error$ = new BehaviorSubject | null>(null) + this.loading$ = new BehaviorSubject(false) + this.teamEnvironmentList$ = new BehaviorSubject([]) + this.isDispose = true + + this.teamEnvironmentCreated$ = null + this.teamEnvironmentDeleted$ = null + this.teamEnvironmentUpdated$ = null + this.teamEnvironmentCreatedSub = null + this.teamEnvironmentDeletedSub = null + this.teamEnvironmentUpdatedSub = null + + if (teamID) this.initialize() + } + + unsubscribeSubscriptions() { + this.teamEnvironmentCreated$?.unsubscribe() + this.teamEnvironmentDeleted$?.unsubscribe() + this.teamEnvironmentUpdated$?.unsubscribe() + this.teamEnvironmentCreatedSub?.unsubscribe() + this.teamEnvironmentDeletedSub?.unsubscribe() + this.teamEnvironmentUpdatedSub?.unsubscribe() + } + + changeTeamID(newTeamID: string | undefined) { + this.teamID = newTeamID + this.teamEnvironmentList$.next([]) + this.loading$.next(false) + + this.unsubscribeSubscriptions() + + if (this.teamID) this.initialize() + } + + async initialize() { + if (!this.isDispose) throw new Error(`Adapter is already initialized`) + + await this.fetchList() + this.registerSubscriptions() + } + + public dispose() { + if (this.isDispose) throw new Error(`Adapter has been disposed`) + + this.isDispose = true + this.unsubscribeSubscriptions() + } + + async fetchList() { + if (this.teamID === undefined) throw new Error("Team ID is null") + + this.loading$.next(true) + + const results: TeamEnvironment[] = [] + + const result = await runGQLQuery({ + query: GetTeamEnvironmentsDocument, + variables: { + teamID: this.teamID, + }, + }) + + if (E.isLeft(result)) { + this.error$.next(result.left) + this.loading$.next(false) + console.error(result.left) + throw new Error(`Failed fetching team environments: ${result.left}`) + } + + if (result.right.team !== undefined && result.right.team !== null) { + results.push( + ...result.right.team.teamEnvironments.map( + (x) => + { + id: x.id, + teamID: x.teamID, + environment: { + name: x.name, + variables: JSON.parse(x.variables), + }, + } + ) + ) + } + + this.teamEnvironmentList$.next(results) + + this.loading$.next(false) + } + + private createNewTeamEnvironment(newEnvironment: TeamEnvironment) { + const teamEnvironments = this.teamEnvironmentList$.value + + teamEnvironments.push(newEnvironment) + + this.teamEnvironmentList$.next(teamEnvironments) + } + + private deleteTeamEnvironment(envId: string) { + const teamEnvironments = this.teamEnvironmentList$.value.filter( + ({ id }) => id !== envId + ) + + this.teamEnvironmentList$.next(teamEnvironments) + } + + private updateTeamEnvironment(updatedEnvironment: TeamEnvironment) { + const teamEnvironments = this.teamEnvironmentList$.value + + const environmentFound = teamEnvironments.find( + ({ id }) => id === updatedEnvironment.id + ) + + if (!environmentFound) return + + Object.assign(environmentFound, updatedEnvironment) + + this.teamEnvironmentList$.next(teamEnvironments) + } + + private registerSubscriptions() { + if (this.teamID === undefined) return + const [teamEnvironmentCreated$, teamEnvironmentCreatedSub] = + runGQLSubscription({ + query: TeamEnvironmentCreatedDocument, + variables: { + teamID: this.teamID, + }, + }) + + this.teamEnvironmentCreatedSub = teamEnvironmentCreatedSub + + this.teamEnvironmentCreated$ = teamEnvironmentCreated$.subscribe( + (result) => { + if (E.isLeft(result)) { + console.error(result.left) + throw new Error(`Team Environment Create Error ${result.left}`) + } + this.createNewTeamEnvironment( + pipe( + result.right.teamEnvironmentCreated, + (x) => + { + id: x.id, + teamID: x.teamID, + environment: { + name: x.name, + variables: JSON.parse(x.variables), + }, + } + ) + ) + } + ) + + const [teamEnvironmentDeleted$, teamEnvironmentDeletedSub] = + runGQLSubscription({ + query: TeamEnvironmentDeletedDocument, + variables: { + teamID: this.teamID, + }, + }) + + this.teamEnvironmentDeletedSub = teamEnvironmentDeletedSub + + this.teamEnvironmentDeleted$ = teamEnvironmentDeleted$.subscribe( + (result) => { + if (E.isLeft(result)) { + console.error(result.left) + throw new Error(`Team Environment Delete Error ${result.left}`) + } + this.deleteTeamEnvironment(result.right.teamEnvironmentDeleted.id) + } + ) + + const [teamEnvironmentUpdated$, teamEnvironmentUpdatedSub] = + runGQLSubscription({ + query: TeamEnvironmentUpdatedDocument, + variables: { + teamID: this.teamID, + }, + }) + + this.teamEnvironmentUpdatedSub = teamEnvironmentUpdatedSub + + this.teamEnvironmentUpdated$ = teamEnvironmentUpdated$.subscribe( + (result) => { + if (E.isLeft(result)) { + console.error(result.left) + throw new Error(`Team Environment Update Error ${result.left}`) + } + this.updateTeamEnvironment( + pipe( + result.right.teamEnvironmentUpdated, + (x) => + { + id: x.id, + teamID: x.teamID, + environment: { + name: x.name, + variables: JSON.parse(x.variables), + }, + } + ) + ) + } + ) + } +} diff --git a/packages/hoppscotch-app/src/newstore/environments.ts b/packages/hoppscotch-app/src/newstore/environments.ts index 92a860f86..54c7c63df 100644 --- a/packages/hoppscotch-app/src/newstore/environments.ts +++ b/packages/hoppscotch-app/src/newstore/environments.ts @@ -6,6 +6,16 @@ import DispatchingStore, { defineDispatchers, } from "~/newstore/DispatchingStore" +type SelectedEnvironmentIndex = + | { type: "NO_ENV_SELECTED" } + | { type: "MY_ENV"; index: number } + | { + type: "TEAM_ENV" + teamID: string + teamEnvID: string + environment: Environment + } + const defaultEnvironmentsState = { environments: [ { @@ -16,27 +26,22 @@ const defaultEnvironmentsState = { globals: [] as Environment["variables"], - // Current environment index specifies the index - // -1 means no environments are selected - currentEnvironmentIndex: -1, + selectedEnvironmentIndex: { + type: "NO_ENV_SELECTED", + } as SelectedEnvironmentIndex, } type EnvironmentStore = typeof defaultEnvironmentsState const dispatchers = defineDispatchers({ - setCurrentEnviromentIndex( - { environments }: EnvironmentStore, - { newIndex }: { newIndex: number } + setSelectedEnvironmentIndex( + _: EnvironmentStore, + { + selectedEnvironmentIndex, + }: { selectedEnvironmentIndex: SelectedEnvironmentIndex } ) { - if (newIndex >= environments.length || newIndex <= -2) { - // console.log( - // `Ignoring possibly invalid current environment index assignment (value: ${newIndex})` - // ) - return {} - } - return { - currentEnvironmentIndex: newIndex, + selectedEnvironmentIndex, } }, appendEnvironments( @@ -90,22 +95,36 @@ const dispatchers = defineDispatchers({ } }, deleteEnvironment( - { environments, currentEnvironmentIndex }: EnvironmentStore, + { + environments, + // currentEnvironmentIndex, + selectedEnvironmentIndex, + }: EnvironmentStore, { envIndex }: { envIndex: number } ) { - let newCurrEnvIndex = currentEnvironmentIndex + let newCurrEnvIndex = selectedEnvironmentIndex // Scenario 1: Currently Selected Env is removed -> Set currently selected to none - if (envIndex === currentEnvironmentIndex) newCurrEnvIndex = -1 + if ( + selectedEnvironmentIndex.type === "MY_ENV" && + envIndex === selectedEnvironmentIndex.index + ) + newCurrEnvIndex = { type: "NO_ENV_SELECTED" } // Scenario 2: Currently Selected Env Index > Deletion Index -> Current Selection Index Shifts One Position to the left -> Correct Env Index by moving back 1 index - if (envIndex < currentEnvironmentIndex) - newCurrEnvIndex = currentEnvironmentIndex - 1 + if ( + selectedEnvironmentIndex.type === "MY_ENV" && + envIndex < selectedEnvironmentIndex.index + ) + newCurrEnvIndex = { + type: "MY_ENV", + index: selectedEnvironmentIndex.index - 1, + } // Scenario 3: Currently Selected Env Index < Deletion Index -> No change happens at selection position -> Noop return { environments: environments.filter((_, index) => index !== envIndex), - currentEnvironmentIndex: newCurrEnvIndex, + selectedEnvironmentIndex: newCurrEnvIndex, } }, renameEnvironment( @@ -263,22 +282,23 @@ export const globalEnv$ = environmentsStore.subject$.pipe( distinctUntilChanged() ) -export const selectedEnvIndex$ = environmentsStore.subject$.pipe( - pluck("currentEnvironmentIndex"), +export const selectedEnvironmentIndex$ = environmentsStore.subject$.pipe( + pluck("selectedEnvironmentIndex"), distinctUntilChanged() ) export const currentEnvironment$ = environmentsStore.subject$.pipe( - map(({ currentEnvironmentIndex, environments }) => { - if (currentEnvironmentIndex === -1) { + map(({ environments, selectedEnvironmentIndex }) => { + if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") { const env: Environment = { name: "No environment", variables: [], } - return env + } else if (selectedEnvironmentIndex.type === "MY_ENV") { + return environments[selectedEnvironmentIndex.index] } else { - return environments[currentEnvironmentIndex] + return selectedEnvironmentIndex.environment } }) ) @@ -336,23 +356,35 @@ export function getAggregateEnvs() { } export function getCurrentEnvironment(): Environment { - if (environmentsStore.value.currentEnvironmentIndex === -1) { + if ( + environmentsStore.value.selectedEnvironmentIndex.type === "NO_ENV_SELECTED" + ) { return { name: "No environment", variables: [], } + } else if ( + environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV" + ) { + return environmentsStore.value.environments[ + environmentsStore.value.selectedEnvironmentIndex.index + ] + } else { + return environmentsStore.value.selectedEnvironmentIndex.environment } - - return environmentsStore.value.environments[ - environmentsStore.value.currentEnvironmentIndex - ] } -export function setCurrentEnvironment(newEnvIndex: number) { +export function getSelectedEnvironmentType() { + return environmentsStore.value.selectedEnvironmentIndex.type +} + +export function setSelectedEnvironmentIndex( + selectedEnvironmentIndex: SelectedEnvironmentIndex +) { environmentsStore.dispatch({ - dispatcher: "setCurrentEnviromentIndex", + dispatcher: "setSelectedEnvironmentIndex", payload: { - newIndex: newEnvIndex, + selectedEnvironmentIndex, }, }) } @@ -539,6 +571,23 @@ export function updateEnvironmentVariable( }) } -export function getEnvironment(index: number) { - return environmentsStore.value.environments[index] +type SelectedEnv = + | { type: "NO_ENV_SELECTED" } + | { type: "MY_ENV"; index: number } + | { type: "TEAM_ENV" } + +export function getEnvironment(selectedEnv: SelectedEnv) { + if (selectedEnv.type === "MY_ENV") { + return environmentsStore.value.environments[selectedEnv.index] + } else if ( + selectedEnv.type === "TEAM_ENV" && + environmentsStore.value.selectedEnvironmentIndex.type === "TEAM_ENV" + ) { + return environmentsStore.value.selectedEnvironmentIndex.environment + } else { + return { + name: "N0_ENV", + variables: [], + } + } } diff --git a/packages/hoppscotch-app/src/newstore/localpersistence.ts b/packages/hoppscotch-app/src/newstore/localpersistence.ts index d613c10f5..cfde49425 100644 --- a/packages/hoppscotch-app/src/newstore/localpersistence.ts +++ b/packages/hoppscotch-app/src/newstore/localpersistence.ts @@ -38,8 +38,8 @@ import { addGlobalEnvVariable, setGlobalEnvVariables, globalEnv$, - selectedEnvIndex$, - setCurrentEnvironment, + setSelectedEnvironmentIndex, + selectedEnvironmentIndex$, } from "./environments" import { getDefaultRESTRequest, @@ -224,11 +224,24 @@ function setupSelectedEnvPersistence() { ), O.getOrElse(() => -1) // If all the above conditions pass, we are good, else set default value (-1) ) + // Check if current environment index is -1 ie. no environment is selected + if (selectedEnvIndex === -1) { + setSelectedEnvironmentIndex({ + type: "NO_ENV_SELECTED", + }) + } else { + setSelectedEnvironmentIndex({ + type: "MY_ENV", + index: selectedEnvIndex, + }) + } - setCurrentEnvironment(selectedEnvIndex) - - selectedEnvIndex$.subscribe((index) => { - window.localStorage.setItem("selectedEnvIndex", index.toString()) + selectedEnvironmentIndex$.subscribe((envIndex) => { + if (envIndex.type === "MY_ENV") { + window.localStorage.setItem("selectedEnvIndex", envIndex.index.toString()) + } else { + window.localStorage.setItem("selectedEnvIndex", "-1") + } }) }