chore: environment selector with new ux (#3052)

Co-authored-by: Nivedin <nivedinp@gmail.com>
This commit is contained in:
Liyas Thomas
2023-05-31 03:17:37 +05:30
committed by GitHub
parent 9a40058329
commit 397b26a9f3
5 changed files with 162 additions and 103 deletions

View File

@@ -46,93 +46,138 @@
} }
" "
/> />
<div v-if="environmentType === 'my-environments'" class="flex flex-col"> <HoppSmartTabs
<hr v-if="myEnvironments.length > 0" /> v-model="selectedEnvTab"
<HoppSmartItem styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary"
v-for="(gen, index) in myEnvironments" render-inactive-tabs
:key="`gen-${index}`" >
:label="gen.name" <HoppSmartTab
:info-icon="index === selectedEnv.index ? IconCheck : undefined" :id="'my-environments'"
:active-info-icon="index === selectedEnv.index" :label="`${t('environment.my_environments')}`"
@click="
() => {
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
hide()
}
"
/>
</div>
<div v-else class="flex flex-col">
<div
v-if="teamEnvLoading"
class="flex flex-col items-center justify-center p-4"
> >
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<hr v-if="teamEnvironmentList.length > 0" />
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in teamEnvironmentList" v-for="(gen, index) in myEnvironments"
:key="`gen-team-${index}`" :key="`gen-${index}`"
:label="gen.environment.name" :label="gen.name"
:info-icon=" :info-icon="index === selectedEnv.index ? IconCheck : undefined"
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined :active-info-icon="index === selectedEnv.index"
"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
@click=" @click="
() => { () => {
selectedEnvironmentIndex = { selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide() hide()
} }
" "
/> />
</div> <div
<div v-if="myEnvironments.length === 0"
v-if="!teamEnvLoading && isAdapterError" class="flex flex-col items-center justify-center text-secondaryLight"
class="flex flex-col items-center py-4" >
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
> >
<icon-lucide-help-circle class="mb-4 svg-icons" /> <div
{{ errorMessage }} v-if="teamListLoading"
</div> class="flex flex-col items-center justify-center p-4"
</div> >
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem
v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`"
:label="gen.environment.name"
:info-icon="
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
@click="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide()
}
"
/>
<div
v-if="teamEnvironmentList.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</div>
<div
v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div> </div>
</template> </template>
</tippy> </tippy>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue" import { computed, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { Environment } from "@hoppscotch/data" import { useReadonlyStream, useStream } from "~/composables/stream"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { useStream } from "~/composables/stream"
import { import {
environments$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import { workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode()
type EnvironmentType = "my-environments" | "team-environments" type EnvironmentType = "my-environments" | "team-environments"
const props = defineProps<{ const myEnvironments = useReadonlyStream(environments$, [])
environmentType: EnvironmentType
myEnvironments: Environment[] const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
teamEnvironmentList: TeamEnvironment[]
teamEnvLoading: boolean const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
isAdapterError: boolean const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
errorMessage: GQLError<string> const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
isTeamSelected: boolean const teamEnvironmentList = useReadonlyStream(
}>() teamEnvListAdapter.teamEnvironmentList$,
[]
)
const selectedEnvironmentIndex = useStream( const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
@@ -140,15 +185,35 @@ const selectedEnvironmentIndex = useStream(
setSelectedEnvironmentIndex setSelectedEnvironmentIndex
) )
const isTeamSelected = computed(
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
)
const selectedEnvTab = ref<EnvironmentType>("my-environments")
watch(
() => workspace.value,
(newVal) => {
if (newVal.type === "personal") {
selectedEnvTab.value = "my-environments"
} else {
selectedEnvTab.value = "team-environments"
if (newVal.teamID) {
teamEnvListAdapter.changeTeamID(newVal.teamID)
}
}
}
)
const selectedEnv = computed(() => { const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") { if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return { return {
type: "MY_ENV", type: "MY_ENV",
index: selectedEnvironmentIndex.value.index, index: selectedEnvironmentIndex.value.index,
name: props.myEnvironments[selectedEnvironmentIndex.value.index].name, name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
} }
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") { } else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = props.teamEnvironmentList.find( const teamEnv = teamEnvironmentList.value.find(
(env) => (env) =>
env.id === env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" && (selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
@@ -170,4 +235,17 @@ const selectedEnv = computed(() => {
// Template refs // Template refs
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -4,15 +4,6 @@
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary" class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
> >
<WorkspaceCurrent :section="t('tab.environments')" /> <WorkspaceCurrent :section="t('tab.environments')" />
<EnvironmentsSelector
:environment-type="environmentType.type"
:my-environments="myEnvironments"
:team-env-loading="loading"
:team-environment-list="teamEnvironmentList"
:is-adapter-error="adapterError !== null"
:error-message="adapterError ? getErrorMessage(adapterError) : ''"
:is-team-selected="environmentType.selectedTeam !== undefined"
/>
<EnvironmentsMyEnvironment <EnvironmentsMyEnvironment
environment-index="Global" environment-index="Global"
:environment="globalEnvironment" :environment="globalEnvironment"
@@ -46,13 +37,11 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useReadonlyStream, useStream } from "@composables/stream" import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { import {
environments$,
globalEnv$, globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { GQLError } from "~/helpers/backend/GQLClient"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { workspaceStatus$ } from "~/newstore/workspace" import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter" import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
@@ -147,24 +136,19 @@ onLoggedIn(() => {
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" }) const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// Used to switch environment type and team when user switch workspace in the global workspace switcher // Switch to my environments if workspace is personal and to team environments if workspace is team
// Check if there is a teamID in the workspace, if yes, switch to team environment and select the team // also resets selected environment if workspace is personal and the previous selected environment was a team environment
// If there is no teamID, switch to my environment
watch(workspace, (newWorkspace) => { watch(workspace, (newWorkspace) => {
if (newWorkspace.type === "personal") { if (newWorkspace.type === "personal") {
switchToMyEnvironments() switchToMyEnvironments()
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
if (selectedEnvironmentIndex.value.type !== "MY_ENV") { if (selectedEnvironmentIndex.value.type !== "MY_ENV") {
setSelectedEnvironmentIndex({ setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED", type: "NO_ENV_SELECTED",
}) })
} }
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
} }
}) })
@@ -207,8 +191,6 @@ defineActionHandler(
} }
) )
const myEnvironments = useReadonlyStream(environments$, [])
const selectedEnvironmentIndex = useStream( const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" }, { type: "NO_ENV_SELECTED" },
@@ -251,17 +233,4 @@ watch(
}, },
{ deep: true } { deep: true }
) )
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div <div
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperSecondaryStickyFold border-dividerLight bg-primary" class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="team === undefined || team.myRole === 'VIEWER'" v-if="team === undefined || team.myRole === 'VIEWER'"

View File

@@ -56,6 +56,9 @@
@update:model-value="onTabUpdate" @update:model-value="onTabUpdate"
/> />
</HoppSmartWindow> </HoppSmartWindow>
<template #actions>
<EnvironmentsSelector class="h-full" />
</template>
</HoppSmartWindows> </HoppSmartWindows>
</template> </template>
<template #sidebar> <template #sidebar>

View File

@@ -100,7 +100,9 @@
</div> </div>
</div> </div>
<slot name="actions" /> <div v-if="hasActions" class="w-64">
<slot name="actions" />
</div>
<input <input
type="range" type="range"
@@ -111,10 +113,10 @@
:class="{ :class="{
'!block': scrollThumb.show, '!block': scrollThumb.show,
}" }"
:style="{ :style="[
'--thumb-width': scrollThumb.width + 'px', `--thumb-width: ${scrollThumb.width}px`,
}" `width: calc(100% - ${hasActions ? '19rem' : '3rem'})`,
style="width: calc(100% - 3rem)" ]"
id="myRange" id="myRange"
/> />
</div> </div>
@@ -140,6 +142,7 @@ import {
inject, inject,
watch, watch,
nextTick, nextTick,
useSlots,
} from "vue" } from "vue"
import { useElementSize } from "@vueuse/core" import { useElementSize } from "@vueuse/core"
import type { Slot } from "vue" import type { Slot } from "vue"
@@ -191,6 +194,12 @@ const emit = defineEmits<{
(e: "addTab"): void (e: "addTab"): void
}>() }>()
const slots = useSlots()
const hasActions = computed(() => {
return !!slots.actions
})
const throwError = (message: string): never => { const throwError = (message: string): never => {
throw new Error(message) throw new Error(message)
} }