feat: sort environments alphabetically (#4280)

* feat: sort environments alphabetically in sidebar and selector

* fix: correct typo in i18n string key

* fix: rename and export team environments bug

* chore: added sortEnvironments util function

* chore: ads doc

* chore: cleanup

---------

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Nivedin
2024-08-28 13:16:10 +05:30
committed by GitHub
parent 43730d66f6
commit 3e2c72bdb2
8 changed files with 146 additions and 31 deletions

View File

@@ -309,6 +309,7 @@
"select": "Select environment", "select": "Select environment",
"set": "Set environment", "set": "Set environment",
"set_as_environment": "Set as environment", "set_as_environment": "Set as environment",
"short_name": "Environment needs to have minimum 3 characters",
"team_environments": "Workspace Environments", "team_environments": "Workspace Environments",
"title": "Environments", "title": "Environments",
"updated": "Environment updated", "updated": "Environment updated",

View File

@@ -77,24 +77,27 @@
:label="`${t('environment.my_environments')}`" :label="`${t('environment.my_environments')}`"
> >
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in myEnvironments" v-for="{
env,
index,
} in alphabeticallySortedPersonalEnvironments"
:key="`gen-${index}`" :key="`gen-${index}`"
:icon="IconLayers" :icon="IconLayers"
:label="gen.name" :label="env.name"
:info-icon="isEnvActive(index) ? IconCheck : undefined" :info-icon="isEnvActive(index) ? IconCheck : undefined"
:active-info-icon="isEnvActive(index)" :active-info-icon="isEnvActive(index)"
@click=" @click="
() => { () => {
handleEnvironmentChange(index, { handleEnvironmentChange(index, {
type: 'my-environment', type: 'my-environment',
environment: gen, environment: env,
}) })
hide() hide()
} }
" "
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="myEnvironments.length === 0" v-if="alphabeticallySortedPersonalEnvironments.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')" :text="t('empty.environments')"
@@ -116,24 +119,24 @@
</div> </div>
<div v-if="isTeamSelected" class="flex flex-col"> <div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in teamEnvironmentList" v-for="{ env, index } in alphabeticallySortedTeamEnvironments"
:key="`gen-team-${index}`" :key="`gen-team-${index}`"
:icon="IconLayers" :icon="IconLayers"
:label="gen.environment.name" :label="env.environment.name"
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined" :info-icon="isEnvActive(env.id) ? IconCheck : undefined"
:active-info-icon="isEnvActive(gen.id)" :active-info-icon="isEnvActive(env.id)"
@click=" @click="
() => { () => {
handleEnvironmentChange(index, { handleEnvironmentChange(index, {
type: 'team-environment', type: 'team-environment',
environment: gen, environment: env,
}) })
hide() hide()
} }
" "
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="teamEnvironmentList.length === 0" v-if="alphabeticallySortedTeamEnvironments.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')" :text="t('empty.environments')"
@@ -316,6 +319,10 @@ import { useLocalState } from "~/newstore/localstate"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import {
sortPersonalEnvironmentsAlphabetically,
sortTeamEnvironmentsAlphabetically,
} from "~/helpers/utils/sortEnvironmentsAlphabetically"
type Scope = type Scope =
| { | {
@@ -389,6 +396,15 @@ const teamEnvironmentList = useReadonlyStream(
[] []
) )
// Sort environments alphabetically by default
const alphabeticallySortedPersonalEnvironments = computed(() =>
sortPersonalEnvironmentsAlphabetically(myEnvironments.value, "asc")
)
const alphabeticallySortedTeamEnvironments = computed(() =>
sortTeamEnvironmentsAlphabetically(teamEnvironmentList.value, "asc")
)
const handleEnvironmentChange = ( const handleEnvironmentChange = (
index: number, index: number,
env?: env?:

View File

@@ -386,6 +386,11 @@ const saveEnvironment = () => {
return return
} }
if (editingName.value.length < 3) {
toast.error(`${t("environment.short_name")}`)
return
}
const filteredVariables = pipe( const filteredVariables = pipe(
vars.value, vars.value,
A.filterMap( A.filterMap(

View File

@@ -26,14 +26,14 @@
</div> </div>
</div> </div>
<EnvironmentsMyEnvironment <EnvironmentsMyEnvironment
v-for="(environment, index) in environments" v-for="{ env, index } in alphabeticallySortedPersonalEnvironments"
:key="`environment-${index}`" :key="`environment-${index}`"
:environment-index="index" :environment-index="index"
:environment="environment" :environment="env"
@edit-environment="editEnvironment(index)" @edit-environment="editEnvironment(index)"
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="!environments.length" v-if="!alphabeticallySortedPersonalEnvironments.length"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')" :text="t('empty.environments')"
@@ -79,7 +79,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref, computed } from "vue"
import { environments$ } from "~/newstore/environments" import { environments$ } from "~/newstore/environments"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
@@ -87,14 +87,19 @@ import { useI18n } from "~/composables/i18n"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconImport from "~icons/lucide/folder-down" import IconImport from "~icons/lucide/folder-down"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import { Environment } from "@hoppscotch/data"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { sortPersonalEnvironmentsAlphabetically } from "~/helpers/utils/sortEnvironmentsAlphabetically"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const environments = useReadonlyStream(environments$, []) const environments = useReadonlyStream(environments$, [])
// Sort environments alphabetically by default
const alphabeticallySortedPersonalEnvironments = computed(() =>
sortPersonalEnvironmentsAlphabetically(environments.value, "asc")
)
const showModalImportExport = ref(false) const showModalImportExport = ref(false)
const showModalDetails = ref(false) const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit") const action = ref<"new" | "edit">("edit")
@@ -130,11 +135,10 @@ defineActionHandler(
"modals.my.environment.edit", "modals.my.environment.edit",
({ envName, variableName, isSecret }) => { ({ envName, variableName, isSecret }) => {
if (variableName) editingVariableName.value = variableName if (variableName) editingVariableName.value = variableName
const envIndex: number = environments.value.findIndex( const envIndex: number =
(environment: Environment) => { alphabeticallySortedPersonalEnvironments.value.findIndex(({ env }) => {
return environment.name === envName return env.name === envName
} })
)
if (envName !== "Global") { if (envName !== "Global") {
editEnvironment(envIndex) editEnvironment(envIndex)
secretOptionSelected.value = isSecret ?? false secretOptionSelected.value = isSecret ?? false

View File

@@ -480,6 +480,8 @@ const getErrorMessage = (err: GQLError<string>) => {
switch (err.error) { switch (err.error) {
case "team_environment/not_found": case "team_environment/not_found":
return t("team_environment.not_found") return t("team_environment.not_found")
case "team_environment/short_name":
return t("environment.short_name")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
class="group flex items-stretch" class="group flex items-stretch"
@contextmenu.prevent="options!.tippy.show()" @contextmenu.prevent="options!.tippy?.show()"
> >
<span <span
class="flex cursor-pointer items-center justify-center px-4" class="flex cursor-pointer items-center justify-center px-4"
@@ -212,6 +212,8 @@ const getErrorMessage = (err: GQLError<string>) => {
switch (err.error) { switch (err.error) {
case "team_environment/not_found": case "team_environment/not_found":
return t("team_environment.not_found") return t("team_environment.not_found")
case "team_environment/short_name":
return t("environment.short_name")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
} }

View File

@@ -44,7 +44,11 @@
</div> </div>
</div> </div>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="!loading && !teamEnvironments.length && !adapterError" v-if="
!loading &&
!alphabeticallySortedTeamEnvironments.length &&
!adapterError
"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')" :text="t('empty.environments')"
@@ -79,15 +83,15 @@
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div v-else-if="!loading"> <div v-else-if="!loading">
<EnvironmentsTeamsEnvironment <EnvironmentsTeamsEnvironment
v-for="(environment, index) in JSON.parse( v-for="{ env, index } in JSON.parse(
JSON.stringify(teamEnvironments) JSON.stringify(alphabeticallySortedTeamEnvironments)
)" )"
:key="`environment-${index}`" :key="`environment-${index}`"
:environment="environment" :environment="env"
:is-viewer="team?.role === 'VIEWER'" :is-viewer="team?.role === 'VIEWER'"
@edit-environment="editEnvironment(environment)" @edit-environment="editEnvironment(env)"
@show-environment-properties=" @show-environment-properties="
showEnvironmentProperties(environment.environment.id) showEnvironmentProperties(env.environment.id)
" "
/> />
</div> </div>
@@ -114,7 +118,9 @@
/> />
<EnvironmentsImportExport <EnvironmentsImportExport
v-if="showModalImportExport" v-if="showModalImportExport"
:team-environments="teamEnvironments" :team-environments="
alphabeticallySortedTeamEnvironments.map(({ env }) => env)
"
:team-id="team?.teamID" :team-id="team?.teamID"
environment-type="TEAM_ENV" environment-type="TEAM_ENV"
@hide-modal="displayModalImportExport(false)" @hide-modal="displayModalImportExport(false)"
@@ -139,6 +145,7 @@ import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down" import IconImport from "~icons/lucide/folder-down"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { TeamWorkspace } from "~/services/workspace.service" import { TeamWorkspace } from "~/services/workspace.service"
import { sortTeamEnvironmentsAlphabetically } from "~/helpers/utils/sortEnvironmentsAlphabetically"
const t = useI18n() const t = useI18n()
@@ -151,6 +158,12 @@ const props = defineProps<{
loading: boolean loading: boolean
}>() }>()
// Sort environments alphabetically by default
const alphabeticallySortedTeamEnvironments = computed(() =>
sortTeamEnvironmentsAlphabetically(props.teamEnvironments, "asc")
)
const showModalImportExport = ref(false) const showModalImportExport = ref(false)
const showModalDetails = ref(false) const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit") const action = ref<"new" | "edit">("edit")
@@ -209,11 +222,12 @@ defineActionHandler(
"modals.team.environment.edit", "modals.team.environment.edit",
({ envName, variableName, isSecret }) => { ({ envName, variableName, isSecret }) => {
if (variableName) editingVariableName.value = variableName if (variableName) editingVariableName.value = variableName
const teamEnvToEdit = props.teamEnvironments.find( const teamEnvToEdit = alphabeticallySortedTeamEnvironments.value.find(
(environment) => environment.environment.name === envName ({ env }) => env.environment.name === envName
) )
if (teamEnvToEdit) { if (teamEnvToEdit) {
editEnvironment(teamEnvToEdit) const { env } = teamEnvToEdit
editEnvironment(env)
secretOptionSelected.value = isSecret ?? false secretOptionSelected.value = isSecret ?? false
} }
} }

View File

@@ -0,0 +1,71 @@
import { Environment } from "@hoppscotch/data"
import { TeamEnvironment } from "../teams/TeamEnvironment"
type SortOrder = "asc" | "desc"
type EnvironmentWithIndex<T> = {
env: T
index: number
}
/**
* Sorts an array of environments alphabetically based on a specified name getter function.
*
* @template T - The type of the environments array elements.
* @param {T[]} environments - The array of environments to be sorted.
* @param {SortOrder} order - The sort order, either "asc" for ascending or "desc" for descending.
* @param {(env: T) => string} getName - The function that retrieves the name from an environment entry.
* @returns {EnvironmentWithIndex<T>[]} - The sorted array of environments with their original indices.
*/
const sortEnvironmentsAlphabetically = <T>(
environments: T[],
order: SortOrder,
getName: (env: T) => string
): EnvironmentWithIndex<T>[] => {
return [...environments]
.map((env, index) => ({
env,
index,
}))
.sort((a, b) => {
const comparison = getName(a.env)
.toLocaleLowerCase()
.localeCompare(getName(b.env).toLocaleLowerCase())
return order === "asc" ? comparison : -comparison
})
}
/**
* Returns an object with sorted personal environments and index.
* @param {Environment[]} environments Array of personal environments.
* @param {SortOrder} order Sorting order.
* @returns {EnvironmentWithIndex<Environment>[]} Object with sorted environments and their index.
*/
export const sortPersonalEnvironmentsAlphabetically = (
environments: Environment[],
order: SortOrder
): EnvironmentWithIndex<Environment>[] => {
return sortEnvironmentsAlphabetically<Environment>(
environments,
order,
(env) => env.name
)
}
/**
* Returns an object with sorted team environments and index.
* @param environments Array of team environments.
* @param order Sorting order.
* @returns {EnvironmentWithIndex<TeamEnvironment>[]} Object with sorted environments and their index.
*/
export const sortTeamEnvironmentsAlphabetically = (
environments: TeamEnvironment[],
order: SortOrder
): EnvironmentWithIndex<TeamEnvironment>[] => {
return sortEnvironmentsAlphabetically<TeamEnvironment>(
environments,
order,
(env) => env.environment.name
)
}