feat: mock server feature enhancements (#5609)
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
f834cc87d3
commit
77af577778
16 changed files with 2005 additions and 623 deletions
|
|
@ -1062,7 +1062,20 @@
|
|||
"response_headers": "Response Headers",
|
||||
"response_body": "Response Body",
|
||||
"log_deleted": "Log deleted successfully",
|
||||
"description": "Mock servers allow you to simulate API responses based on your collection's example responses."
|
||||
"description": "Mock servers allow you to simulate API responses based on your collection's example responses.",
|
||||
"set_in_environment": "Set in environment",
|
||||
"set_in_environment_hint": "The mock server URL will be automatically added as 'mockUrl' variable in the collection's environment",
|
||||
"environment_variable_added": "Mock URL added to environment",
|
||||
"environment_variable_updated": "Mock URL updated in environment",
|
||||
"environment_created_with_variable": "Environment created with mock URL",
|
||||
"create_example_collection": "Create example collection",
|
||||
"create_example_collection_hint": "Create a pet store example collection with sample requests (GET, POST, PUT, DELETE)",
|
||||
"creating_example_collection": "Creating example collection...",
|
||||
"failed_to_create_collection": "Failed to create example collection",
|
||||
"enable_example_collection_hint": "Please enable 'Create example collection' toggle for new collection mode",
|
||||
"new_collection_name_hint": "The collection will be created with the same name as your mock server",
|
||||
"existing_collection": "Existing Collection",
|
||||
"new_collection": "New Collection"
|
||||
},
|
||||
"preRequest": {
|
||||
"javascript_code": "JavaScript Code",
|
||||
|
|
|
|||
|
|
@ -241,15 +241,19 @@ declare module 'vue' {
|
|||
IconLucideCode2: typeof import('~icons/lucide/code2')['default']
|
||||
IconLucideEyeOff: typeof import('~icons/lucide/eye-off')['default']
|
||||
IconLucideFileQuestion: typeof import('~icons/lucide/file-question')['default']
|
||||
IconLucideFileText: typeof import('~icons/lucide/file-text')['default']
|
||||
IconLucideFolder: typeof import('~icons/lucide/folder')['default']
|
||||
IconLucideFolderOpen: typeof import('~icons/lucide/folder-open')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideLightbulb: typeof import('~icons/lucide/lightbulb')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
|
||||
IconLucideLoader2: typeof import('~icons/lucide/loader2')['default']
|
||||
IconLucideLock: typeof import('~icons/lucide/lock')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucidePlusCircle: typeof import('~icons/lucide/plus-circle')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
|
|
@ -285,9 +289,12 @@ declare module 'vue' {
|
|||
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
|
||||
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||
MockServerConfigureMockServerModal: typeof import('./components/mockServer/ConfigureMockServerModal.vue')['default']
|
||||
MockServerCreateMockServer: typeof import('./components/mockServer/CreateMockServer.vue')['default']
|
||||
MockServerCreateNewMockServerModal: typeof import('./components/mockServer/CreateNewMockServerModal.vue')['default']
|
||||
MockServerEditMockServer: typeof import('./components/mockServer/EditMockServer.vue')['default']
|
||||
MockServerLogSection: typeof import('./components/mockServer/LogSection.vue')['default']
|
||||
MockServerMockServerCreatedInfo: typeof import('./components/mockServer/MockServerCreatedInfo.vue')['default']
|
||||
MockServerMockServerDashboard: typeof import('./components/mockServer/MockServerDashboard.vue')['default']
|
||||
MockServerMockServerLogs: typeof import('./components/mockServer/MockServerLogs.vue')['default']
|
||||
MonacoScriptEditor: typeof import('./components/MonacoScriptEditor.vue')['default']
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { PropType, Ref, computed, ref } from "vue"
|
||||
import { transformCollectionForImport } from "~/helpers/collection/collection"
|
||||
|
||||
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||
import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
|
||||
|
|
@ -125,7 +126,7 @@ const importToPersonalWorkspace = async (collections: HoppCollection[]) => {
|
|||
if (currentUser.value) {
|
||||
try {
|
||||
const transformedCollection = collections.map((collection) =>
|
||||
translateToPersonalCollectionFormat(collection)
|
||||
transformCollectionForImport(collection)
|
||||
)
|
||||
|
||||
const res = await importUserCollectionsFromJSON(
|
||||
|
|
@ -162,52 +163,6 @@ const importToPersonalWorkspace = async (collections: HoppCollection[]) => {
|
|||
}
|
||||
}
|
||||
|
||||
function translateToTeamCollectionFormat(x: HoppCollection) {
|
||||
const folders: HoppCollection[] = (x.folders ?? []).map(
|
||||
translateToTeamCollectionFormat
|
||||
)
|
||||
|
||||
const data = {
|
||||
auth: x.auth,
|
||||
headers: x.headers,
|
||||
variables: x.variables,
|
||||
description: x.description,
|
||||
}
|
||||
|
||||
const obj = {
|
||||
...x,
|
||||
folders,
|
||||
data,
|
||||
}
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
function translateToPersonalCollectionFormat(x: HoppCollection) {
|
||||
const folders: HoppCollection[] = (x.folders ?? []).map(
|
||||
translateToPersonalCollectionFormat
|
||||
)
|
||||
|
||||
const data = {
|
||||
auth: x.auth,
|
||||
headers: x.headers,
|
||||
variables: x.variables,
|
||||
description: x.description,
|
||||
}
|
||||
|
||||
const obj = {
|
||||
...x,
|
||||
folders,
|
||||
data,
|
||||
}
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
const importToTeamsWorkspace = async (collections: HoppCollection[]) => {
|
||||
if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
|
||||
return E.left({
|
||||
|
|
@ -216,7 +171,7 @@ const importToTeamsWorkspace = async (collections: HoppCollection[]) => {
|
|||
}
|
||||
|
||||
const transformedCollection = collections.map((collection) =>
|
||||
translateToTeamCollectionFormat(collection)
|
||||
transformCollectionForImport(collection)
|
||||
)
|
||||
|
||||
const res = await toTeamsImporter(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import { HoppCollection } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { ref } from "vue"
|
||||
import { transformCollectionForImport } from "~/helpers/collection/collection"
|
||||
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
|
|
@ -242,7 +243,7 @@ const handleImportToStore = async (gqlCollections: HoppCollection[]) => {
|
|||
if (currentUser.value) {
|
||||
try {
|
||||
const transformedCollection = gqlCollections.map((collection) =>
|
||||
translateToPersonalCollectionFormat(collection)
|
||||
transformCollectionForImport(collection)
|
||||
)
|
||||
|
||||
const res = await importUserCollectionsFromJSON(
|
||||
|
|
@ -282,28 +283,6 @@ const handleImportToStore = async (gqlCollections: HoppCollection[]) => {
|
|||
}
|
||||
}
|
||||
|
||||
function translateToPersonalCollectionFormat(x: HoppCollection) {
|
||||
const folders: HoppCollection[] = (x.folders ?? []).map(
|
||||
translateToPersonalCollectionFormat
|
||||
)
|
||||
|
||||
const data = {
|
||||
auth: x.auth,
|
||||
headers: x.headers,
|
||||
variables: x.variables,
|
||||
}
|
||||
|
||||
const obj = {
|
||||
...x,
|
||||
folders,
|
||||
data,
|
||||
}
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): () => void
|
||||
}>()
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@
|
|||
@hide-modal="showCollectionsRunnerModal = false"
|
||||
/>
|
||||
|
||||
<MockServerCreateMockServer />
|
||||
<MockServerConfigureMockServerModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,313 @@
|
|||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="
|
||||
isExistingMockServer
|
||||
? t('mock_server.mock_server_configuration')
|
||||
: t('mock_server.create_mock_server')
|
||||
"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<!-- Collection Info (Read-only) -->
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("collection.title") }}
|
||||
</label>
|
||||
<div class="text-body text-secondary">
|
||||
{{ collectionName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing Mock Server Info -->
|
||||
<div v-if="isExistingMockServer" class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.mock_server_name") }}
|
||||
</label>
|
||||
<div class="text-body text-secondary">
|
||||
{{ existingMockServer?.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.base_url") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
class="flex-1 px-3 py-2 border border-divider rounded bg-primaryLight"
|
||||
>
|
||||
{{
|
||||
existingMockServer?.serverUrlPathBased ||
|
||||
existingMockServer?.serverUrlDomainBased ||
|
||||
""
|
||||
}}
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
existingMockServer?.serverUrlPathBased ||
|
||||
existingMockServer?.serverUrlDomainBased ||
|
||||
''
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("app.status") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="
|
||||
existingMockServer?.isActive
|
||||
? 'bg-green-600/20 text-green-500 border border-green-600/30'
|
||||
: 'text-secondary border border-secondaryLight'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full mr-2"
|
||||
:class="
|
||||
existingMockServer?.isActive
|
||||
? 'bg-green-400'
|
||||
: 'bg-secondaryLight'
|
||||
"
|
||||
></span>
|
||||
{{
|
||||
existingMockServer?.isActive
|
||||
? t("mockServer.dashboard.active")
|
||||
: t("mockServer.dashboard.inactive")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Mock Server Form -->
|
||||
<div v-else class="flex flex-col space-y-6">
|
||||
<HoppSmartInput
|
||||
v-model="mockServerName"
|
||||
v-focus
|
||||
:label="t('mock_server.mock_server_name')"
|
||||
input-styles="floating-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-48">
|
||||
<HoppSmartInput
|
||||
v-model="delayInMsVal"
|
||||
:label="t('mock_server.delay_ms')"
|
||||
type="number"
|
||||
input-styles="floating-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle :on="isPublic" @change="isPublic = !isPublic">
|
||||
{{ t("mock_server.make_public") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hint for private mock servers -->
|
||||
<div v-if="!isPublic" class="w-full mt-2 text-xs text-secondaryLight">
|
||||
{{ t("mock_server.private_access_hint") }}
|
||||
</div>
|
||||
|
||||
<!-- Set in Environment Toggle -->
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="setInEnvironment"
|
||||
@change="setInEnvironment = !setInEnvironment"
|
||||
>
|
||||
{{ t("mock_server.set_in_environment") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
<div
|
||||
v-if="setInEnvironment"
|
||||
class="w-full text-xs text-secondaryLight"
|
||||
>
|
||||
{{ t("mock_server.set_in_environment_hint") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MockServerCreatedInfo :mock-server="createdServer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<!-- Start/Stop Server Button for existing mock server -->
|
||||
<HoppButtonPrimary
|
||||
v-if="isExistingMockServer"
|
||||
:label="
|
||||
existingMockServer?.isActive
|
||||
? t('mock_server.stop_server')
|
||||
: t('mock_server.start_server')
|
||||
"
|
||||
:loading="loading"
|
||||
:icon="existingMockServer?.isActive ? IconSquare : IconPlay"
|
||||
@click="handleToggleMockServer"
|
||||
/>
|
||||
|
||||
<!-- Create Mock Server Button for new mock server -->
|
||||
<HoppButtonPrimary
|
||||
v-else
|
||||
:label="t('mock_server.create_mock_server')"
|
||||
:loading="loading"
|
||||
:disabled="!mockServerName.trim() || !collectionID"
|
||||
:icon="IconServer"
|
||||
@click="handleCreateMockServer"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { MockServer } from "~/helpers/backend/graphql"
|
||||
import { copyToClipboard as copyToClipboardHelper } from "~/helpers/utils/clipboard"
|
||||
import {
|
||||
mockServers$,
|
||||
showCreateMockServerModal$,
|
||||
} from "~/newstore/mockServers"
|
||||
import { useMockServer } from "~/composables/useMockServer"
|
||||
import MockServerCreatedInfo from "~/components/mockServer/MockServerCreatedInfo.vue"
|
||||
|
||||
// Icons
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
import IconServer from "~icons/lucide/server"
|
||||
import IconSquare from "~icons/lucide/square"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// Use the composable for shared logic
|
||||
const { createMockServer, toggleMockServer } = useMockServer()
|
||||
|
||||
// Modal state
|
||||
const modalData = useReadonlyStream(showCreateMockServerModal$, {
|
||||
show: false,
|
||||
collectionID: undefined,
|
||||
collectionName: undefined,
|
||||
})
|
||||
|
||||
const mockServers = useReadonlyStream(mockServers$, [])
|
||||
|
||||
// Component state
|
||||
const mockServerName = ref("")
|
||||
const loading = ref(false)
|
||||
const createdServer = ref<MockServer | null>(null)
|
||||
const delayInMsVal = ref<string>("0")
|
||||
const isPublic = ref<boolean>(true)
|
||||
const setInEnvironment = ref<boolean>(true)
|
||||
|
||||
// Props computed from modal data
|
||||
// This modal only shows when collectionID is provided (from collection context menu)
|
||||
const show = computed(
|
||||
() => modalData.value.show && !!modalData.value.collectionID
|
||||
)
|
||||
const collectionID = computed(() => modalData.value.collectionID)
|
||||
const collectionName = computed(
|
||||
() => modalData.value.collectionName || "Unknown Collection"
|
||||
)
|
||||
|
||||
// Find existing mock server for the collection
|
||||
const existingMockServer = computed(() => {
|
||||
const collId = collectionID.value
|
||||
if (!collId) return null
|
||||
return mockServers.value.find((server) => server.collectionID === collId)
|
||||
})
|
||||
|
||||
const isExistingMockServer = computed(() => !!existingMockServer.value)
|
||||
|
||||
// Copy functionality for existing mock server
|
||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
copyToClipboardHelper(text)
|
||||
copyIcon.value = IconCheck
|
||||
toast.success(t("state.copied_to_clipboard"))
|
||||
}
|
||||
|
||||
// Create new mock server
|
||||
const handleCreateMockServer = async () => {
|
||||
if (!mockServerName.value.trim() || !collectionID.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
const result = await createMockServer({
|
||||
mockServerName: mockServerName.value,
|
||||
collectionID: collectionID.value,
|
||||
delayInMs: Number(delayInMsVal.value) || 0,
|
||||
isPublic: isPublic.value,
|
||||
setInEnvironment: setInEnvironment.value,
|
||||
collectionName: collectionName.value,
|
||||
})
|
||||
|
||||
loading.value = false
|
||||
|
||||
if (result.success && result.server) {
|
||||
createdServer.value = result.server
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle mock server active state
|
||||
const handleToggleMockServer = async () => {
|
||||
if (!existingMockServer.value) return
|
||||
|
||||
loading.value = true
|
||||
await toggleMockServer(existingMockServer.value as any)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// Close modal
|
||||
const closeModal = () => {
|
||||
showCreateMockServerModal$.next({
|
||||
show: false,
|
||||
collectionID: undefined,
|
||||
collectionName: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// Reset form when modal opens/closes
|
||||
watch(show, (newShow) => {
|
||||
if (newShow) {
|
||||
mockServerName.value = ""
|
||||
loading.value = false
|
||||
delayInMsVal.value = "0"
|
||||
isPublic.value = true
|
||||
setInEnvironment.value = true
|
||||
createdServer.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,543 +0,0 @@
|
|||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="
|
||||
isExistingMockServer
|
||||
? t('mock_server.mock_server_configuration')
|
||||
: t('mock_server.create_mock_server')
|
||||
"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<!-- Collection Selector or Info -->
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("collection.title") }}
|
||||
</label>
|
||||
<!-- Collection Selector (when no collection is pre-selected) -->
|
||||
<div v-if="!collectionID && !isExistingMockServer" class="flex">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions?.focus()"
|
||||
>
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
class="flex flex-1 !justify-start rounded-none pr-8"
|
||||
:label="
|
||||
selectedCollectionName || t('mock_server.select_collection')
|
||||
"
|
||||
outline
|
||||
/>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartLink
|
||||
v-for="option in collectionOptions"
|
||||
:key="option.value"
|
||||
class="flex flex-1"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': option.disabled,
|
||||
}"
|
||||
@click="
|
||||
() => {
|
||||
if (!option.disabled) {
|
||||
selectCollection(option)
|
||||
hide()
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:label="option.label"
|
||||
:active-info-icon="selectedCollectionID === option.value"
|
||||
:info-icon="
|
||||
selectedCollectionID === option.value
|
||||
? IconCheck
|
||||
: option.hasMockServer
|
||||
? IconServer
|
||||
: null
|
||||
"
|
||||
:disabled="option.disabled"
|
||||
/>
|
||||
</HoppSmartLink>
|
||||
<div
|
||||
v-if="collectionOptions.length === 0"
|
||||
class="flex items-center justify-center px-4 py-8 text-secondaryLight"
|
||||
>
|
||||
{{ t("empty.collections") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</div>
|
||||
<!-- Collection Info (when collection is pre-selected) -->
|
||||
<div v-else class="text-body text-secondary">
|
||||
{{ collectionName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing Mock Server Info -->
|
||||
<div v-if="isExistingMockServer" class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.mock_server_name") }}
|
||||
</label>
|
||||
<div class="text-body text-secondary">
|
||||
{{ existingMockServer?.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.base_url") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
class="flex-1 px-3 py-2 border border-divider rounded bg-primaryLight"
|
||||
>
|
||||
{{
|
||||
existingMockServer?.serverUrlPathBased ||
|
||||
existingMockServer?.serverUrlDomainBased ||
|
||||
""
|
||||
}}
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
existingMockServer?.serverUrlPathBased ||
|
||||
existingMockServer?.serverUrlDomainBased ||
|
||||
''
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("app.status") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="
|
||||
existingMockServer?.isActive
|
||||
? 'bg-green-600/20 text-green-500 border border-green-600/30'
|
||||
: 'text-secondary border border-secondaryLight'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full mr-2"
|
||||
:class="
|
||||
existingMockServer?.isActive
|
||||
? 'bg-green-400'
|
||||
: 'bg-secondaryLight'
|
||||
"
|
||||
></span>
|
||||
{{
|
||||
existingMockServer?.isActive
|
||||
? t("mockServer.dashboard.active")
|
||||
: t("mockServer.dashboard.inactive")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Mock Server Form -->
|
||||
<div v-else class="flex flex-col space-y-6">
|
||||
<HoppSmartInput
|
||||
v-model="mockServerName"
|
||||
v-focus
|
||||
:label="t('mock_server.mock_server_name')"
|
||||
input-styles="floating-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-48">
|
||||
<HoppSmartInput
|
||||
v-model="delayInMsVal"
|
||||
:label="t('mock_server.delay_ms')"
|
||||
type="number"
|
||||
input-styles="floating-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle :on="isPublic" @change="isPublic = !isPublic">
|
||||
{{ t("mock_server.make_public") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hint for private mock servers -->
|
||||
<div v-if="!isPublic" class="w-full mt-2 text-xs text-secondaryLight">
|
||||
{{ t("mock_server.private_access_hint") }}
|
||||
</div>
|
||||
|
||||
<!-- Display created server info -->
|
||||
<div v-if="createdServer" class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.path_based_url") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
class="flex-1 px-3 py-2 border border-divider rounded bg-primaryLight text-body font-mono"
|
||||
>
|
||||
{{ createdServer.serverUrlPathBased }}
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="
|
||||
copyToClipboard(createdServer.serverUrlPathBased || '')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subdomain-based URL (May be null) -->
|
||||
<div
|
||||
v-if="createdServer.serverUrlDomainBased"
|
||||
class="flex flex-col space-y-2"
|
||||
>
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.subdomain_based_url") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
class="flex-1 px-3 py-2 border border-divider rounded bg-primaryLight text-body font-mono"
|
||||
>
|
||||
{{ createdServer.serverUrlDomainBased }}
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="copyToClipboard(createdServer.serverUrlDomainBased)"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-secondaryLight">
|
||||
<span class="font-medium">{{ t("mock_server.note") }}:</span>
|
||||
{{ t("mock_server.subdomain_note") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Text -->
|
||||
<div
|
||||
class="py-4 px-3 bg-primaryLight rounded-md border border-dividerLight shadow-sm"
|
||||
>
|
||||
<p class="text-secondary flex space-x-2 items-start">
|
||||
<Icon-lucide-info class="svg-icons text-accent" />
|
||||
<span>
|
||||
{{ t("mock_server.description") }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<!-- Start/Stop Server Button for existing mock server -->
|
||||
<HoppButtonPrimary
|
||||
v-if="isExistingMockServer"
|
||||
:label="
|
||||
existingMockServer?.isActive
|
||||
? t('mock_server.stop_server')
|
||||
: t('mock_server.start_server')
|
||||
"
|
||||
:loading="loading"
|
||||
:icon="existingMockServer?.isActive ? IconSquare : IconPlay"
|
||||
@click="toggleMockServer"
|
||||
/>
|
||||
|
||||
<!-- Create Mock Server Button for new mock server -->
|
||||
<HoppButtonPrimary
|
||||
v-else
|
||||
:label="t('mock_server.create_mock_server')"
|
||||
:loading="loading"
|
||||
:disabled="!mockServerName.trim() || !effectiveCollectionID"
|
||||
:icon="IconServer"
|
||||
@click="createMockServer"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { MockServer, WorkspaceType } from "~/helpers/backend/graphql"
|
||||
import {
|
||||
createMockServer as createMockServerMutation,
|
||||
updateMockServer,
|
||||
} from "~/helpers/backend/mutations/MockServer"
|
||||
import { copyToClipboard as copyToClipboardHelper } from "~/helpers/utils/clipboard"
|
||||
import { restCollections$ } from "~/newstore/collections"
|
||||
import {
|
||||
addMockServer,
|
||||
mockServers$,
|
||||
showCreateMockServerModal$,
|
||||
updateMockServer as updateMockServerInStore,
|
||||
} from "~/newstore/mockServers"
|
||||
import { TeamCollectionsService } from "~/services/team-collection.service"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
// Icons
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
import IconServer from "~icons/lucide/server"
|
||||
import IconSquare from "~icons/lucide/square"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const teamCollectionsService = useService(TeamCollectionsService)
|
||||
|
||||
// Modal state
|
||||
const modalData = useReadonlyStream(showCreateMockServerModal$, {
|
||||
show: false,
|
||||
collectionID: undefined,
|
||||
collectionName: undefined,
|
||||
})
|
||||
|
||||
const mockServers = useReadonlyStream(mockServers$, [])
|
||||
const collections = useReadonlyStream(restCollections$, [])
|
||||
const currentWorkspace = computed(() => workspaceService.currentWorkspace.value)
|
||||
|
||||
// Get collections based on current workspace
|
||||
const availableCollections = computed(() => {
|
||||
if (currentWorkspace.value.type === "team" && currentWorkspace.value.teamID) {
|
||||
return teamCollectionsService.collections.value || []
|
||||
}
|
||||
return collections.value
|
||||
})
|
||||
|
||||
// Component state
|
||||
const mockServerName = ref("")
|
||||
const loading = ref(false)
|
||||
const showCloseButton = ref(false)
|
||||
const createdServer = ref<MockServer | null>(null)
|
||||
const delayInMsVal = ref<string>("0")
|
||||
const isPublic = ref<boolean>(true)
|
||||
const selectedCollectionID = ref("")
|
||||
const selectedCollectionName = ref("")
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
|
||||
// Props computed from modal data
|
||||
const show = computed(() => modalData.value.show)
|
||||
const collectionID = computed(() => modalData.value.collectionID)
|
||||
const collectionName = computed(() => {
|
||||
// Prefer name provided by modalData (pre-selected from caller)
|
||||
if (modalData.value.collectionName) return modalData.value.collectionName
|
||||
|
||||
// If user selected a collection inside the modal, use that
|
||||
if (selectedCollectionName.value) return selectedCollectionName.value
|
||||
|
||||
// Try finding the collection from availableCollections using effectiveCollectionID
|
||||
const id = effectiveCollectionID.value
|
||||
if (!id) return "Unknown Collection"
|
||||
|
||||
const coll = availableCollections.value.find((c: any) => (c as any).id === id)
|
||||
return (coll as any)?.name || (coll as any)?.title || "Unknown Collection"
|
||||
})
|
||||
|
||||
// Find existing mock server for the effective collection (pre-selected or user-selected)
|
||||
const existingMockServer = computed(() => {
|
||||
const collId = effectiveCollectionID.value
|
||||
if (!collId) return null
|
||||
return mockServers.value.find((server) => server.collectionID === collId)
|
||||
})
|
||||
|
||||
const isExistingMockServer = computed(() => !!existingMockServer.value)
|
||||
|
||||
// Collection options for the selector (only root collections)
|
||||
const collectionOptions = computed(() => {
|
||||
return availableCollections.value.map((collection) => {
|
||||
const collectionId =
|
||||
currentWorkspace.value.type === "team"
|
||||
? collection.id
|
||||
: (collection.id ?? collection._ref_id) // TODO: fix this fallback logic for personal workspaces in the future
|
||||
const hasMockServer = mockServers.value.some(
|
||||
(server) => server.collectionID === collectionId
|
||||
)
|
||||
|
||||
return {
|
||||
label: collection.name || collection.title,
|
||||
value: collectionId,
|
||||
collection: collection,
|
||||
hasMockServer: hasMockServer,
|
||||
disabled: hasMockServer,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Get the effective collection ID (either pre-selected or user-selected)
|
||||
const effectiveCollectionID = computed(() => {
|
||||
return collectionID.value || selectedCollectionID.value
|
||||
})
|
||||
|
||||
// Collection selection handler
|
||||
const selectCollection = (option: any) => {
|
||||
// Prevent selection of collections that already have mock servers
|
||||
if (option.disabled || option.hasMockServer) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedCollectionID.value = option.value
|
||||
selectedCollectionName.value = option.label
|
||||
}
|
||||
|
||||
// Copy functionality
|
||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
copyToClipboardHelper(text)
|
||||
copyIcon.value = IconCheck
|
||||
toast.success(t("state.copied_to_clipboard"))
|
||||
}
|
||||
|
||||
// Reset form when modal opens/closes
|
||||
watch(show, (newShow) => {
|
||||
if (newShow) {
|
||||
mockServerName.value = ""
|
||||
loading.value = false
|
||||
delayInMsVal.value = "0"
|
||||
isPublic.value = true
|
||||
selectedCollectionID.value = ""
|
||||
selectedCollectionName.value = ""
|
||||
showCloseButton.value = false
|
||||
createdServer.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// Create new mock server
|
||||
const createMockServer = async () => {
|
||||
if (!mockServerName.value.trim() || !effectiveCollectionID.value) {
|
||||
if (!effectiveCollectionID.value) {
|
||||
toast.error(t("mock_server.select_collection_error"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
// Determine workspace type and ID based on current workspace
|
||||
const workspaceType =
|
||||
currentWorkspace.value.type === "team"
|
||||
? WorkspaceType.Team
|
||||
: WorkspaceType.User
|
||||
const workspaceID =
|
||||
currentWorkspace.value.type === "team"
|
||||
? currentWorkspace.value.teamID
|
||||
: undefined
|
||||
|
||||
await pipe(
|
||||
createMockServerMutation(
|
||||
mockServerName.value.trim(),
|
||||
effectiveCollectionID.value,
|
||||
workspaceType,
|
||||
workspaceID,
|
||||
Number(delayInMsVal.value) || 0, // delayInMs
|
||||
Boolean(isPublic.value) // isPublic
|
||||
),
|
||||
TE.match(
|
||||
(error) => {
|
||||
// `error` here is the message string produced by the mutation helper.
|
||||
// Show the backend-provided error message if available, otherwise fallback to generic
|
||||
toast.error(String(error) || t("error.something_went_wrong"))
|
||||
loading.value = false
|
||||
},
|
||||
(result) => {
|
||||
toast.success(t("mock_server.mock_server_created"))
|
||||
|
||||
// Add the new mock server to the store
|
||||
addMockServer(result)
|
||||
|
||||
// Store the created server data and show close button
|
||||
createdServer.value = result
|
||||
showCloseButton.value = true
|
||||
|
||||
loading.value = false
|
||||
// Don't close the modal automatically
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
// Toggle mock server active state
|
||||
const toggleMockServer = async () => {
|
||||
if (!existingMockServer.value) return
|
||||
|
||||
loading.value = true
|
||||
const newActiveState = !existingMockServer.value.isActive
|
||||
|
||||
await pipe(
|
||||
updateMockServer(existingMockServer.value.id, { isActive: newActiveState }),
|
||||
TE.match(
|
||||
() => {
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
loading.value = false
|
||||
},
|
||||
() => {
|
||||
toast.success(
|
||||
newActiveState
|
||||
? t("mock_server.mock_server_started")
|
||||
: t("mock_server.mock_server_stopped")
|
||||
)
|
||||
|
||||
// Update the mock server in the store
|
||||
updateMockServerInStore(existingMockServer.value!.id, {
|
||||
isActive: newActiveState,
|
||||
})
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
// Close modal function
|
||||
const closeModal = () => {
|
||||
showCreateMockServerModal$.next({
|
||||
show: false,
|
||||
collectionID: undefined,
|
||||
collectionName: undefined,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('mock_server.create_mock_server')"
|
||||
@close="closeModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<!-- Collection Selection Mode (hidden after server is created) -->
|
||||
<div v-if="!createdServer" class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("collection.title") }}
|
||||
</label>
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
<!-- Radio buttons for collection selection mode -->
|
||||
<div class="flex space-x-6">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
v-model="collectionSelectionMode"
|
||||
type="radio"
|
||||
value="existing"
|
||||
class="w-4 h-4 text-accent border-divider focus:ring-accent"
|
||||
/>
|
||||
<span class="text-body text-secondary">
|
||||
{{ t("mock_server.existing_collection") }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
v-model="collectionSelectionMode"
|
||||
type="radio"
|
||||
value="new"
|
||||
class="w-4 h-4 text-accent border-divider focus:ring-accent"
|
||||
/>
|
||||
<span class="text-body text-secondary">
|
||||
{{ t("mock_server.new_collection") }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Collection dropdown (shown for existing collection mode) -->
|
||||
<div v-if="collectionSelectionMode === 'existing'" class="flex">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions?.focus()"
|
||||
>
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
class="flex flex-1 !justify-start rounded-none pr-8"
|
||||
:label="
|
||||
selectedCollectionName ||
|
||||
t('mock_server.select_collection')
|
||||
"
|
||||
outline
|
||||
/>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartLink
|
||||
v-for="option in collectionOptions"
|
||||
:key="option.value"
|
||||
class="flex flex-1"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': option.disabled,
|
||||
}"
|
||||
@click="
|
||||
() => {
|
||||
if (!option.disabled) {
|
||||
selectCollection(option)
|
||||
hide()
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:label="option.label"
|
||||
:active-info-icon="
|
||||
selectedCollectionID === option.value
|
||||
"
|
||||
:info-icon="
|
||||
selectedCollectionID === option.value
|
||||
? IconCheck
|
||||
: option.hasMockServer
|
||||
? IconServer
|
||||
: null
|
||||
"
|
||||
:disabled="option.disabled"
|
||||
/>
|
||||
</HoppSmartLink>
|
||||
<div
|
||||
v-if="collectionOptions.length === 0"
|
||||
class="flex items-center justify-center px-4 py-8 text-secondaryLight"
|
||||
>
|
||||
{{ t("empty.collections") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show selected collection name after server is created -->
|
||||
<div v-else class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("collection.title") }}
|
||||
</label>
|
||||
<div class="text-body text-secondary">
|
||||
{{ selectedCollectionName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mock Server Form (Before Creation) -->
|
||||
<div v-if="!createdServer" class="flex flex-col gap-6">
|
||||
<div>
|
||||
<HoppSmartInput
|
||||
v-model="mockServerName"
|
||||
v-focus
|
||||
:label="t('mock_server.mock_server_name')"
|
||||
input-styles="floating-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<!-- Hint for new collection mode -->
|
||||
<div
|
||||
v-if="collectionSelectionMode === 'new'"
|
||||
class="w-full text-xs text-secondaryLight mt-3"
|
||||
>
|
||||
{{ t("mock_server.new_collection_name_hint") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-48">
|
||||
<HoppSmartInput
|
||||
v-model="delayInMsVal"
|
||||
:label="t('mock_server.delay_ms')"
|
||||
type="number"
|
||||
input-styles="floating-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle :on="isPublic" @change="isPublic = !isPublic">
|
||||
{{ t("mock_server.make_public") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hint for private mock servers -->
|
||||
<div v-if="!isPublic" class="w-full mt-2 text-xs text-secondaryLight">
|
||||
{{ t("mock_server.private_access_hint") }}
|
||||
</div>
|
||||
|
||||
<!-- Set in Environment Toggle -->
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="setInEnvironment"
|
||||
@change="setInEnvironment = !setInEnvironment"
|
||||
>
|
||||
{{ t("mock_server.set_in_environment") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
<div
|
||||
v-if="setInEnvironment"
|
||||
class="w-full text-xs text-secondaryLight"
|
||||
>
|
||||
{{ t("mock_server.set_in_environment_hint") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Example Collection Toggle (only when "new collection" is selected) -->
|
||||
<div
|
||||
v-if="collectionSelectionMode === 'new'"
|
||||
class="flex flex-col space-y-2"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<HoppSmartToggle
|
||||
:on="createExampleCollection"
|
||||
@change="createExampleCollection = !createExampleCollection"
|
||||
>
|
||||
{{ t("mock_server.create_example_collection") }}
|
||||
</HoppSmartToggle>
|
||||
</div>
|
||||
<div
|
||||
v-if="createExampleCollection"
|
||||
class="w-full text-xs text-secondaryLight"
|
||||
>
|
||||
{{ t("mock_server.create_example_collection_hint") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mock Server Created Info (After Creation) -->
|
||||
<div v-else class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.mock_server_name") }}
|
||||
</label>
|
||||
<div class="text-body text-secondary">
|
||||
{{ createdServer.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("app.status") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="
|
||||
createdServer.isActive
|
||||
? 'bg-green-600/20 text-green-500 border border-green-600/30'
|
||||
: 'text-secondary border border-secondaryLight'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full mr-2"
|
||||
:class="
|
||||
createdServer.isActive
|
||||
? 'bg-green-400'
|
||||
: 'bg-secondaryLight'
|
||||
"
|
||||
></span>
|
||||
{{
|
||||
createdServer.isActive
|
||||
? t("mockServer.dashboard.active")
|
||||
: t("mockServer.dashboard.inactive")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MockServerCreatedInfo :mock-server="createdServer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<!-- Start/Stop Server Button (after creation) -->
|
||||
<HoppButtonPrimary
|
||||
v-if="createdServer"
|
||||
:label="
|
||||
createdServer.isActive
|
||||
? t('mock_server.stop_server')
|
||||
: t('mock_server.start_server')
|
||||
"
|
||||
:loading="loading"
|
||||
:icon="createdServer.isActive ? IconSquare : IconPlay"
|
||||
@click="handleToggleMockServer"
|
||||
/>
|
||||
|
||||
<!-- Create Mock Server Button (before creation) -->
|
||||
<HoppButtonPrimary
|
||||
v-else
|
||||
:label="t('mock_server.create_mock_server')"
|
||||
:loading="loading"
|
||||
:disabled="
|
||||
!mockServerName.trim() ||
|
||||
(!effectiveCollectionID &&
|
||||
collectionSelectionMode === 'existing') ||
|
||||
(collectionSelectionMode === 'new' && !createExampleCollection)
|
||||
"
|
||||
:icon="IconServer"
|
||||
@click="handleCreateMockServer"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { MockServer } from "~/helpers/backend/graphql"
|
||||
import { showCreateMockServerModal$ } from "~/newstore/mockServers"
|
||||
import { useMockServer } from "~/composables/useMockServer"
|
||||
import MockServerCreatedInfo from "~/components/mockServer/MockServerCreatedInfo.vue"
|
||||
import { useService } from "dioc/vue"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import {
|
||||
createMockCollectionForTeam,
|
||||
createMockCollectionForPersonal,
|
||||
} from "~/helpers/mockServer/exampleMockCollection"
|
||||
|
||||
// Icons
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
import IconServer from "~icons/lucide/server"
|
||||
import IconSquare from "~icons/lucide/square"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// Use the composable for shared logic
|
||||
const {
|
||||
mockServers,
|
||||
availableCollections,
|
||||
createMockServer,
|
||||
toggleMockServer,
|
||||
} = useMockServer()
|
||||
|
||||
// Services
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
|
||||
// Current workspace
|
||||
const currentWorkspace = computed(() => workspaceService.currentWorkspace.value)
|
||||
|
||||
// Modal state
|
||||
const modalData = useReadonlyStream(showCreateMockServerModal$, {
|
||||
show: false,
|
||||
collectionID: undefined,
|
||||
collectionName: undefined,
|
||||
})
|
||||
|
||||
// Component state
|
||||
const mockServerName = ref("")
|
||||
const loading = ref(false)
|
||||
const createdServer = ref<MockServer | null>(null)
|
||||
const delayInMsVal = ref<string>("0")
|
||||
const isPublic = ref<boolean>(true)
|
||||
const setInEnvironment = ref<boolean>(true)
|
||||
const createExampleCollection = ref<boolean>(false)
|
||||
const selectedCollectionID = ref("")
|
||||
const selectedCollectionName = ref("")
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const collectionSelectionMode = ref<"new" | "existing">("existing")
|
||||
|
||||
// Props computed from modal data
|
||||
// This modal only shows when collectionID is NOT provided (from dashboard "New" button)
|
||||
const show = computed(
|
||||
() => modalData.value.show && !modalData.value.collectionID
|
||||
)
|
||||
|
||||
// Collection options for the selector (only root collections)
|
||||
const collectionOptions = computed(() => {
|
||||
return availableCollections.value.map((collection: any) => {
|
||||
const collectionId = collection.id ?? collection._ref_id
|
||||
const hasMockServer = mockServers.value.some(
|
||||
(server) => server.collectionID === collectionId
|
||||
)
|
||||
|
||||
return {
|
||||
label: collection.name || collection.title,
|
||||
value: collectionId,
|
||||
collection: collection,
|
||||
hasMockServer: hasMockServer,
|
||||
disabled: hasMockServer,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Get the effective collection ID (user-selected)
|
||||
const effectiveCollectionID = computed(() => {
|
||||
return selectedCollectionID.value
|
||||
})
|
||||
|
||||
// Get collection name
|
||||
const collectionName = computed(() => {
|
||||
if (selectedCollectionName.value) return selectedCollectionName.value
|
||||
return "Unknown Collection"
|
||||
})
|
||||
|
||||
// Collection selection handler
|
||||
const selectCollection = (option: any) => {
|
||||
// Prevent selection of collections that already have mock servers
|
||||
if (option.disabled || option.hasMockServer) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedCollectionID.value = option.value
|
||||
selectedCollectionName.value = option.label
|
||||
}
|
||||
|
||||
// Function to create an example collection and return its ID and name
|
||||
const createExampleCollectionAndGetID = async (
|
||||
collectionName: string
|
||||
): Promise<{
|
||||
id: string
|
||||
name: string
|
||||
}> => {
|
||||
const workspaceType = currentWorkspace.value.type
|
||||
|
||||
if (workspaceType === "personal") {
|
||||
// For personal workspace
|
||||
const result = await createMockCollectionForPersonal(collectionName)
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
throw new Error(result.left)
|
||||
}
|
||||
|
||||
return result.right
|
||||
} else if (workspaceType === "team" && currentWorkspace.value.teamID) {
|
||||
// For team workspace
|
||||
const teamID = currentWorkspace.value.teamID
|
||||
const result = await createMockCollectionForTeam(teamID, collectionName)
|
||||
|
||||
if (E.isLeft(result)) {
|
||||
throw new Error(result.left)
|
||||
}
|
||||
|
||||
// Wait a bit for the subscription to update
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
return result.right
|
||||
}
|
||||
|
||||
throw new Error("Unknown workspace type")
|
||||
}
|
||||
|
||||
// Create new mock server
|
||||
const handleCreateMockServer = async () => {
|
||||
// Validate mock server name first
|
||||
if (!mockServerName.value.trim()) {
|
||||
toast.error(t("mock_server.provide_mock_server_name"))
|
||||
return
|
||||
}
|
||||
|
||||
// Start loading and show creating message
|
||||
loading.value = true
|
||||
|
||||
// If "new collection" mode is selected, create example collection (if toggle is enabled)
|
||||
let collectionIDToUse = effectiveCollectionID.value
|
||||
|
||||
if (collectionSelectionMode.value === "new") {
|
||||
if (createExampleCollection.value) {
|
||||
try {
|
||||
// Silently create the collection in the background
|
||||
const newCollection = await createExampleCollectionAndGetID(
|
||||
mockServerName.value.trim()
|
||||
)
|
||||
|
||||
// Update the selected collection with the actual created collection's ID and name
|
||||
collectionIDToUse = newCollection.id
|
||||
selectedCollectionID.value = newCollection.id
|
||||
selectedCollectionName.value = newCollection.name
|
||||
} catch (error) {
|
||||
console.error("Failed to create collection:", error)
|
||||
// If collection creation fails, stop the entire process
|
||||
toast.error(t("mock_server.failed_to_create_mock_server"))
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If new collection mode but example collection is not enabled
|
||||
toast.error(t("mock_server.enable_example_collection_hint"))
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate collection ID
|
||||
if (!collectionIDToUse) {
|
||||
toast.error(t("mock_server.select_collection_error"))
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Wait a bit more to ensure collection is fully available in the system
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
// Now create the mock server
|
||||
const result = await createMockServer({
|
||||
mockServerName: mockServerName.value,
|
||||
collectionID: collectionIDToUse,
|
||||
delayInMs: Number(delayInMsVal.value) || 0,
|
||||
isPublic: isPublic.value,
|
||||
setInEnvironment: setInEnvironment.value,
|
||||
collectionName: collectionName.value,
|
||||
})
|
||||
|
||||
loading.value = false
|
||||
|
||||
if (result.success && result.server) {
|
||||
createdServer.value = result.server
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle mock server active state
|
||||
const handleToggleMockServer = async () => {
|
||||
if (!createdServer.value) return
|
||||
|
||||
loading.value = true
|
||||
const result = await toggleMockServer(createdServer.value as any)
|
||||
loading.value = false
|
||||
|
||||
// Update the local `createdServer` state with the toggled state
|
||||
if (result.success && createdServer.value) {
|
||||
createdServer.value = {
|
||||
...createdServer.value,
|
||||
isActive: !createdServer.value.isActive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal
|
||||
const closeModal = () => {
|
||||
showCreateMockServerModal$.next({
|
||||
show: false,
|
||||
collectionID: undefined,
|
||||
collectionName: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// Reset form when modal opens/closes
|
||||
watch(show, (newShow) => {
|
||||
if (newShow) {
|
||||
mockServerName.value = ""
|
||||
loading.value = false
|
||||
delayInMsVal.value = "0"
|
||||
isPublic.value = true
|
||||
setInEnvironment.value = true
|
||||
createExampleCollection.value = false
|
||||
selectedCollectionID.value = ""
|
||||
selectedCollectionName.value = ""
|
||||
createdServer.value = null
|
||||
collectionSelectionMode.value = "existing"
|
||||
}
|
||||
})
|
||||
|
||||
// Auto-enable example collection toggle when switching to "new" mode
|
||||
watch(collectionSelectionMode, (newMode) => {
|
||||
if (newMode === "new") {
|
||||
createExampleCollection.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Display created server info -->
|
||||
<div v-if="mockServer" class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.path_based_url") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
class="flex-1 px-3 py-2 border border-divider rounded bg-primaryLight text-body font-mono"
|
||||
>
|
||||
{{ mockServer.serverUrlPathBased }}
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="handleCopy(mockServer.serverUrlPathBased || '')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subdomain-based URL (May be null) -->
|
||||
<div
|
||||
v-if="mockServer.serverUrlDomainBased"
|
||||
class="flex flex-col space-y-2"
|
||||
>
|
||||
<label class="text-sm font-semibold text-secondaryDark">
|
||||
{{ t("mock_server.subdomain_based_url") }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
class="flex-1 px-3 py-2 border border-divider rounded bg-primaryLight text-body font-mono"
|
||||
>
|
||||
{{ mockServer.serverUrlDomainBased }}
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="handleCopy(mockServer.serverUrlDomainBased || '')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Text -->
|
||||
<div
|
||||
class="py-4 px-3 bg-primaryLight rounded-md border border-dividerLight shadow-sm"
|
||||
>
|
||||
<p class="text-secondary flex space-x-2 items-start">
|
||||
<Icon-lucide-info class="svg-icons text-accent" />
|
||||
<span>
|
||||
{{ t("mock_server.description") }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { MockServer } from "~/helpers/backend/graphql"
|
||||
import { copyToClipboard as copyToClipboardHelper } from "~/helpers/utils/clipboard"
|
||||
|
||||
// Icons
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
|
||||
interface Props {
|
||||
mockServer: MockServer | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// Copy functionality
|
||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
copyToClipboardHelper(text)
|
||||
copyIcon.value = IconCheck
|
||||
toast.success(t("state.copied_to_clipboard"))
|
||||
}
|
||||
</script>
|
||||
|
|
@ -177,11 +177,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<MockServerCreateMockServer
|
||||
v-if="showCreateModal"
|
||||
:show="showCreateModal"
|
||||
@hide-modal="showCreateModal = false"
|
||||
/>
|
||||
<MockServerCreateNewMockServerModal />
|
||||
<MockServerEditMockServer
|
||||
v-if="showEditModal && selectedMockServer"
|
||||
:show="showEditModal"
|
||||
|
|
@ -223,7 +219,6 @@ import {
|
|||
updateMockServer as updateMockServerInStore,
|
||||
} from "~/newstore/mockServers"
|
||||
|
||||
import MockServerCreateMockServer from "~/components/mockServer/CreateMockServer.vue"
|
||||
import MockServerEditMockServer from "~/components/mockServer/EditMockServer.vue"
|
||||
import MockServerLogs from "~/components/mockServer/MockServerLogs.vue"
|
||||
import {
|
||||
|
|
@ -248,7 +243,6 @@ const toast = useToast()
|
|||
const colorMode = useColorMode()
|
||||
const { mockServers } = useMockServerStatus()
|
||||
const loading = ref(false)
|
||||
const showCreateModal = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const showLogsModal = ref(false)
|
||||
const selectedMockServer = ref<MockServer | null>(null)
|
||||
|
|
|
|||
302
packages/hoppscotch-common/src/composables/useMockServer.ts
Normal file
302
packages/hoppscotch-common/src/composables/useMockServer.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useService } from "dioc/vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { computed } from "vue"
|
||||
import { MockServer, WorkspaceType } from "~/helpers/backend/graphql"
|
||||
import {
|
||||
createMockServer as createMockServerMutation,
|
||||
updateMockServer,
|
||||
} from "~/helpers/backend/mutations/MockServer"
|
||||
import {
|
||||
createTeamEnvironment,
|
||||
updateTeamEnvironment,
|
||||
} from "~/helpers/backend/mutations/TeamEnvironment"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { restCollections$ } from "~/newstore/collections"
|
||||
import {
|
||||
addEnvironmentVariable,
|
||||
createEnvironment,
|
||||
environments$,
|
||||
getSelectedEnvironmentIndex,
|
||||
updateEnvironmentVariable,
|
||||
} from "~/newstore/environments"
|
||||
import {
|
||||
addMockServer,
|
||||
mockServers$,
|
||||
updateMockServer as updateMockServerInStore,
|
||||
} from "~/newstore/mockServers"
|
||||
import { TeamCollectionsService } from "~/services/team-collection.service"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
|
||||
export function useMockServer() {
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const teamCollectionsService = useService(TeamCollectionsService)
|
||||
|
||||
const mockServers = useReadonlyStream(mockServers$, [])
|
||||
const collections = useReadonlyStream(restCollections$, [])
|
||||
const currentWorkspace = computed(
|
||||
() => workspaceService.currentWorkspace.value
|
||||
)
|
||||
|
||||
// Get collections based on current workspace
|
||||
const availableCollections = computed(() => {
|
||||
if (
|
||||
currentWorkspace.value.type === "team" &&
|
||||
currentWorkspace.value.teamID
|
||||
) {
|
||||
return teamCollectionsService.collections.value || []
|
||||
}
|
||||
return collections.value
|
||||
})
|
||||
|
||||
// Environment management
|
||||
const myEnvironments = useReadonlyStream(environments$, [])
|
||||
const teamEnvironmentAdapter = new TeamEnvironmentAdapter(
|
||||
currentWorkspace.value.type === "team"
|
||||
? currentWorkspace.value.teamID
|
||||
: undefined
|
||||
)
|
||||
|
||||
// Function to add mock URL to environment
|
||||
const addMockUrlToEnvironment = async (
|
||||
mockUrl: string,
|
||||
collectionName: string
|
||||
) => {
|
||||
const workspaceType = currentWorkspace.value.type
|
||||
|
||||
if (workspaceType === "personal") {
|
||||
// For personal workspace, add to selected environment or create new one
|
||||
const selectedEnvIndex = getSelectedEnvironmentIndex()
|
||||
|
||||
if (selectedEnvIndex.type === "MY_ENV") {
|
||||
// Check if mockUrl already exists in the environment
|
||||
const env = myEnvironments.value[selectedEnvIndex.index]
|
||||
const existingVariableIndex = env.variables.findIndex(
|
||||
(v) => v.key === "mockUrl"
|
||||
)
|
||||
|
||||
if (existingVariableIndex === -1) {
|
||||
// Add to existing selected environment
|
||||
addEnvironmentVariable(selectedEnvIndex.index, {
|
||||
key: "mockUrl",
|
||||
initialValue: mockUrl,
|
||||
currentValue: mockUrl,
|
||||
secret: false,
|
||||
})
|
||||
toast.success(t("mock_server.environment_variable_added"))
|
||||
} else {
|
||||
// Update existing mockUrl variable with new value using the store dispatcher
|
||||
updateEnvironmentVariable(
|
||||
selectedEnvIndex.index,
|
||||
existingVariableIndex,
|
||||
{
|
||||
key: "mockUrl",
|
||||
initialValue: mockUrl,
|
||||
currentValue: mockUrl,
|
||||
}
|
||||
)
|
||||
toast.success(t("mock_server.environment_variable_updated"))
|
||||
}
|
||||
} else {
|
||||
// Create a new environment with the mock URL
|
||||
const envName = `${collectionName} Environment`
|
||||
createEnvironment(envName, [
|
||||
{
|
||||
key: "mockUrl",
|
||||
initialValue: mockUrl,
|
||||
currentValue: mockUrl,
|
||||
secret: false,
|
||||
},
|
||||
])
|
||||
toast.success(t("mock_server.environment_created_with_variable"))
|
||||
}
|
||||
} else if (workspaceType === "team" && currentWorkspace.value.teamID) {
|
||||
// For team workspace, create a new team environment or update existing one
|
||||
const teamID = currentWorkspace.value.teamID
|
||||
|
||||
// Check if there's an existing team environment for this collection
|
||||
const teamEnvs = teamEnvironmentAdapter.teamEnvironmentList$.value
|
||||
const existingEnv = teamEnvs.find((env) =>
|
||||
env.environment.name.includes(collectionName)
|
||||
)
|
||||
|
||||
if (existingEnv) {
|
||||
// Update existing environment (add or update the mockUrl variable)
|
||||
const existingVariableIndex =
|
||||
existingEnv.environment.variables.findIndex(
|
||||
(v) => v.key === "mockUrl"
|
||||
)
|
||||
|
||||
let updatedVariables
|
||||
let successMessage
|
||||
|
||||
if (existingVariableIndex === -1) {
|
||||
// Variable doesn't exist, add it
|
||||
updatedVariables = [
|
||||
...existingEnv.environment.variables,
|
||||
{ key: "mockUrl", value: mockUrl },
|
||||
]
|
||||
successMessage = t("mock_server.environment_variable_added")
|
||||
} else {
|
||||
// Variable exists, update its value
|
||||
updatedVariables = existingEnv.environment.variables.map((v, idx) =>
|
||||
idx === existingVariableIndex ? { ...v, value: mockUrl } : v
|
||||
)
|
||||
successMessage = t("mock_server.environment_variable_updated")
|
||||
}
|
||||
|
||||
await pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(updatedVariables),
|
||||
existingEnv.id,
|
||||
existingEnv.environment.name
|
||||
),
|
||||
TE.match(
|
||||
(error) => {
|
||||
console.error("Failed to update team environment:", error)
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
},
|
||||
() => {
|
||||
toast.success(successMessage)
|
||||
}
|
||||
)
|
||||
)()
|
||||
} else {
|
||||
// Create new team environment
|
||||
const envName = `${collectionName} Environment`
|
||||
const variables = [{ key: "mockUrl", value: mockUrl }]
|
||||
|
||||
await pipe(
|
||||
createTeamEnvironment(JSON.stringify(variables), teamID, envName),
|
||||
TE.match(
|
||||
(error) => {
|
||||
console.error("Failed to create team environment:", error)
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
},
|
||||
() => {
|
||||
toast.success(t("mock_server.environment_created_with_variable"))
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new mock server
|
||||
const createMockServer = async (params: {
|
||||
mockServerName: string
|
||||
collectionID: string
|
||||
delayInMs: number
|
||||
isPublic: boolean
|
||||
setInEnvironment: boolean
|
||||
collectionName: string
|
||||
}) => {
|
||||
const {
|
||||
mockServerName,
|
||||
collectionID,
|
||||
delayInMs,
|
||||
isPublic,
|
||||
setInEnvironment,
|
||||
collectionName,
|
||||
} = params
|
||||
|
||||
if (!mockServerName.trim() || !collectionID) {
|
||||
if (!collectionID) {
|
||||
toast.error(t("mock_server.select_collection_error"))
|
||||
}
|
||||
return { success: false, server: null }
|
||||
}
|
||||
|
||||
// Determine workspace type and ID based on current workspace
|
||||
const workspaceType =
|
||||
currentWorkspace.value.type === "team"
|
||||
? WorkspaceType.Team
|
||||
: WorkspaceType.User
|
||||
const workspaceID =
|
||||
currentWorkspace.value.type === "team"
|
||||
? currentWorkspace.value.teamID
|
||||
: undefined
|
||||
|
||||
const result = await pipe(
|
||||
createMockServerMutation(
|
||||
mockServerName.trim(),
|
||||
collectionID,
|
||||
workspaceType,
|
||||
workspaceID,
|
||||
delayInMs,
|
||||
isPublic
|
||||
),
|
||||
TE.match(
|
||||
(error) => {
|
||||
toast.error(String(error) || t("error.something_went_wrong"))
|
||||
return null as MockServer | null
|
||||
},
|
||||
(result) => {
|
||||
toast.success(t("mock_server.mock_server_created"))
|
||||
// Add the new mock server to the store
|
||||
addMockServer(result)
|
||||
return result as MockServer
|
||||
}
|
||||
)
|
||||
)()
|
||||
|
||||
if (!result) {
|
||||
return { success: false, server: null }
|
||||
}
|
||||
|
||||
// Add mock URL to environment if enabled
|
||||
if (setInEnvironment) {
|
||||
const mockUrl =
|
||||
result.serverUrlPathBased || result.serverUrlDomainBased || ""
|
||||
if (mockUrl) {
|
||||
await addMockUrlToEnvironment(mockUrl, collectionName)
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, server: result }
|
||||
}
|
||||
|
||||
// Toggle mock server active state
|
||||
const toggleMockServer = async (mockServer: MockServer) => {
|
||||
const newActiveState = !mockServer.isActive
|
||||
|
||||
return await pipe(
|
||||
updateMockServer(mockServer.id, { isActive: newActiveState }),
|
||||
TE.match(
|
||||
() => {
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
return { success: false }
|
||||
},
|
||||
() => {
|
||||
toast.success(
|
||||
newActiveState
|
||||
? t("mock_server.server_started")
|
||||
: t("mock_server.server_stopped")
|
||||
)
|
||||
|
||||
// Update the mock server in the store
|
||||
updateMockServerInStore(mockServer.id, { isActive: newActiveState })
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
mockServers,
|
||||
availableCollections,
|
||||
currentWorkspace,
|
||||
|
||||
// Functions
|
||||
createMockServer,
|
||||
toggleMockServer,
|
||||
addMockUrlToEnvironment,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
mutation CreateRESTRootUserCollection($title: String!, $data: String) {
|
||||
createRESTRootUserCollection(title: $title, data: $data) {
|
||||
id
|
||||
title
|
||||
data
|
||||
type
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
mutation CreateRESTUserRequest(
|
||||
$collectionID: ID!
|
||||
$title: String!
|
||||
$request: String!
|
||||
) {
|
||||
createRESTUserRequest(
|
||||
collectionID: $collectionID
|
||||
title: $title
|
||||
request: $request
|
||||
) {
|
||||
id
|
||||
title
|
||||
request
|
||||
}
|
||||
}
|
||||
|
|
@ -286,3 +286,31 @@ export function getFoldersByPath(
|
|||
|
||||
return currentCollection.folders
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a collection to the format expected by team or personal collections.
|
||||
* Extracts auth, headers, and variables into a data object and recursively processes folders.
|
||||
* @param collection The collection to transform
|
||||
* @returns The transformed collection
|
||||
*/
|
||||
export function transformCollectionForImport(collection: any): any {
|
||||
const folders: any[] = (collection.folders ?? []).map(
|
||||
transformCollectionForImport
|
||||
)
|
||||
|
||||
const data = {
|
||||
auth: collection.auth,
|
||||
headers: collection.headers,
|
||||
variables: collection.variables,
|
||||
}
|
||||
|
||||
const obj = {
|
||||
...collection,
|
||||
folders,
|
||||
data,
|
||||
}
|
||||
|
||||
if (collection.id) obj.id = collection.id
|
||||
|
||||
return obj
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,327 @@
|
|||
import {
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
makeCollection,
|
||||
makeRESTRequest,
|
||||
} from "@hoppscotch/data"
|
||||
import { uniqueID } from "~/helpers/utils/uniqueID"
|
||||
|
||||
const MOCK_URL_VAR = "<<mockUrl>>"
|
||||
|
||||
/**
|
||||
* Returns a JSON string of the Pet Store example collection for import
|
||||
* @returns JSON string representation of the collection
|
||||
*/
|
||||
export function getPetStoreExampleJSON(): string {
|
||||
const collection = createExamplePetStoreCollection()
|
||||
return JSON.stringify(collection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an example Pet Store collection with 4 requests (GET, POST, PUT, DELETE)
|
||||
* @param collectionName The name for the collection
|
||||
* @returns A HoppCollection object with example pet store requests
|
||||
*/
|
||||
export function createExamplePetStoreCollection(
|
||||
collectionName: string = "Pet Store Mock Server"
|
||||
): HoppCollection {
|
||||
const requests: HoppRESTRequest[] = [
|
||||
// GET - List all pets
|
||||
makeRESTRequest({
|
||||
id: uniqueID(),
|
||||
name: "Get All Pets",
|
||||
method: "GET",
|
||||
endpoint: `${MOCK_URL_VAR}/pets`,
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
requestVariables: [],
|
||||
responses: {
|
||||
[uniqueID()]: {
|
||||
status: 200,
|
||||
body: JSON.stringify(
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
name: "Buddy",
|
||||
species: "Dog",
|
||||
breed: "Golden Retriever",
|
||||
age: 3,
|
||||
status: "available",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Whiskers",
|
||||
species: "Cat",
|
||||
breed: "Siamese",
|
||||
age: 2,
|
||||
status: "available",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Charlie",
|
||||
species: "Dog",
|
||||
breed: "Beagle",
|
||||
age: 4,
|
||||
status: "adopted",
|
||||
},
|
||||
],
|
||||
null,
|
||||
2
|
||||
),
|
||||
headers: [
|
||||
{
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// GET - Get a single pet by ID
|
||||
makeRESTRequest({
|
||||
id: uniqueID(),
|
||||
name: "Get Pet by ID",
|
||||
method: "GET",
|
||||
endpoint: `${MOCK_URL_VAR}/pets/1`,
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
requestVariables: [],
|
||||
responses: {
|
||||
[uniqueID()]: {
|
||||
status: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
id: 1,
|
||||
name: "Buddy",
|
||||
species: "Dog",
|
||||
breed: "Golden Retriever",
|
||||
age: 3,
|
||||
status: "available",
|
||||
description: "Friendly and energetic golden retriever",
|
||||
vaccinated: true,
|
||||
neutered: true,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
headers: [
|
||||
{
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// POST - Create a new pet
|
||||
makeRESTRequest({
|
||||
id: uniqueID(),
|
||||
name: "Create New Pet",
|
||||
method: "POST",
|
||||
endpoint: `${MOCK_URL_VAR}/pets`,
|
||||
params: [],
|
||||
headers: [
|
||||
{
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
{
|
||||
name: "Max",
|
||||
species: "Dog",
|
||||
breed: "Labrador",
|
||||
age: 2,
|
||||
status: "available",
|
||||
description: "Playful labrador looking for a home",
|
||||
vaccinated: true,
|
||||
neutered: false,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
requestVariables: [],
|
||||
responses: {
|
||||
[uniqueID()]: {
|
||||
status: 201,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
id: 4,
|
||||
name: "Max",
|
||||
species: "Dog",
|
||||
breed: "Labrador",
|
||||
age: 2,
|
||||
status: "available",
|
||||
description: "Playful labrador looking for a home",
|
||||
vaccinated: true,
|
||||
neutered: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
headers: [
|
||||
{
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
key: "Location",
|
||||
value: "/pets/4",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// PUT - Update an existing pet
|
||||
makeRESTRequest({
|
||||
id: uniqueID(),
|
||||
name: "Update Pet",
|
||||
method: "PUT",
|
||||
endpoint: `${MOCK_URL_VAR}/pets/1`,
|
||||
params: [],
|
||||
headers: [
|
||||
{
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(
|
||||
{
|
||||
name: "Buddy",
|
||||
species: "Dog",
|
||||
breed: "Golden Retriever",
|
||||
age: 4,
|
||||
status: "adopted",
|
||||
description: "Friendly golden retriever - Now adopted!",
|
||||
vaccinated: true,
|
||||
neutered: true,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
requestVariables: [],
|
||||
responses: {
|
||||
[uniqueID()]: {
|
||||
status: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
id: 1,
|
||||
name: "Buddy",
|
||||
species: "Dog",
|
||||
breed: "Golden Retriever",
|
||||
age: 4,
|
||||
status: "adopted",
|
||||
description: "Friendly golden retriever - Now adopted!",
|
||||
vaccinated: true,
|
||||
neutered: true,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
headers: [
|
||||
{
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// DELETE - Delete a pet
|
||||
makeRESTRequest({
|
||||
id: uniqueID(),
|
||||
name: "Delete Pet",
|
||||
method: "DELETE",
|
||||
endpoint: `${MOCK_URL_VAR}/pets/3`,
|
||||
params: [],
|
||||
headers: [],
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
requestVariables: [],
|
||||
responses: {
|
||||
[uniqueID()]: {
|
||||
status: 204,
|
||||
body: "",
|
||||
headers: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
return makeCollection({
|
||||
name: collectionName,
|
||||
folders: [],
|
||||
requests,
|
||||
auth: {
|
||||
authType: "inherit",
|
||||
authActive: true,
|
||||
},
|
||||
headers: [],
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as E from "fp-ts/Either"
|
||||
import {
|
||||
HoppRESTRequest,
|
||||
RESTReqSchemaVersion,
|
||||
makeCollection,
|
||||
} from "@hoppscotch/data"
|
||||
import { createNewRootCollection } from "~/helpers/backend/mutations/TeamCollection"
|
||||
import { createRequestInCollection } from "~/helpers/backend/mutations/TeamRequest"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import {
|
||||
CreateRestRootUserCollectionDocument,
|
||||
CreateRestRootUserCollectionMutation,
|
||||
CreateRestRootUserCollectionMutationVariables,
|
||||
CreateRestUserRequestDocument,
|
||||
CreateRestUserRequestMutation,
|
||||
CreateRestUserRequestMutationVariables,
|
||||
} from "~/helpers/backend/graphql"
|
||||
import { addRESTCollection } from "~/newstore/collections"
|
||||
|
||||
/**
|
||||
* Get example REST requests for mock server collection
|
||||
*/
|
||||
export function getExampleMockRequests(): HoppRESTRequest[] {
|
||||
const petBody = JSON.stringify(
|
||||
{
|
||||
id: 1,
|
||||
category: {
|
||||
id: 1,
|
||||
name: "string",
|
||||
},
|
||||
name: "doggie",
|
||||
photoUrls: ["string"],
|
||||
tags: [],
|
||||
status: "available",
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
const oauthAuth = {
|
||||
authType: "oauth-2" as const,
|
||||
authActive: true,
|
||||
grantTypeInfo: {
|
||||
authEndpoint: "<<mockUrl>>/oauth/authorize",
|
||||
clientID: "",
|
||||
grantType: "IMPLICIT" as const,
|
||||
scopes: "write:pets read:pets",
|
||||
token: "",
|
||||
authRequestParams: [],
|
||||
refreshRequestParams: [],
|
||||
},
|
||||
addTo: "HEADERS" as const,
|
||||
}
|
||||
|
||||
const requests: HoppRESTRequest[] = [
|
||||
// addPet request
|
||||
{
|
||||
v: RESTReqSchemaVersion,
|
||||
name: "addPet",
|
||||
method: "POST",
|
||||
endpoint: "<<mockUrl>>/v2/pet",
|
||||
params: [],
|
||||
headers: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: "application/json",
|
||||
body: petBody,
|
||||
},
|
||||
auth: oauthAuth,
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
},
|
||||
// updatePet request
|
||||
{
|
||||
v: RESTReqSchemaVersion,
|
||||
name: "updatePet",
|
||||
method: "PUT",
|
||||
endpoint: "<<mockUrl>>/v2/pet",
|
||||
params: [],
|
||||
headers: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: "application/json",
|
||||
body: petBody,
|
||||
},
|
||||
auth: oauthAuth,
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
},
|
||||
// findByStatus request
|
||||
{
|
||||
v: RESTReqSchemaVersion,
|
||||
name: "findByStatus",
|
||||
method: "GET",
|
||||
endpoint: "<<mockUrl>>/v2/pet/findByStatus",
|
||||
params: [
|
||||
{
|
||||
key: "status",
|
||||
value: "available",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
headers: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
auth: oauthAuth,
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
},
|
||||
// getPetById request
|
||||
{
|
||||
v: RESTReqSchemaVersion,
|
||||
name: "getPetById",
|
||||
method: "GET",
|
||||
endpoint: "<<mockUrl>>/v2/pet/1",
|
||||
params: [],
|
||||
headers: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
auth: {
|
||||
authType: "api-key",
|
||||
authActive: true,
|
||||
key: "api_key",
|
||||
value: "",
|
||||
addTo: "HEADERS",
|
||||
},
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
},
|
||||
// updatePetWithForm request
|
||||
{
|
||||
v: RESTReqSchemaVersion,
|
||||
name: "updatePetWithForm",
|
||||
method: "POST",
|
||||
endpoint: "<<mockUrl>>/v2/pet/1",
|
||||
params: [],
|
||||
headers: [],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: "name=doggie&status=available",
|
||||
},
|
||||
auth: oauthAuth,
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
},
|
||||
// deletePet request
|
||||
{
|
||||
v: RESTReqSchemaVersion,
|
||||
name: "deletePet",
|
||||
method: "DELETE",
|
||||
endpoint: "<<mockUrl>>/v2/pet/1",
|
||||
params: [],
|
||||
headers: [
|
||||
{
|
||||
key: "api_key",
|
||||
value: "",
|
||||
active: true,
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
auth: oauthAuth,
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
},
|
||||
]
|
||||
|
||||
return requests
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock collection for team workspace
|
||||
*/
|
||||
export async function createMockCollectionForTeam(
|
||||
teamID: string,
|
||||
collectionName: string
|
||||
): Promise<E.Either<string, { id: string; name: string }>> {
|
||||
// Create the root collection
|
||||
const collectionResult = await pipe(
|
||||
createNewRootCollection(collectionName, teamID),
|
||||
TE.match(
|
||||
(error) => E.left(`Failed to create collection: ${error}`),
|
||||
(collection) => E.right(collection)
|
||||
)
|
||||
)()
|
||||
|
||||
if (E.isLeft(collectionResult)) {
|
||||
return collectionResult
|
||||
}
|
||||
|
||||
const collectionID = collectionResult.right.createRootCollection.id
|
||||
|
||||
// Create requests in the collection
|
||||
const requests = getExampleMockRequests()
|
||||
|
||||
for (const request of requests) {
|
||||
const requestResult = await pipe(
|
||||
createRequestInCollection(collectionID, {
|
||||
request: JSON.stringify(request),
|
||||
teamID,
|
||||
title: request.name,
|
||||
}),
|
||||
TE.match(
|
||||
(error) => E.left(`Failed to create request: ${error}`),
|
||||
(req) => E.right(req)
|
||||
)
|
||||
)()
|
||||
|
||||
if (E.isLeft(requestResult)) {
|
||||
// Log error but continue with other requests
|
||||
console.error(
|
||||
"Failed to create request:",
|
||||
request.name,
|
||||
requestResult.left
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return E.right({
|
||||
id: collectionID,
|
||||
name: collectionName,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock collection for personal workspace
|
||||
* Uses backend GraphQL mutations to create the collection with proper backend ID
|
||||
*/
|
||||
export async function createMockCollectionForPersonal(
|
||||
collectionName: string
|
||||
): Promise<E.Either<string, { id: string; name: string }>> {
|
||||
// Prepare collection data
|
||||
const data = {
|
||||
auth: {
|
||||
authType: "inherit" as const,
|
||||
authActive: true,
|
||||
},
|
||||
headers: [],
|
||||
variables: [],
|
||||
}
|
||||
|
||||
// Create the root collection using GraphQL mutation
|
||||
const collectionResult = await pipe(
|
||||
runMutation<
|
||||
CreateRestRootUserCollectionMutation,
|
||||
CreateRestRootUserCollectionMutationVariables,
|
||||
""
|
||||
>(CreateRestRootUserCollectionDocument, {
|
||||
title: collectionName,
|
||||
data: JSON.stringify(data),
|
||||
}),
|
||||
TE.match(
|
||||
(error) => E.left(`Failed to create collection: ${error}`),
|
||||
(response) => E.right(response)
|
||||
)
|
||||
)()
|
||||
|
||||
if (E.isLeft(collectionResult)) {
|
||||
return collectionResult
|
||||
}
|
||||
|
||||
// Extract the collection ID from the response
|
||||
const collectionID = collectionResult.right.createRESTRootUserCollection.id
|
||||
|
||||
// Create requests in the collection using GraphQL mutation
|
||||
const requests = getExampleMockRequests()
|
||||
const createdRequests: HoppRESTRequest[] = []
|
||||
|
||||
for (const request of requests) {
|
||||
const requestResult = await pipe(
|
||||
runMutation<
|
||||
CreateRestUserRequestMutation,
|
||||
CreateRestUserRequestMutationVariables,
|
||||
""
|
||||
>(CreateRestUserRequestDocument, {
|
||||
collectionID,
|
||||
title: request.name,
|
||||
request: JSON.stringify(request),
|
||||
}),
|
||||
TE.match(
|
||||
(error) => E.left(`Failed to create request: ${error}`),
|
||||
(req) => E.right(req)
|
||||
)
|
||||
)()
|
||||
|
||||
if (E.isLeft(requestResult)) {
|
||||
// Log error but continue with other requests
|
||||
console.error(
|
||||
"Failed to create request:",
|
||||
request.name,
|
||||
requestResult.left
|
||||
)
|
||||
} else {
|
||||
// Add the request ID to the created request
|
||||
const createdRequest = {
|
||||
...request,
|
||||
id: requestResult.right.createRESTUserRequest.id,
|
||||
}
|
||||
createdRequests.push(createdRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a HoppCollection object and add it to the store immediately
|
||||
const collection = makeCollection({
|
||||
name: collectionName,
|
||||
folders: [],
|
||||
requests: createdRequests,
|
||||
auth: data.auth,
|
||||
headers: data.headers,
|
||||
variables: data.variables,
|
||||
})
|
||||
|
||||
// Add the backend ID to the collection
|
||||
collection.id = collectionID
|
||||
|
||||
// Add the collection to the store so it's visible immediately
|
||||
addRESTCollection(collection)
|
||||
|
||||
return E.right({
|
||||
id: collectionID,
|
||||
name: collectionName,
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue