feat: CLI collection runner command generation UI flow (#4141)
Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
149
packages/hoppscotch-common/src/components/collections/Runner.vue
Normal file
149
packages/hoppscotch-common/src/components/collections/Runner.vue
Normal 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>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user