From 5742968eda4f1c32de8a8638a7b121a5df347814 Mon Sep 17 00:00:00 2001 From: James George <25279263+jamesgeorge007@users.noreply.github.com> Date: Wed, 25 Sep 2024 05:44:55 -0700 Subject: [PATCH] feat: duplicate global environment under team workspaces (#4334) Co-authored-by: nivedin --- .../team-environments.service.ts | 2 +- .../src/components/environments/Add.vue | 25 +++------ .../src/components/environments/Selector.vue | 24 +++------ .../src/components/environments/index.vue | 54 +++++++++++++++++-- .../environments/my/Environment.vue | 46 ++++++++++------ .../components/environments/teams/Details.vue | 19 ++----- .../environments/teams/Environment.vue | 45 +++++++--------- .../components/environments/teams/index.vue | 15 +----- .../src/helpers/error-messages/index.ts | 18 +++++++ 9 files changed, 136 insertions(+), 112 deletions(-) create mode 100644 packages/hoppscotch-common/src/helpers/error-messages/index.ts diff --git a/packages/hoppscotch-backend/src/team-environments/team-environments.service.ts b/packages/hoppscotch-backend/src/team-environments/team-environments.service.ts index f2b28b70b..91779e4b5 100644 --- a/packages/hoppscotch-backend/src/team-environments/team-environments.service.ts +++ b/packages/hoppscotch-backend/src/team-environments/team-environments.service.ts @@ -194,7 +194,7 @@ export class TeamEnvironmentsService { const result = await this.prisma.teamEnvironment.create({ data: { - name: environment.name, + name: `${environment.name} - Duplicate`, teamID: environment.teamID, variables: environment.variables as Prisma.JsonArray, }, diff --git a/packages/hoppscotch-common/src/components/environments/Add.vue b/packages/hoppscotch-common/src/components/environments/Add.vue index 3446d2349..5eed9be63 100644 --- a/packages/hoppscotch-common/src/components/environments/Add.vue +++ b/packages/hoppscotch-common/src/components/environments/Add.vue @@ -71,20 +71,21 @@ diff --git a/packages/hoppscotch-common/src/components/environments/Selector.vue b/packages/hoppscotch-common/src/components/environments/Selector.vue index 1f859bc08..d0e511cd1 100644 --- a/packages/hoppscotch-common/src/components/environments/Selector.vue +++ b/packages/hoppscotch-common/src/components/environments/Selector.vue @@ -147,7 +147,7 @@ class="flex flex-col items-center py-4" > - {{ getErrorMessage(teamAdapterError) }} + {{ t(getEnvActionErrorMessage(teamAdapterError)) }} @@ -304,10 +304,14 @@ import { TippyComponent } from "vue-tippy" import { useI18n } from "~/composables/i18n" import { useReadonlyStream, useStream } from "~/composables/stream" import { invokeAction } from "~/helpers/actions" -import { GQLError } from "~/helpers/backend/GQLClient" import { GetMyTeamsQuery } from "~/helpers/backend/graphql" +import { getEnvActionErrorMessage } from "~/helpers/error-messages" import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" +import { + sortPersonalEnvironmentsAlphabetically, + sortTeamEnvironmentsAlphabetically, +} from "~/helpers/utils/sortEnvironmentsAlphabetically" import { environments$, globalEnv$, @@ -316,10 +320,6 @@ import { } from "~/newstore/environments" import { useLocalState } from "~/newstore/localstate" import { WorkspaceService } from "~/services/workspace.service" -import { - sortPersonalEnvironmentsAlphabetically, - sortTeamEnvironmentsAlphabetically, -} from "~/helpers/utils/sortEnvironmentsAlphabetically" import IconCheck from "~icons/lucide/check" import IconEdit from "~icons/lucide/edit" import IconEye from "~icons/lucide/eye" @@ -590,18 +590,6 @@ onMounted(() => { const envSelectorActions = ref(null) const envQuickPeekActions = ref(null) -const getErrorMessage = (err: GQLError) => { - if (err.type === "network_error") { - return t("error.network_error") - } - switch (err.error) { - case "team_environment/not_found": - return t("team_environment.not_found") - default: - return t("error.something_went_wrong") - } -} - const globalEnvs = useReadonlyStream(globalEnv$, {} as GlobalEnvironment) const environmentVariables = computed(() => { diff --git a/packages/hoppscotch-common/src/components/environments/index.vue b/packages/hoppscotch-common/src/components/environments/index.vue index 431bbafce..3ff8d2f5a 100644 --- a/packages/hoppscotch-common/src/components/environments/index.vue +++ b/packages/hoppscotch-common/src/components/environments/index.vue @@ -7,8 +7,12 @@ @@ -52,16 +56,22 @@ import { Environment, GlobalEnvironment } from "@hoppscotch/data" import { useService } from "dioc/vue" import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { isEqual } from "lodash-es" +import { cloneDeep, isEqual } from "lodash-es" import { computed, ref, watch } from "vue" import { useI18n } from "~/composables/i18n" import { useToast } from "~/composables/toast" import { defineActionHandler } from "~/helpers/actions" import { GQLError } from "~/helpers/backend/GQLClient" -import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment" +import { + createTeamEnvironment, + deleteTeamEnvironment, +} from "~/helpers/backend/mutations/TeamEnvironment" +import { getEnvActionErrorMessage } from "~/helpers/error-messages" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" import { + createEnvironment, deleteEnvironment, + getGlobalVariables, getSelectedEnvironmentIndex, globalEnv$, selectedEnvironmentIndex$, @@ -88,7 +98,7 @@ const environmentType = ref({ const globalEnv = useReadonlyStream(globalEnv$, {} as GlobalEnvironment) -const globalEnvironment = computed(() => ({ +const globalEnvironment = computed(() => ({ v: 1 as const, id: "Global", name: "Global", @@ -189,6 +199,7 @@ const editingEnvironmentIndex = ref<"Global" | null>(null) const editingVariableName = ref("") const editingVariableValue = ref("") const secretOptionSelected = ref(false) +const duplicateGlobalEnvironmentLoading = ref(false) const position = ref({ top: 0, left: 0 }) @@ -210,6 +221,41 @@ const editEnvironment = (environmentIndex: "Global") => { displayModalEdit(true) } +const duplicateGlobalEnvironment = async () => { + if (workspace.value.type === "team") { + duplicateGlobalEnvironmentLoading.value = true + + await pipe( + createTeamEnvironment( + JSON.stringify(globalEnvironment.value.variables), + workspace.value.teamID, + `Global - ${t("action.duplicate")}` + ), + TE.match( + (err: GQLError) => { + console.error(err) + + toast.error(t(getEnvActionErrorMessage(err))) + }, + () => { + toast.success(t("environment.duplicated")) + } + ) + )() + + duplicateGlobalEnvironmentLoading.value = false + + return + } + + createEnvironment( + `Global - ${t("action.duplicate")}`, + cloneDeep(getGlobalVariables()) + ) + + toast.success(`${t("environment.duplicated")}`) +} + const removeSelectedEnvironment = () => { const selectedEnvIndex = getSelectedEnvironmentIndex() if (selectedEnvIndex?.type === "NO_ENV_SELECTED") return diff --git a/packages/hoppscotch-common/src/components/environments/my/Environment.vue b/packages/hoppscotch-common/src/components/environments/my/Environment.vue index 5464117d3..f220bf87c 100644 --- a/packages/hoppscotch-common/src/components/environments/my/Environment.vue +++ b/packages/hoppscotch-common/src/components/environments/my/Environment.vue @@ -45,7 +45,7 @@ tabindex="0" role="menu" @keyup.e="edit!.$el.click()" - @keyup.d="showDuplicateAction ? duplicate!.$el.click() : null" + @keyup.d="duplicate!.$el.click()" @keyup.j="exportAsJsonEl!.$el.click()" @keyup.delete=" !(environmentIndex === 'Global') @@ -59,6 +59,7 @@ :icon="IconEdit" :label="`${t('action.edit')}`" :shortcut="['E']" + :disabled="duplicateGlobalEnvironmentLoading" @click=" () => { emit('edit-environment') @@ -67,15 +68,15 @@ " /> @@ -84,6 +85,7 @@ :icon="IconEdit" :label="`${t('export.as_json')}`" :shortcut="['J']" + :disabled="duplicateGlobalEnvironmentLoading" @click=" () => { exportEnvironmentAsJSON() @@ -97,6 +99,7 @@ :icon="IconTrash2" :label="`${t('action.delete')}`" :shortcut="['⌫']" + :disabled="duplicateGlobalEnvironmentLoading" @click=" () => { confirmRemove = true @@ -123,17 +126,14 @@ import { useToast } from "@composables/toast" import { Environment } from "@hoppscotch/data" import { HoppSmartItem } from "@hoppscotch/ui" import { useService } from "dioc/vue" -import { cloneDeep } from "lodash-es" -import { computed, ref } from "vue" +import { computed, ref, watch } from "vue" import { TippyComponent } from "vue-tippy" import * as E from "fp-ts/Either" import { exportAsJSON } from "~/helpers/import-export/export/environment" import { - createEnvironment, deleteEnvironment, duplicateEnvironment, - getGlobalVariables, } from "~/newstore/environments" import { SecretEnvironmentService } from "~/services/secret-environment.service" import IconCopy from "~icons/lucide/copy" @@ -148,21 +148,33 @@ const props = withDefaults( defineProps<{ environment: Environment environmentIndex: number | "Global" | null - showDuplicateAction: boolean + duplicateGlobalEnvironmentLoading?: boolean + showContextMenuLoadingState?: boolean }>(), { - showDuplicateAction: true, + duplicateGlobalEnvironmentLoading: false, + showContextMenuLoadingState: false, } ) const emit = defineEmits<{ (e: "edit-environment"): void + (e: "duplicate-global-environment"): void }>() const confirmRemove = ref(false) const secretEnvironmentService = useService(SecretEnvironmentService) +watch( + () => props.duplicateGlobalEnvironmentLoading, + (newDuplicateGlobalEnvironmentLoadingVal) => { + if (!newDuplicateGlobalEnvironmentLoadingVal) { + options?.value?.tippy?.hide() + } + } +) + const isGlobalEnvironment = computed(() => props.environmentIndex === "Global") const exportEnvironmentAsJSON = async () => { @@ -191,14 +203,16 @@ const removeEnvironment = () => { } const duplicateEnvironments = () => { - if (props.environmentIndex === null) return - if (isGlobalEnvironment.value) { - createEnvironment( - `Global - ${t("action.duplicate")}`, - cloneDeep(getGlobalVariables()) - ) - } else duplicateEnvironment(props.environmentIndex as number) + if (props.environmentIndex === null) { + return + } + if (isGlobalEnvironment.value) { + emit("duplicate-global-environment") + return + } + + duplicateEnvironment(props.environmentIndex as number) toast.success(`${t("environment.duplicated")}`) } diff --git a/packages/hoppscotch-common/src/components/environments/teams/Details.vue b/packages/hoppscotch-common/src/components/environments/teams/Details.vue index 676408beb..3906dced5 100644 --- a/packages/hoppscotch-common/src/components/environments/teams/Details.vue +++ b/packages/hoppscotch-common/src/components/environments/teams/Details.vue @@ -165,6 +165,7 @@ import IconHelpCircle from "~icons/lucide/help-circle" import { platform } from "~/platform" import { useService } from "dioc/vue" import { SecretEnvironmentService } from "~/services/secret-environment.service" +import { getEnvActionErrorMessage } from "~/helpers/error-messages" type EnvironmentVariable = { id: number @@ -405,7 +406,7 @@ const saveEnvironment = async () => { TE.match( (err: GQLError) => { console.error(err) - toast.error(`${getErrorMessage(err)}`) + toast.error(t(getEnvActionErrorMessage(err))) isLoading.value = false }, (res) => { @@ -453,7 +454,7 @@ const saveEnvironment = async () => { TE.match( (err: GQLError) => { console.error(err) - toast.error(`${getErrorMessage(err)}`) + toast.error(t(getEnvActionErrorMessage(err))) isLoading.value = false }, () => { @@ -474,18 +475,4 @@ const hideModal = () => { selectedEnvOption.value = "variables" emit("hide-modal") } - -const getErrorMessage = (err: GQLError) => { - if (err.type === "network_error") { - return t("error.network_error") - } - 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") - } -} diff --git a/packages/hoppscotch-common/src/components/environments/teams/Environment.vue b/packages/hoppscotch-common/src/components/environments/teams/Environment.vue index fdc0a683a..a7e74f35c 100644 --- a/packages/hoppscotch-common/src/components/environments/teams/Environment.vue +++ b/packages/hoppscotch-common/src/components/environments/teams/Environment.vue @@ -48,6 +48,7 @@ :icon="IconEdit" :label="`${t('action.edit')}`" :shortcut="['E']" + :disabled="duplicateEnvironmentLoading" @click=" () => { emit('edit-environment') @@ -62,12 +63,8 @@ :icon="IconCopy" :label="`${t('action.duplicate')}`" :shortcut="['D']" - @click=" - () => { - duplicateEnvironments() - hide() - } - " + :loading="duplicateEnvironmentLoading" + @click="duplicateEnvironment" /> () const exportAsJsonEl = ref() const propertiesAction = ref() +const duplicateEnvironmentLoading = ref(false) + const removeEnvironment = () => { pipe( deleteTeamEnvironment(props.environment.id), TE.match( (err: GQLError) => { console.error(err) - toast.error(`${getErrorMessage(err)}`) + toast.error(t(getEnvActionErrorMessage(err))) }, () => { toast.success(`${t("team_environment.deleted")}`) @@ -193,32 +196,24 @@ const removeEnvironment = () => { )() } -const duplicateEnvironments = () => { - pipe( - duplicateEnvironment(props.environment.id), +const duplicateEnvironment = async () => { + duplicateEnvironmentLoading.value = true + + await pipe( + duplicateTeamEnvironment(props.environment.id), TE.match( (err: GQLError) => { console.error(err) - toast.error(`${getErrorMessage(err)}`) + toast.error(t(getEnvActionErrorMessage(err))) }, () => { toast.success(`${t("environment.duplicated")}`) } ) )() -} -const getErrorMessage = (err: GQLError) => { - if (err.type === "network_error") { - return t("error.network_error") - } - 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") - } + duplicateEnvironmentLoading.value = false + + options.value!.tippy?.hide() } diff --git a/packages/hoppscotch-common/src/components/environments/teams/index.vue b/packages/hoppscotch-common/src/components/environments/teams/index.vue index 6b6e44fcc..a69482bb2 100644 --- a/packages/hoppscotch-common/src/components/environments/teams/index.vue +++ b/packages/hoppscotch-common/src/components/environments/teams/index.vue @@ -104,7 +104,7 @@ class="flex flex-col items-center py-4" > - {{ getErrorMessage(adapterError) }} + {{ t(getEnvActionErrorMessage(adapterError)) }} { secretOptionSelected.value = false } -const getErrorMessage = (err: GQLError) => { - if (err.type === "network_error") { - return t("error.network_error") - } - switch (err.error) { - case "team_environment/not_found": - return t("team_environment.not_found") - default: - return t("error.something_went_wrong") - } -} - const showEnvironmentProperties = (environmentID: string) => { showEnvironmentsPropertiesModal.value = true selectedEnvironmentID.value = environmentID diff --git a/packages/hoppscotch-common/src/helpers/error-messages/index.ts b/packages/hoppscotch-common/src/helpers/error-messages/index.ts new file mode 100644 index 000000000..76382204e --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/error-messages/index.ts @@ -0,0 +1,18 @@ +import { GQLError } from "../backend/GQLClient" + +export const getEnvActionErrorMessage = (err: GQLError) => { + if (err.type === "network_error") { + return "error.network_error" + } + + switch (err.error) { + case "team_environment/not_found": + return "team_environment.not_found" + case "team_environment/short_name": + return "environment.short_name" + case "Forbidden resource": + return "profile.no_permission" + default: + return "error.something_went_wrong" + } +}