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",
"set": "Set environment",
"set_as_environment": "Set as environment",
"short_name": "Environment needs to have minimum 3 characters",
"team_environments": "Workspace Environments",
"title": "Environments",
"updated": "Environment updated",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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