feat: CLI collection runner command generation UI flow (#4141)

Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
James George
2024-06-27 06:47:56 -07:00
committed by GitHub
parent a9afb17dc0
commit 3b70668162
11 changed files with 586 additions and 99 deletions

View File

@@ -191,7 +191,8 @@
"save_as": "Save as",
"save_to_collection": "Save to Collection",
"select": "Select a Collection",
"select_location": "Select location"
"select_location": "Select location",
"details": "Details"
},
"confirm": {
"close_unsaved_tab": "Are you sure you want to close this tab?",
@@ -309,7 +310,9 @@
"value": "Value",
"variable": "Variable",
"variables": "Variables",
"variable_list": "Variable List"
"variable_list": "Variable List",
"properties": "Environment Properties",
"details": "Details"
},
"error": {
"authproviders_load_error": "Unable to load auth providers",
@@ -1062,5 +1065,18 @@
"generate_new_token": "Generate new token",
"generate_modal_title": "New Personal Access Token",
"deletion_success": "The access token {label} has been deleted"
},
"collection_runner": {
"collection_id": "Collection ID",
"environment_id": "Environment ID",
"cli_collection_id_description": "This collection ID will be used by the CLI collection runner for Hoppscotch.",
"cli_environment_id_description": "This environment ID will be used by the CLI collection runner for Hoppscotch.",
"include_active_environment": "Include active environment:",
"cli": "CLI",
"ui": "Runner (coming soon)",
"cli_command_generation_description_cloud": "Copy the below command and run it from the CLI. Please specify a personal access token.",
"cli_command_generation_description_sh": "Copy the below command and run it from the CLI. Please specify a personal access token and verify the generated SH instance server URL.",
"cli_command_generation_description_sh_with_server_url_placeholder": "Copy the below command and run it from the CLI. Please specify a personal access token and the SH instance server URL.",
"run_collection": "Run collection"
}
}

View File

@@ -61,6 +61,7 @@ declare module 'vue' {
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
CollectionsProperties: typeof import('./components/collections/Properties.vue')['default']
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsRunner: typeof import('./components/collections/Runner.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
@@ -72,6 +73,7 @@ declare module 'vue' {
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']
EnvironmentsProperties: typeof import('./components/environments/Properties.vue')['default']
EnvironmentsSelector: typeof import('./components/environments/Selector.vue')['default']
EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']

View File

@@ -33,7 +33,7 @@
dropItemID = ''
}
"
@contextmenu.prevent="options?.tippy.show()"
@contextmenu.prevent="options?.tippy?.show()"
>
<div
class="flex min-w-0 flex-1 items-center justify-center"
@@ -73,6 +73,14 @@
class="hidden group-hover:inline-flex"
@click="emit('add-folder')"
/>
<HoppButtonSecondary
v-if="collectionsType === 'team-collections'"
v-tippy="{ theme: 'tooltip' }"
:icon="IconPlaySquare"
:title="t('collection_runner.run_collection')"
class="hidden group-hover:inline-flex"
@click="emit('run-collection', props.id)"
/>
<span>
<tippy
ref="options"
@@ -97,6 +105,7 @@
@keyup.delete="deleteAction?.$el.click()"
@keyup.x="exportAction?.$el.click()"
@keyup.p="propertiesAction?.$el.click()"
@keyup.t="runCollectionAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
@@ -172,6 +181,19 @@
}
"
/>
<HoppSmartItem
v-if="collectionsType === 'team-collections'"
ref="runCollectionAction"
:icon="IconPlaySquare"
:label="t('collection_runner.run_collection')"
:shortcut="['T']"
@click="
() => {
emit('run-collection', props.id)
hide()
}
"
/>
</div>
</template>
</tippy>
@@ -197,26 +219,27 @@
</template>
<script setup lang="ts">
import IconCheckCircle from "~icons/lucide/check-circle"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconFilePlus from "~icons/lucide/file-plus"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconDownload from "~icons/lucide/download"
import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open"
import IconSettings2 from "~icons/lucide/settings-2"
import { ref, computed, watch } from "vue"
import { HoppCollection } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
import { HoppCollection } from "@hoppscotch/data"
import { computed, ref, watch } from "vue"
import { TippyComponent } from "vue-tippy"
import { useReadonlyStream } from "~/composables/stream"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import {
changeCurrentReorderStatus,
currentReorderingStatus$,
} from "~/newstore/reordering"
import { useReadonlyStream } from "~/composables/stream"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconDownload from "~icons/lucide/download"
import IconEdit from "~icons/lucide/edit"
import IconFilePlus from "~icons/lucide/file-plus"
import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconPlaySquare from "~icons/lucide/play-square"
import IconSettings2 from "~icons/lucide/settings-2"
import IconTrash2 from "~icons/lucide/trash-2"
type CollectionType = "my-collections" | "team-collections"
type FolderType = "collection" | "folder"
@@ -267,16 +290,18 @@ const emit = defineEmits<{
(event: "dragging", payload: boolean): void
(event: "update-collection-order", payload: DataTransfer): void
(event: "update-last-collection-order", payload: DataTransfer): void
(event: "run-collection", collectionID: string): void
}>()
const tippyActions = ref<TippyComponent | null>(null)
const tippyActions = ref<HTMLDivElement | null>(null)
const requestAction = ref<HTMLButtonElement | null>(null)
const folderAction = ref<HTMLButtonElement | null>(null)
const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const exportAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null)
const propertiesAction = ref<TippyComponent | null>(null)
const propertiesAction = ref<HTMLButtonElement | null>(null)
const runCollectionAction = ref<HTMLButtonElement | null>(null)
const dragging = ref(false)
const ordering = ref(false)
@@ -319,7 +344,7 @@ watch(
() => props.exportLoading,
(val) => {
if (!val) {
options.value!.tippy.hide()
options.value!.tippy?.hide()
}
}
)

View File

@@ -12,7 +12,7 @@
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
render-inactive-tabs
>
<HoppSmartTab :id="'headers'" :label="`${t('tab.headers')}`">
<HoppSmartTab id="headers" :label="`${t('tab.headers')}`">
<HttpHeaders
v-model="editableCollection"
:is-collection-property="true"
@@ -24,15 +24,13 @@
{{ t("helpers.collection_properties_header") }}
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'authorization'"
:label="`${t('tab.authorization')}`"
>
<HoppSmartTab id="authorization" :label="`${t('tab.authorization')}`">
<HttpAuthorization
v-model="editableCollection.auth"
:is-collection-property="true"
:is-root-collection="editingProperties?.isRootCollection"
:inherited-properties="editingProperties?.inheritedProperties"
:is-root-collection="editingProperties.isRootCollection"
:inherited-properties="editingProperties.inheritedProperties"
:source="source"
/>
<div
@@ -42,23 +40,77 @@
{{ t("helpers.collection_properties_authorization") }}
</div>
</HoppSmartTab>
<HoppSmartTab
v-if="showDetails"
:id="'details'"
:label="t('collection.details')"
>
<div
class="flex flex-shrink-0 items-center justify-between border-b border-dividerLight bg-primary pl-4"
>
<span>{{ t("collection_runner.collection_id") }}</span>
<!-- TODO: Make it point to the section about accessing collections via the ID -->
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/clients/cli"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
</div>
<div class="p-4">
<div
class="flex items-center justify-between py-2 px-4 rounded-md bg-primaryLight select-text"
>
<div class="text-secondaryDark">
{{ editingProperties.path }}
</div>
<HoppButtonSecondary
filled
:icon="copyIcon"
@click="copyCollectionID"
/>
</div>
</div>
<div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
>
<icon-lucide-info class="svg-icons mr-2" />
{{ t("collection_runner.cli_collection_id_description") }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</template>
<template #footer>
<span class="flex space-x-2">
<div class="flex gap-x-2">
<HoppButtonPrimary
v-if="activeTabIsDetails"
:label="t('action.copy')"
:icon="copyIcon"
outline
filled
@click="copyCollectionID"
/>
<HoppButtonPrimary
v-else
:label="t('action.save')"
:loading="loadingState"
outline
@click="saveEditedCollection"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
:label="activeTabIsDetails ? t('action.close') : t('action.cancel')"
outline
filled
@click="hideModal"
/>
</span>
</div>
</template>
</HoppSmartModal>
</template>
@@ -72,13 +124,18 @@ import {
HoppRESTAuth,
HoppRESTHeaders,
} from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { refAutoReset, useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { clone } from "lodash-es"
import { ref, watch } from "vue"
import { computed, ref, watch } from "vue"
import { useToast } from "~/composables/toast"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { PersistenceService } from "~/services/persistence"
import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy"
import IconHelpCircle from "~icons/lucide/help-circle"
const persistenceService = useService(PersistenceService)
const t = useI18n()
@@ -93,18 +150,21 @@ export type EditingProperties = {
type HoppCollectionAuth = HoppRESTAuth | HoppGQLAuth
type HoppCollectionHeaders = HoppRESTHeaders | GQLHeader[]
const toast = useToast()
const props = withDefaults(
defineProps<{
show: boolean
loadingState: boolean
editingProperties: EditingProperties | null
editingProperties: EditingProperties
source: "REST" | "GraphQL"
modelValue: string
showDetails: boolean
}>(),
{
show: false,
loadingState: false,
editingProperties: null,
showDetails: false,
}
)
@@ -128,15 +188,22 @@ const editableCollection = ref<{
},
})
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const activeTabIsDetails = computed(() => activeTab.value === "details")
watch(
editableCollection,
(updatedEditableCollection) => {
if (props.show && props.editingProperties) {
const unsavedCollectionProperties: EditingProperties = {
collection: updatedEditableCollection,
isRootCollection: props.editingProperties?.isRootCollection ?? false,
path: props.editingProperties?.path,
inheritedProperties: props.editingProperties?.inheritedProperties,
isRootCollection: props.editingProperties.isRootCollection ?? false,
path: props.editingProperties.path,
inheritedProperties: props.editingProperties.inheritedProperties,
}
persistenceService.setLocalConfig(
"unsaved_collection_properties",
@@ -154,7 +221,13 @@ const activeTab = useVModel(props, "modelValue", emit)
watch(
() => props.show,
(show) => {
if (show && props.editingProperties?.collection) {
// `Details` tab doesn't exist for personal workspace, hence switching to the `Headers` tab
// The modal can appear empty while switching from a team workspace with `Details` as the active tab
if (activeTab.value === "details" && !props.showDetails) {
activeTab.value = "headers"
}
if (show && props.editingProperties.collection) {
editableCollection.value.auth = clone(
props.editingProperties.collection.auth as HoppCollectionAuth
)
@@ -194,4 +267,11 @@ const hideModal = () => {
persistenceService.removeLocalConfig("unsaved_collection_properties")
emit("hide-modal")
}
const copyCollectionID = () => {
copyToClipboard(props.editingProperties.path)
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
</script>

View File

@@ -0,0 +1,149 @@
<template>
<HoppSmartModal
dialog
:title="t('collection_runner.run_collection')"
@close="closeModal"
>
<template #body>
<HoppSmartTabs v-model="activeTab">
<HoppSmartTab id="cli" :label="t('collection_runner.cli')">
<div class="space-y-4 p-4">
<p
class="p-4 mb-4 border rounded-md text-amber-500 border-amber-600"
>
{{ cliCommandGenerationDescription }}
</p>
<div v-if="environmentID" class="flex gap-x-2 items-center">
<HoppSmartCheckbox
:on="includeEnvironmentID"
@change="toggleIncludeEnvironment"
/>
<span class="truncate"
>{{ t("collection_runner.include_active_environment") }}
<span class="text-secondaryDark">{{
activeEnvironment
}}</span></span
>
</div>
<div
class="p-4 rounded-md bg-primaryLight text-secondaryDark select-text"
>
{{ generatedCLICommand }}
</div>
</div>
</HoppSmartTab>
<HoppSmartTab id="runner" disabled :label="t('collection_runner.ui')" />
</HoppSmartTabs>
</template>
<template #footer>
<div class="flex space-x-2">
<HoppButtonPrimary
:label="`${t('action.copy')}`"
:icon="copyIcon"
outline
@click="copyCLICommandToClipboard"
/>
<HoppButtonSecondary
:label="`${t('action.close')}`"
outline
filled
@click="closeModal"
/>
</div>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { refAutoReset } from "@vueuse/core"
import { computed, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { SelectedEnvironmentIndex } from "~/newstore/environments"
import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
collectionID: string
environmentID?: string | null
selectedEnvironmentIndex: SelectedEnvironmentIndex
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const includeEnvironmentID = ref(false)
const activeTab = ref("cli")
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const activeEnvironment = computed(() => {
const selectedEnv = props.selectedEnvironmentIndex
if (selectedEnv.type === "TEAM_ENV") {
return selectedEnv.environment.name
}
return null
})
const isCloudInstance = window.location.hostname === "hoppscotch.io"
const cliCommandGenerationDescription = computed(() => {
if (isCloudInstance) {
return t("collection_runner.cli_command_generation_description_cloud")
}
if (import.meta.env.VITE_BACKEND_API_URL) {
return t("collection_runner.cli_command_generation_description_sh")
}
return t(
"collection_runner.cli_command_generation_description_sh_with_server_url_placeholder"
)
})
const generatedCLICommand = computed(() => {
const { collectionID, environmentID } = props
const environmentFlag =
includeEnvironmentID.value && environmentID ? `-e ${environmentID}` : ""
const serverUrl = import.meta.env.VITE_BACKEND_API_URL?.endsWith("/v1")
? // Removing `/v1` prefix
import.meta.env.VITE_BACKEND_API_URL.slice(0, -3)
: "<server_url>"
const serverFlag = isCloudInstance ? "" : `--server ${serverUrl}`
return `hopp test ${collectionID} ${environmentFlag} --token <access_token> ${serverFlag}`
})
const toggleIncludeEnvironment = () => {
includeEnvironmentID.value = !includeEnvironmentID.value
}
const copyCLICommandToClipboard = () => {
copyToClipboard(generatedCLICommand.value)
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const closeModal = () => {
emit("hide-modal")
}
</script>

View File

@@ -129,6 +129,7 @@
})
}
"
@run-collection="emit('run-collection', $event)"
@click="
() => {
handleCollectionClick({
@@ -218,6 +219,7 @@
})
}
"
@run-collection="emit('run-collection', $event)"
@click="
() => {
handleCollectionClick({
@@ -586,6 +588,7 @@ const emit = defineEmits<{
(event: "expand-team-collection", payload: string): void
(event: "display-modal-add"): void
(event: "display-modal-import-export"): void
(event: "run-collection", collectionID: string): void
}>()
const getPath = (path: string) => {

View File

@@ -94,6 +94,7 @@
@display-modal-add="displayModalAdd(true)"
@display-modal-import-export="displayModalImportExport(true)"
@collection-click="handleCollectionClick"
@run-collection="runCollectionHandler"
/>
<div
class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
@@ -164,42 +165,26 @@
v-model="collectionPropertiesModalActiveTab"
:show="showModalEditProperties"
:editing-properties="editingProperties"
:show-details="collectionsType.type === 'team-collections'"
source="REST"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
<!-- `selectedCollectionID` is guaranteed to be a string when `showCollectionsRunnerModal` is `true` -->
<CollectionsRunner
v-if="showCollectionsRunnerModal"
:collection-i-d="selectedCollectionID!"
:environment-i-d="activeEnvironmentID"
:selected-environment-index="selectedEnvironmentIndex"
@hide-modal="showCollectionsRunnerModal = false"
/>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, PropType, ref, watch } from "vue"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked"
import { useReadonlyStream } from "~/composables/stream"
import { useLocalState } from "~/newstore/localstate"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import {
addRESTCollection,
addRESTFolder,
editRESTCollection,
editRESTFolder,
editRESTRequest,
moveRESTRequest,
removeRESTCollection,
removeRESTFolder,
removeRESTRequest,
restCollections$,
saveRESTRequestAs,
updateRESTRequestOrder,
updateRESTCollectionOrder,
moveRESTFolder,
navigateToFolderWithIndexPath,
restCollectionStore,
cascadeParentCollectionForHeaderAuth,
} from "~/newstore/collections"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import { useToast } from "@composables/toast"
import {
HoppCollection,
HoppRESTAuth,
@@ -207,51 +192,82 @@ import {
HoppRESTRequest,
makeCollection,
} from "@hoppscotch/data"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { cloneDeep, debounce, isEqual } from "lodash-es"
import { PropType, computed, nextTick, onMounted, ref, watch } from "vue"
import { useReadonlyStream, useStream } from "~/composables/stream"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { GQLError } from "~/helpers/backend/GQLClient"
import {
createNewRootCollection,
getCompleteCollectionTree,
teamCollToHoppRESTColl,
} from "~/helpers/backend/helpers"
import {
createChildCollection,
createNewRootCollection,
deleteCollection,
moveRESTTeamCollection,
updateOrderRESTTeamCollection,
updateTeamCollection,
} from "~/helpers/backend/mutations/TeamCollection"
import {
updateTeamRequest,
createRequestInCollection,
deleteTeamRequest,
moveRESTTeamRequest,
updateOrderRESTTeamRequest,
updateTeamRequest,
} from "~/helpers/backend/mutations/TeamRequest"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { Collection as NodeCollection } from "./MyCollections.vue"
import {
getCompleteCollectionTree,
teamCollToHoppRESTColl,
} from "~/helpers/backend/helpers"
import { platform } from "~/platform"
getFoldersByPath,
resetTeamRequestsContext,
resolveSaveContextOnCollectionReorder,
updateInheritedPropertiesForAffectedRequests,
updateSaveContextForAffectedRequests,
} from "~/helpers/collection/collection"
import {
getRequestsByPath,
resolveSaveContextOnRequestReorder,
} from "~/helpers/collection/request"
import {
getFoldersByPath,
resolveSaveContextOnCollectionReorder,
updateSaveContextForAffectedRequests,
updateInheritedPropertiesForAffectedRequests,
resetTeamRequestsContext,
} from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { TeamSearchService } from "~/helpers/teams/TeamsSearch.service"
import { PersistenceService } from "~/services/persistence"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { Picked } from "~/helpers/types/HoppPicked"
import {
addRESTCollection,
addRESTFolder,
cascadeParentCollectionForHeaderAuth,
editRESTCollection,
editRESTFolder,
editRESTRequest,
moveRESTFolder,
moveRESTRequest,
navigateToFolderWithIndexPath,
removeRESTCollection,
removeRESTFolder,
removeRESTRequest,
restCollectionStore,
restCollections$,
saveRESTRequestAs,
updateRESTCollectionOrder,
updateRESTRequestOrder,
} from "~/newstore/collections"
import {
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import { useLocalState } from "~/newstore/localstate"
import { currentReorderingStatus$ } from "~/newstore/reordering"
import { platform } from "~/platform"
import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { PersistenceService } from "~/services/persistence"
import { RESTTabService } from "~/services/tab/rest"
import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { Collection as NodeCollection } from "./MyCollections.vue"
import { EditingProperties } from "./Properties.vue"
const t = useI18n()
@@ -339,6 +355,16 @@ const teamLoadingCollections = useReadonlyStream(
teamCollectionAdapter.loadingCollections$,
[]
)
const teamEnvironmentAdapter = new TeamEnvironmentAdapter(undefined)
const teamEnvironmentList = useReadonlyStream(
teamEnvironmentAdapter.teamEnvironmentList$,
[]
)
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
setSelectedEnvironmentIndex
)
const {
cascadeParentCollectionForHeaderAuthForSearchResults,
@@ -482,6 +508,8 @@ watch(
switchToMyCollections()
} else if (newWorkspace.type === "team") {
updateSelectedTeam(newWorkspace)
teamEnvironmentAdapter.changeTeamID(newWorkspace.teamID)
}
},
{
@@ -630,6 +658,10 @@ const showModalEditProperties = ref(false)
const showConfirmModal = ref(false)
const showTeamModalAdd = ref(false)
const showCollectionsRunnerModal = ref(false)
const selectedCollectionID = ref<string | null>(null)
const activeEnvironmentID = ref<string | null | undefined>(null)
const displayModalAdd = (show: boolean) => {
showModalAdd.value = show
@@ -2251,6 +2283,27 @@ const setCollectionProperties = (newCollection: {
displayModalEditProperties(false)
}
const runCollectionHandler = (collectionID: string) => {
selectedCollectionID.value = collectionID
showCollectionsRunnerModal.value = true
const activeWorkspace = workspace.value
const currentEnv = selectedEnvironmentIndex.value
if (["NO_ENV_SELECTED", "MY_ENV"].includes(currentEnv.type)) {
activeEnvironmentID.value = null
return
}
if (activeWorkspace.type === "team" && currentEnv.type === "TEAM_ENV") {
activeEnvironmentID.value = teamEnvironmentList.value.find(
(env) =>
env.teamID === activeWorkspace.teamID &&
env.environment.id === currentEnv.environment.id
)?.environment.id
}
}
const resolveConfirmModal = (title: string | null) => {
if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()

View File

@@ -0,0 +1,123 @@
<template>
<HoppSmartModal
dialog
:full-width-body="true"
:title="t('environment.properties')"
@close="hideModal"
>
<template #body>
<HoppSmartTabs
v-model="activeTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
render-inactive-tabs
>
<HoppSmartTab id="details" :label="t('environment.details')">
<div
class="flex flex-shrink-0 items-center justify-between border-b border-dividerLight bg-primary pl-4"
>
<span>{{ t("collection_runner.environment_id") }}</span>
<!-- TODO: Make it point to the section about accessing environments via the ID -->
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/clients/cli"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
</div>
<div class="p-4">
<div
class="flex items-center justify-between py-2 px-4 rounded-md bg-primaryLight select-text"
>
<div class="text-secondaryDark">
{{ environmentID }}
</div>
<HoppButtonSecondary
filled
:icon="copyTextIcon"
@click="copyText"
/>
</div>
</div>
<div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
>
<icon-lucide-info class="svg-icons mr-2" />
{{ t("collection_runner.cli_environment_id_description") }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</template>
<template #footer>
<div class="flex gap-x-2 items-center">
<HoppButtonPrimary
:label="t('action.copy')"
:icon="copyIcon"
outline
filled
@click="copyEnvironmentID"
/>
<HoppButtonSecondary
:label="t('action.close')"
outline
filled
@click="hideModal"
/>
</div>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { refAutoReset, useVModel } from "@vueuse/core"
import { toRef } from "vue"
import { useCopyResponse } from "~/composables/lens-actions"
import { useToast } from "~/composables/toast"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy"
import IconHelpCircle from "~icons/lucide/help-circle"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
modelValue: string
environmentID: string
}>()
const environmentIDRef = toRef(props, "environmentID")
const { copyIcon: copyTextIcon, copyResponse: copyText } =
useCopyResponse(environmentIDRef)
const emit = defineEmits<{
(e: "hide-modal"): void
(e: "update:modelValue"): void
}>()
const activeTab = useVModel(props, "modelValue", emit)
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const hideModal = () => {
emit("hide-modal")
}
const copyEnvironmentID = () => {
copyToClipboard(props.environmentID)
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
</script>

View File

@@ -40,7 +40,8 @@
@keyup.d="duplicate!.$el.click()"
@keyup.j="exportAsJsonEl!.$el.click()"
@keyup.delete="deleteAction!.$el.click()"
@keyup.escape="options!.tippy().hide()"
@keyup.p="propertiesAction!.$el.click()"
@keyup.escape="options!.tippy?.hide()"
>
<HoppSmartItem
ref="edit"
@@ -94,6 +95,18 @@
}
"
/>
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
:label="t('action.properties')"
:shortcut="['P']"
@click="
() => {
emit('show-environment-properties')
hide()
}
"
/>
</div>
</template>
</tippy>
@@ -108,26 +121,28 @@
</template>
<script setup lang="ts">
import { ref } from "vue"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { useToast } from "@composables/toast"
import { HoppSmartItem } from "@hoppscotch/ui"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { ref } from "vue"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient"
import {
deleteTeamEnvironment,
createDuplicateEnvironment as duplicateEnvironment,
} from "~/helpers/backend/mutations/TeamEnvironment"
import { GQLError } from "~/helpers/backend/GQLClient"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2"
import IconMoreVertical from "~icons/lucide/more-vertical"
import { TippyComponent } from "vue-tippy"
import { HoppSmartItem } from "@hoppscotch/ui"
import { exportAsJSON } from "~/helpers/import-export/export/environment"
import { useService } from "dioc/vue"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import IconCopy from "~icons/lucide/copy"
import IconEdit from "~icons/lucide/edit"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconSettings2 from "~icons/lucide/settings-2"
import IconTrash2 from "~icons/lucide/trash-2"
const t = useI18n()
const toast = useToast()
@@ -139,6 +154,7 @@ const props = defineProps<{
const emit = defineEmits<{
(e: "edit-environment"): void
(e: "show-environment-properties"): void
}>()
const secretEnvironmentService = useService(SecretEnvironmentService)
@@ -156,6 +172,7 @@ const edit = ref<typeof HoppSmartItem>()
const duplicate = ref<typeof HoppSmartItem>()
const deleteAction = ref<typeof HoppSmartItem>()
const exportAsJsonEl = ref<typeof HoppSmartItem>()
const propertiesAction = ref<typeof HoppSmartItem>()
const removeEnvironment = () => {
pipe(

View File

@@ -86,6 +86,9 @@
:environment="environment"
:is-viewer="team?.role === 'VIEWER'"
@edit-environment="editEnvironment(environment)"
@show-environment-properties="
showEnvironmentProperties(environment.environment.id)
"
/>
</div>
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
@@ -116,6 +119,12 @@
environment-type="TEAM_ENV"
@hide-modal="displayModalImportExport(false)"
/>
<EnvironmentsProperties
v-if="showEnvironmentsPropertiesModal"
v-model="environmentsPropertiesModalActiveTab"
:environment-i-d="selectedEnvironmentID!"
@hide-modal="showEnvironmentsPropertiesModal = false"
/>
</div>
</template>
@@ -149,6 +158,10 @@ const editingEnvironment = ref<TeamEnvironment | null>(null)
const editingVariableName = ref("")
const secretOptionSelected = ref(false)
const showEnvironmentsPropertiesModal = ref(false)
const environmentsPropertiesModalActiveTab = ref("details")
const selectedEnvironmentID = ref<string | null>(null)
const isTeamViewer = computed(() => props.team?.role === "VIEWER")
const displayModalAdd = (shouldDisplay: boolean) => {
@@ -187,6 +200,11 @@ const getErrorMessage = (err: GQLError<string>) => {
}
}
const showEnvironmentProperties = (environmentID: string) => {
showEnvironmentsPropertiesModal.value = true
selectedEnvironmentID.value = environmentID
}
defineActionHandler(
"modals.team.environment.edit",
({ envName, variableName, isSecret }) => {

View File

@@ -20,6 +20,7 @@ interface ImportMetaEnv {
readonly VITE_BACKEND_GQL_URL: string
readonly VITE_BACKEND_WS_URL: string
readonly VITE_BACKEND_API_URL: string
readonly VITE_SENTRY_DSN?: string
readonly VITE_SENTRY_ENVIRONMENT?: string