From 9504369ce1bae914d7bb8d66e9538bd419658dac Mon Sep 17 00:00:00 2001
From: Nivedin <53208152+nivedin@users.noreply.github.com>
Date: Tue, 26 Aug 2025 17:24:23 +0530
Subject: [PATCH] fix: handle edge cases and bugs in collection variables
(#5348)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
---
.../src/components/app/Inspection.vue | 7 +-
.../collections/TeamCollections.vue | 30 +-
.../src/components/collections/index.vue | 51 +-
.../ImportExportSteps/AllCollectionImport.vue | 6 +
.../src/components/smart/EnvInput.vue | 51 +-
.../subscriptions/TeamCollectionMoved.graphql | 1 +
.../src/helpers/collection/collection.ts | 84 +++-
.../editor/extensions/HoppEnvironment.ts | 147 +++---
.../import/openapi/example-generators/v2.ts | 2 +-
.../src/helpers/teams/TeamCollection.ts | 2 +-
.../helpers/teams/TeamCollectionAdapter.ts | 22 +-
.../helpers/types/HoppInheritedProperties.ts | 1 +
.../inheritedCollectionVarTransformer.ts | 27 +-
.../src/newstore/collections.ts | 32 +-
.../src/newstore/environments.ts | 220 +++------
.../src/services/inspection/index.ts | 1 +
.../inspectors/environment.inspector.ts | 462 +++++++++---------
.../persistence/validation-schemas/index.ts | 1 +
18 files changed, 609 insertions(+), 538 deletions(-)
diff --git a/packages/hoppscotch-common/src/components/app/Inspection.vue b/packages/hoppscotch-common/src/components/app/Inspection.vue
index 9338f66c..fa628bf5 100644
--- a/packages/hoppscotch-common/src/components/app/Inspection.vue
+++ b/packages/hoppscotch-common/src/components/app/Inspection.vue
@@ -51,9 +51,12 @@
-
+
()
-const getPath = (path: string) => {
+const getPath = (path: string, pop: boolean = true) => {
const pathArray = path.split("/")
- pathArray.pop()
+ if (pop) pathArray.pop()
return pathArray.join("/")
}
@@ -783,17 +789,25 @@ const dragRequest = (
dataTransfer.setData("requestIndex", requestIndex)
}
-const dragEvent = (dataTransfer: DataTransfer, collectionIndex: string) => {
+const dragEvent = (
+ dataTransfer: DataTransfer,
+ collectionIndex: string,
+ parentIndex?: string
+) => {
dataTransfer.setData("collectionIndex", collectionIndex)
+ if (parentIndex) dataTransfer.setData("parentIndex", parentIndex)
}
const dropEvent = (
dataTransfer: DataTransfer,
- destinationCollectionIndex: string
+ destinationCollectionIndex: string,
+ destinationParentPath?: string
) => {
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
+ const currentParentIndex = dataTransfer.getData("parentIndex")
+
if (folderPath && requestIndex) {
emit("drop-request", {
folderPath,
@@ -804,6 +818,8 @@ const dropEvent = (
emit("drop-collection", {
collectionIndexDragged,
destinationCollectionIndex,
+ destinationParentPath,
+ currentParentIndex,
})
}
}
diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue
index a0188afb..5431675f 100644
--- a/packages/hoppscotch-common/src/components/collections/index.vue
+++ b/packages/hoppscotch-common/src/components/collections/index.vue
@@ -2296,8 +2296,15 @@ const isMoveToSameLocation = (
const dropCollection = (payload: {
collectionIndexDragged: string
destinationCollectionIndex: string
+ destinationParentPath?: string
+ currentParentIndex?: string
}) => {
- const { collectionIndexDragged, destinationCollectionIndex } = payload
+ const {
+ collectionIndexDragged,
+ destinationCollectionIndex,
+ destinationParentPath,
+ currentParentIndex,
+ } = payload
if (!collectionIndexDragged || !destinationCollectionIndex) return
if (collectionIndexDragged === destinationCollectionIndex) return
@@ -2339,18 +2346,20 @@ const dropCollection = (payload: {
"drop"
)
+ const newCollectionPath = `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
+
updateSaveContextForAffectedRequests(
collectionIndexDragged,
- `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
+ newCollectionPath
)
const inheritedProperty = cascadeParentCollectionForProperties(
- `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
+ newCollectionPath,
"rest"
)
updateInheritedPropertiesForAffectedRequests(
- `${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
+ newCollectionPath,
inheritedProperty,
"rest"
)
@@ -2381,16 +2390,25 @@ const dropCollection = (payload: {
1
)
+ if (destinationParentPath && currentParentIndex) {
+ updateSaveContextForAffectedRequests(
+ currentParentIndex,
+ `${destinationParentPath}`
+ )
+ }
+
const inheritedProperty =
teamCollectionAdapter.cascadeParentCollectionForProperties(
- destinationCollectionIndex
+ `${destinationParentPath}/${collectionIndexDragged}`
)
- updateInheritedPropertiesForAffectedRequests(
- `${destinationCollectionIndex}`,
- inheritedProperty,
- "rest"
- )
+ setTimeout(() => {
+ updateInheritedPropertiesForAffectedRequests(
+ `${destinationParentPath}/${collectionIndexDragged}`,
+ inheritedProperty,
+ "rest"
+ )
+ }, 300)
}
)
)()
@@ -2430,6 +2448,17 @@ const dropToRoot = ({ dataTransfer }: DragEvent) => {
collectionIndexDragged,
`${rootLength - 1}`
)
+
+ const inheritedProperty = cascadeParentCollectionForProperties(
+ `${rootLength - 1}`,
+ "rest"
+ )
+
+ updateInheritedPropertiesForAffectedRequests(
+ `${rootLength - 1}`,
+ inheritedProperty,
+ "rest"
+ )
}
draggingToRoot.value = false
@@ -2989,7 +3018,7 @@ const setCollectionProperties = (newCollection: {
"rest",
collectionId
)
- }, 200)
+ }, 300)
}
displayModalEditProperties(false)
diff --git a/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/AllCollectionImport.vue b/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/AllCollectionImport.vue
index 83f904c7..310fd1f4 100644
--- a/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/AllCollectionImport.vue
+++ b/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/AllCollectionImport.vue
@@ -94,6 +94,7 @@
import {
GQLHeader,
HoppCollection,
+ HoppCollectionVariable,
HoppGQLAuth,
HoppGQLRequest,
HoppRESTAuth,
@@ -261,11 +262,13 @@ const convertToInheritedProperties = (
): {
auth: HoppRESTAuth | HoppGQLAuth
headers: Array
+ variables: HoppCollectionVariable[]
} => {
const collectionLevelAuthAndHeaders = data
? (JSON.parse(data) as {
auth: HoppRESTAuth | HoppGQLAuth
headers: Array
+ variables: HoppCollectionVariable[]
})
: null
@@ -276,9 +279,12 @@ const convertToInheritedProperties = (
authActive: true,
}
+ const variables = collectionLevelAuthAndHeaders?.variables ?? []
+
return {
auth,
headers,
+ variables,
}
}
diff --git a/packages/hoppscotch-common/src/components/smart/EnvInput.vue b/packages/hoppscotch-common/src/components/smart/EnvInput.vue
index b33a1774..dfeda1c3 100644
--- a/packages/hoppscotch-common/src/components/smart/EnvInput.vue
+++ b/packages/hoppscotch-common/src/components/smart/EnvInput.vue
@@ -392,33 +392,52 @@ const aggregateEnvs = useReadonlyStream(
const tabs = useService(RESTTabService)
const envVars = computed(() => {
+ // If envs are passed directly as props, mask secrets and return them
if (props.envs?.length) {
- return props.envs.map((x) => {
- const { key, secret } = x
- const currentValue = secret ? "********" : x.currentValue
- const initialValue = secret ? "********" : x.initialValue
- const sourceEnv = "sourceEnv" in x ? x.sourceEnv : ""
- return {
+ return props.envs.map(
+ ({ key, currentValue, initialValue, secret, sourceEnv }) => ({
key,
- currentValue,
- initialValue,
- sourceEnv,
+ currentValue: secret ? "********" : currentValue,
+ initialValue: secret ? "********" : initialValue,
+ sourceEnv: sourceEnv ?? "",
secret,
- }
- })
+ })
+ )
}
+ const currentTab = tabs.currentActiveTab.value
+ const { document } = currentTab
+ const isRequest = document.type === "request"
+ const isExample = document.type === "example-response"
+
+ // variables inherited from the collection if we're in a request or example
const collectionVariables =
- tabs.currentActiveTab.value.document.type === "request" ||
- tabs.currentActiveTab.value.document.type === "example-response"
+ isRequest || isExample
? transformInheritedCollectionVariablesToAggregateEnv(
- tabs.currentActiveTab.value.document.inheritedProperties?.variables ??
- [],
+ document.inheritedProperties?.variables ?? [],
false
)
: []
- return [...collectionVariables, ...aggregateEnvs.value]
+ // request-level variables
+ const rawRequestVars = isRequest
+ ? document.request.requestVariables
+ : isExample
+ ? document.response.originalRequest.requestVariables
+ : []
+
+ // formated request variables
+ const requestVariables = rawRequestVars
+ .filter((v) => v.active)
+ .map(({ key, value }) => ({
+ key,
+ currentValue: value,
+ initialValue: value,
+ sourceEnv: "RequestVariable",
+ secret: false,
+ }))
+
+ return [...requestVariables, ...collectionVariables, ...aggregateEnvs.value]
})
function envAutoCompletion(context: CompletionContext) {
diff --git a/packages/hoppscotch-common/src/helpers/backend/gql/subscriptions/TeamCollectionMoved.graphql b/packages/hoppscotch-common/src/helpers/backend/gql/subscriptions/TeamCollectionMoved.graphql
index 6fff331a..8338e1f6 100644
--- a/packages/hoppscotch-common/src/helpers/backend/gql/subscriptions/TeamCollectionMoved.graphql
+++ b/packages/hoppscotch-common/src/helpers/backend/gql/subscriptions/TeamCollectionMoved.graphql
@@ -5,5 +5,6 @@ subscription TeamCollectionMoved($teamID: ID!) {
parent {
id
}
+ data
}
}
diff --git a/packages/hoppscotch-common/src/helpers/collection/collection.ts b/packages/hoppscotch-common/src/helpers/collection/collection.ts
index 8feb47c1..5bd965bc 100644
--- a/packages/hoppscotch-common/src/helpers/collection/collection.ts
+++ b/packages/hoppscotch-common/src/helpers/collection/collection.ts
@@ -78,6 +78,17 @@ export function resolveSaveContextOnCollectionReorder(
}
}
+/**
+ * Returns the last folder path from the given path.
+ * @param path Path can be folder path or collection path
+ * @returns Get the last folder path from the given path
+ */
+const getLastParentFolderPath = (path?: string) => {
+ if (!path) return ""
+ const pathArray = path.split("/")
+ return pathArray.slice(pathArray.length - 1, pathArray.length).join("/")
+}
+
/**
* Resolve save context for affected requests on drop folder from one to another
* @param oldFolderPath
@@ -91,13 +102,18 @@ export function updateSaveContextForAffectedRequests(
) {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
- return (
- tab.document.saveContext?.originLocation === "user-collection" &&
- tab.document.saveContext.folderPath.startsWith(oldFolderPath)
- )
+ if (tab.document.type === "test-runner") return false
+
+ return tab.document.saveContext?.originLocation === "user-collection"
+ ? tab.document.saveContext.folderPath.startsWith(oldFolderPath)
+ : tab.document.saveContext?.originLocation === "team-collection"
+ ? tab.document.saveContext.collectionID!.startsWith(oldFolderPath) ||
+ tab.document.saveContext.collectionID === oldFolderPath
+ : false
})
for (const tab of tabs) {
+ if (tab.value.document.type === "test-runner") return
if (tab.value.document.saveContext?.originLocation === "user-collection") {
tab.value.document.saveContext = {
...tab.value.document.saveContext,
@@ -106,6 +122,16 @@ export function updateSaveContextForAffectedRequests(
newFolderPath
),
}
+ } else if (
+ tab.value.document.saveContext?.originLocation === "team-collection"
+ ) {
+ tab.value.document.saveContext = {
+ ...tab.value.document.saveContext,
+ collectionID: tab.value.document.saveContext!.collectionID?.replace(
+ oldFolderPath,
+ newFolderPath
+ ),
+ }
}
}
}
@@ -166,6 +192,32 @@ function removeDuplicatesAndKeepLast(arr: HoppInheritedProperty["headers"]) {
return result
}
+/**
+ * Order collection variables based on their parentPath and parentID
+ * eg: path like 4/0/0 should come before 4/0/1 nad 4 should come before 4/0
+ * @param vars Collection of variables to be ordered
+ * @returns Ordered collection of variables
+ */
+const orderCollectionVariables = (
+ vars: HoppInheritedProperty["variables"]
+): HoppInheritedProperty["variables"] => {
+ return vars.sort((a, b) => {
+ if (a.parentPath && b.parentPath) {
+ return a.parentPath.localeCompare(b.parentPath)
+ }
+
+ if (a.parentPath) {
+ return -1
+ }
+
+ if (b.parentPath) {
+ return 1
+ }
+
+ return a.parentID.localeCompare(b.parentID)
+ })
+}
+
export function updateInheritedPropertiesForAffectedRequests(
path: string,
inheritedProperties: HoppInheritedProperty,
@@ -176,6 +228,8 @@ export function updateInheritedPropertiesForAffectedRequests(
type === "rest" ? getService(RESTTabService) : getService(GQLTabService)
const effectedTabs = tabService.getTabsRefTo((tab) => {
+ if ("type" in tab.document && tab.document.type === "test-runner")
+ return false
const saveContext = tab.document.saveContext
const saveContextPath =
@@ -183,7 +237,12 @@ export function updateInheritedPropertiesForAffectedRequests(
? saveContext.collectionID
: saveContext?.folderPath
- return saveContextPath?.startsWith(path) ?? false
+ return (
+ (saveContextPath?.startsWith(path) ||
+ getLastParentFolderPath(saveContextPath) ===
+ getLastParentFolderPath(path)) ??
+ false
+ )
})
effectedTabs.map((tab) => {
@@ -217,7 +276,8 @@ export function updateInheritedPropertiesForAffectedRequests(
// filter out the headers with the parentID as the path in the inheritedProperties
const inheritedHeaders = inheritedProperties.headers.filter(
- (header) => header.parentID === path
+ (header) =>
+ path.startsWith(header.parentID ?? "") || header.parentID === path
)
// merge the headers with the parentID as the path
@@ -228,7 +288,13 @@ export function updateInheritedPropertiesForAffectedRequests(
tab.value.document.inheritedProperties.headers = mergedHeaders
}
- if (tab.value.document.inheritedProperties?.variables && collectionId) {
+ if (tab.value.document.inheritedProperties?.variables && !collectionId) {
+ tab.value.document.inheritedProperties.variables =
+ inheritedProperties.variables
+ } else if (
+ tab.value.document.inheritedProperties?.variables &&
+ collectionId
+ ) {
const tabInheritedVariables =
tab.value.document.inheritedProperties.variables.filter(
(variable) => variable.parentID !== collectionId
@@ -239,7 +305,9 @@ export function updateInheritedPropertiesForAffectedRequests(
(variable) => variable.parentID === collectionId
)
- const finalVariables = [...inheritedVariables, ...tabInheritedVariables]
+ const finalVariables = orderCollectionVariables([
+ ...new Set([...inheritedVariables, ...tabInheritedVariables]),
+ ])
tab.value.document.inheritedProperties.variables = finalVariables
}
diff --git a/packages/hoppscotch-common/src/helpers/editor/extensions/HoppEnvironment.ts b/packages/hoppscotch-common/src/helpers/editor/extensions/HoppEnvironment.ts
index dd1616d3..10c9ec27 100644
--- a/packages/hoppscotch-common/src/helpers/editor/extensions/HoppEnvironment.ts
+++ b/packages/hoppscotch-common/src/helpers/editor/extensions/HoppEnvironment.ts
@@ -7,7 +7,10 @@ import {
hoverTooltip,
} from "@codemirror/view"
import { StreamSubscriberFunc } from "@composables/stream"
-import { parseTemplateStringE } from "@hoppscotch/data"
+import {
+ HoppRESTRequestVariables,
+ parseTemplateStringE,
+} from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import { Ref, watch } from "vue"
@@ -22,20 +25,22 @@ import {
} from "~/newstore/environments"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { RESTTabService } from "~/services/tab/rest"
+import { CurrentValueService } from "~/services/current-environment-value.service"
+
import IconEdit from "~icons/lucide/edit?raw"
import IconUser from "~icons/lucide/user?raw"
import IconUsers from "~icons/lucide/users?raw"
import IconGlobe from "~icons/lucide/globe?raw"
import IconVariable from "~icons/lucide/variable?raw"
import IconLibrary from "~icons/lucide/library?raw"
+
import { isComment } from "./helpers"
-import { CurrentValueService } from "~/services/current-environment-value.service"
+import { transformInheritedCollectionVariablesToAggregateEnv } from "~/helpers/utils/inheritedCollectionVarTransformer"
+import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
-
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
-
const HOPP_REQUEST_VARIABLE_HIGHLIGHT = "request-variable-highlight"
const HOPP_COLLECTION_ENVIRONMENT_HIGHLIGHT = "collection-variable-highlight"
const HOPP_ENVIRONMENT_HIGHLIGHT = "environment-variable-highlight"
@@ -44,7 +49,6 @@ const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "environment-not-found-highlight"
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
-
const restTabs = getService(RESTTabService)
/**
@@ -56,11 +60,9 @@ const filterNonEmptyEnvironmentVariables = (
envs: AggregateEnvironment[]
): AggregateEnvironment[] => {
const envsMap = new Map()
-
envs.forEach((env) => {
if (envsMap.has(env.key)) {
const existingEnv = envsMap.get(env.key)
-
if (
existingEnv?.currentValue === "" &&
existingEnv?.initialValue === "" &&
@@ -72,7 +74,6 @@ const filterNonEmptyEnvironmentVariables = (
envsMap.set(env.key, env)
}
})
-
return Array.from(envsMap.values())
}
@@ -80,9 +81,8 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
hoverTooltip(
(view, pos, side) => {
// Check if the current position is inside a comment then disable the tooltip
- if (isComment(view.state, pos)) {
- return null
- }
+ if (isComment(view.state, pos)) return null
+
const { from, to, text } = view.state.doc.lineAt(pos)
// TODO: When Codemirror 6 allows this to work (not make the
@@ -98,7 +98,6 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
// Tracking the start and the end of the words
let start = pos
let end = pos
-
while (start > from && /[a-zA-Z0-9-_]+/.test(text[start - from - 1]))
start--
while (end < to && /[a-zA-Z0-9-_]+/.test(text[end - from])) end++
@@ -113,20 +112,15 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
return null
const parsedEnvKey = text.slice(start - from, end - from)
-
const envsWithNoEmptyValues =
filterNonEmptyEnvironmentVariables(aggregateEnvs)
-
const tooltipEnv = envsWithNoEmptyValues.find(
(env) => env.key === parsedEnvKey
)
-
const currentSelectedEnvironment = getCurrentEnvironment()
-
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
let envInitialValue = tooltipEnv?.initialValue
-
// If the environment is not a request variable, get the current value from the current environment service
let envCurrentValue =
tooltipEnv?.sourceEnv !== "RequestVariable"
@@ -141,16 +135,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const isSecret = tooltipEnv?.secret === true
const hasSource = Boolean(tooltipEnv?.sourceEnv)
- let tooltipSourceEnvID = "Global"
-
- if (tooltipEnv?.sourceEnv === "Global") {
- tooltipSourceEnvID = "Global"
- } else {
- tooltipSourceEnvID =
- tooltipEnv?.sourceEnv === "CollectionVariable"
+ const tooltipSourceEnvID =
+ tooltipEnv?.sourceEnv === "Global"
+ ? "Global"
+ : tooltipEnv?.sourceEnv === "CollectionVariable"
? tooltipEnv.sourceEnvID!
: currentSelectedEnvironment.id
- }
const hasSecretStored = secretEnvironmentService.hasSecretValue(
tooltipSourceEnvID,
@@ -225,13 +215,13 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
| "modals.team.environment.edit"
| "modals.global.environment.update" = "modals.my.environment.edit"
- if (tooltipEnv?.sourceEnv === "Global") {
+ if (tooltipEnv?.sourceEnv === "Global")
invokeActionType = "modals.global.environment.update"
- } else if (selectedEnvType === "MY_ENV") {
+ else if (selectedEnvType === "MY_ENV")
invokeActionType = "modals.my.environment.edit"
- } else if (selectedEnvType === "TEAM_ENV") {
+ else if (selectedEnvType === "TEAM_ENV")
invokeActionType = "modals.team.environment.edit"
- } else {
+ else {
invokeActionType = "modals.my.environment.edit"
}
@@ -250,9 +240,8 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
}
})
editIcon.innerHTML = `${IconEdit}`
- if (tooltipEnv?.sourceEnv !== "CollectionVariable") {
+ if (tooltipEnv?.sourceEnv !== "CollectionVariable")
tooltip.appendChild(editIcon)
- }
}
return {
@@ -264,7 +253,6 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
arrow: true,
create() {
const dom = document.createElement("div")
-
const tooltipContainer = document.createElement("div")
const tooltipHeaderBlock = document.createElement("div")
@@ -279,7 +267,6 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const icon = document.createElement("span")
icon.innerHTML = envTypeIcon
-
const envNameBlock = document.createElement("span")
envNameBlock.innerText = envName
@@ -296,20 +283,20 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const initialValueBlock = document.createElement("div")
initialValueBlock.className = "flex items-center space-x-2"
const initialValueTitle = document.createElement("div")
- const initialValue = document.createElement("span")
- initialValue.textContent = envInitialValue || ""
initialValueTitle.textContent = "Initial"
initialValueTitle.className = "font-bold mr-4 "
+ const initialValue = document.createElement("span")
+ initialValue.textContent = envInitialValue || ""
initialValueBlock.appendChild(initialValueTitle)
initialValueBlock.appendChild(initialValue)
const currentValueBlock = document.createElement("div")
currentValueBlock.className = "flex items-center space-x-2"
const currentValueTitle = document.createElement("div")
+ currentValueTitle.textContent = "Current"
+ currentValueTitle.className = "font-bold mr-1.5"
const currentValue = document.createElement("span")
currentValue.textContent = envCurrentValue || ""
- currentValueTitle.textContent = "Current "
- currentValueTitle.className = "font-bold mr-1.5"
currentValueBlock.appendChild(currentValueTitle)
currentValueBlock.appendChild(currentValue)
@@ -332,7 +319,6 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
let className = HOPP_ENV_HIGHLIGHT_NOT_FOUND
-
const envSource = aggregateEnvs.find(
(k: { key: string }) => k.key === env.slice(2, -2)
)?.sourceEnv
@@ -344,9 +330,7 @@ function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
else if (envSource === "Global") className = HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT
else if (envSource !== undefined) className = HOPP_ENVIRONMENT_HIGHLIGHT
- return Decoration.mark({
- class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
- })
+ return Decoration.mark({ class: `${HOPP_ENV_HIGHLIGHT} ${className}` })
}
const getMatchDecorator = (aggregateEnvs: AggregateEnvironment[]) =>
@@ -354,9 +338,7 @@ const getMatchDecorator = (aggregateEnvs: AggregateEnvironment[]) =>
regexp: HOPP_ENVIRONMENT_REGEX,
decoration: (m, view, pos) => {
// Check if the current position is inside a comment then disable the highlight
- if (isComment(view.state, pos)) {
- return null
- }
+ if (isComment(view.state, pos)) return null
return checkEnv(m[0], aggregateEnvs)
},
})
@@ -366,9 +348,7 @@ export const environmentHighlightStyle = (
) => {
const envsWithNoEmptyValues =
filterNonEmptyEnvironmentVariables(aggregateEnvs)
-
const decorator = getMatchDecorator(envsWithNoEmptyValues)
-
return ViewPlugin.define(
(view) => ({
decorations: decorator.createDeco(view),
@@ -376,15 +356,40 @@ export const environmentHighlightStyle = (
this.decorations = decorator.updateDeco(u, this.decorations)
},
}),
- {
- decorations: (v) => v.decorations,
- }
+ { decorations: (v) => v.decorations }
)
}
+/**
+ * Function to get the request variables and collection variables in AggregateEnvironment type
+ * @param requestVariables Request Variables defined in the request
+ * @param collectionVariables Inherited Collection Variables
+ * @returns Transforms the request and collection variables to AggregateEnvironment type
+ */
+const getRequestAndCollectionVariables = (
+ requestVariables: HoppRESTRequestVariables,
+ collectionVariables: HoppInheritedProperty["variables"]
+) => {
+ const reqVars = requestVariables
+ .filter((v) => v.active)
+ .map(({ key, value }) => ({
+ key,
+ currentValue: value,
+ initialValue: value,
+ sourceEnv: "RequestVariable",
+ secret: false,
+ }))
+
+ const collVars = transformInheritedCollectionVariablesToAggregateEnv(
+ collectionVariables,
+ false
+ )
+
+ return [...reqVars, ...collVars]
+}
+
export class HoppEnvironmentPlugin {
private compartment = new Compartment()
-
private envs: AggregateEnvironment[] = []
constructor(
@@ -392,18 +397,42 @@ export class HoppEnvironmentPlugin {
private editorView: Ref
) {
const aggregateEnvs = getAggregateEnvsWithCurrentValue()
+ const currentTab = restTabs.currentActiveTab.value
+ const currentTabRequest =
+ currentTab.document.type === "example-response"
+ ? currentTab.document.response.originalRequest
+ : currentTab.document.request
+ const currentTabInheritedProperty = currentTab.document.inheritedProperties
- this.envs = [...aggregateEnvs]
+ if (!currentTabRequest || !currentTabInheritedProperty) return
- this.editorView.value?.dispatch({
- effects: this.compartment.reconfigure([
- cursorTooltipField(this.envs),
- environmentHighlightStyle(this.envs),
- ]),
- })
+ watch(
+ [currentTabRequest, currentTabInheritedProperty],
+ ([request, document]) => {
+ const requestAndCollVars = getRequestAndCollectionVariables(
+ request.requestVariables,
+ document.variables
+ )
+
+ this.envs = [...requestAndCollVars, ...aggregateEnvs]
+
+ this.editorView.value?.dispatch({
+ effects: this.compartment.reconfigure([
+ cursorTooltipField(this.envs),
+ environmentHighlightStyle(this.envs),
+ ]),
+ })
+ },
+ { immediate: true, deep: true }
+ )
+
+ const requestAndCollVars = getRequestAndCollectionVariables(
+ currentTabRequest.requestVariables,
+ currentTabInheritedProperty.variables
+ )
subscribeToStream(aggregateEnvsWithCurrentValue$, (envs) => {
- this.envs = [...envs]
+ this.envs = [...requestAndCollVars, ...envs]
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
@@ -424,7 +453,6 @@ export class HoppEnvironmentPlugin {
export class HoppReactiveEnvPlugin {
private compartment = new Compartment()
-
private envs: AggregateEnvironment[] = []
constructor(
@@ -435,7 +463,6 @@ export class HoppReactiveEnvPlugin {
envsRef,
(envs) => {
this.envs = envs
-
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
diff --git a/packages/hoppscotch-common/src/helpers/import-export/import/openapi/example-generators/v2.ts b/packages/hoppscotch-common/src/helpers/import-export/import/openapi/example-generators/v2.ts
index 296cbfd4..cd00ef57 100644
--- a/packages/hoppscotch-common/src/helpers/import-export/import/openapi/example-generators/v2.ts
+++ b/packages/hoppscotch-common/src/helpers/import-export/import/openapi/example-generators/v2.ts
@@ -133,7 +133,7 @@ const generateRequestBodyExampleFromOpenAPIV2BodySchema = (
)
),
O.map((schema) => schema.items as OpenAPIV2.ItemsObject),
- O.filter((items) => items != null), // Filter out null/undefined items
+ O.filter((items) => items !== null), // Filter out null/undefined items
O.map(generateExampleArrayFromOpenAPIV2ItemsObject)
)
diff --git a/packages/hoppscotch-common/src/helpers/teams/TeamCollection.ts b/packages/hoppscotch-common/src/helpers/teams/TeamCollection.ts
index 3272f3f5..5665cfdf 100644
--- a/packages/hoppscotch-common/src/helpers/teams/TeamCollection.ts
+++ b/packages/hoppscotch-common/src/helpers/teams/TeamCollection.ts
@@ -13,7 +13,7 @@ export interface TeamCollection {
title: string
children: TeamCollection[] | null
requests: TeamRequest[] | null
- data: string | null
+ data?: string | null
}
export const getSingleCollection = (collectionID: string) =>
diff --git a/packages/hoppscotch-common/src/helpers/teams/TeamCollectionAdapter.ts b/packages/hoppscotch-common/src/helpers/teams/TeamCollectionAdapter.ts
index 5524fec0..b4b4998e 100644
--- a/packages/hoppscotch-common/src/helpers/teams/TeamCollectionAdapter.ts
+++ b/packages/hoppscotch-common/src/helpers/teams/TeamCollectionAdapter.ts
@@ -230,6 +230,10 @@ export default class NewTeamCollectionAdapter {
private teamRequestOrderUpdatedSub: WSubscription | null
private teamCollectionOrderUpdatedSub: WSubscription | null
+ //collection variables current value and secret value
+ private secretEnvironmentService = getService(SecretEnvironmentService)
+ private currentEnvironmentValueService = getService(CurrentValueService)
+
constructor(private teamID: string | null) {
this.collections$ = new BehaviorSubject([])
this.loadingCollections$ = new BehaviorSubject([])
@@ -534,7 +538,8 @@ export default class NewTeamCollectionAdapter {
private async moveCollection(
collectionID: string,
parentID: string | null,
- title: string
+ title: string,
+ data?: string | null
) {
// Remove the collection from the current position
this.removeCollection(collectionID)
@@ -551,7 +556,7 @@ export default class NewTeamCollectionAdapter {
children: null,
requests: null,
title: title,
- data: null,
+ data,
},
parentID ?? null
)
@@ -851,11 +856,11 @@ export default class NewTeamCollectionAdapter {
)
const { teamCollectionMoved } = result.right
- const { id, parent, title } = teamCollectionMoved
+ const { id, parent, title, data } = teamCollectionMoved
const parentID = parent?.id ?? null
- this.moveCollection(id, parentID, title)
+ this.moveCollection(id, parentID, title, data)
})
const [teamRequestOrderUpdated$, teamRequestOrderUpdated] =
@@ -1043,17 +1048,13 @@ export default class NewTeamCollectionAdapter {
varIndex: number,
collectionID: string
) => {
- //collection variables current value and secret value
- const secretEnvironmentService = getService(SecretEnvironmentService)
- const currentEnvironmentValueService = getService(CurrentValueService)
-
if (env && env.secret) {
- return secretEnvironmentService.getSecretEnvironmentVariable(
+ return this.secretEnvironmentService.getSecretEnvironmentVariable(
collectionID,
varIndex
)?.value
}
- return currentEnvironmentValueService.getEnvironmentVariable(
+ return this.currentEnvironmentValueService.getEnvironmentVariable(
collectionID,
varIndex
)?.currentValue
@@ -1190,6 +1191,7 @@ export default class NewTeamCollectionAdapter {
const currentPath = [...path.slice(0, i + 1)].join("/")
variables.push({
+ parentPath: path.slice(0, i + 1).join("/"),
parentID: parentFolder.id ?? currentPath,
parentName: parentFolder.title,
inheritedVariables: this.populateValues(
diff --git a/packages/hoppscotch-common/src/helpers/types/HoppInheritedProperties.ts b/packages/hoppscotch-common/src/helpers/types/HoppInheritedProperties.ts
index 482ffa07..05609b43 100644
--- a/packages/hoppscotch-common/src/helpers/types/HoppInheritedProperties.ts
+++ b/packages/hoppscotch-common/src/helpers/types/HoppInheritedProperties.ts
@@ -18,6 +18,7 @@ export type HoppInheritedProperty = {
inheritedHeader: HoppRESTHeader | GQLHeader
}[]
variables: {
+ parentPath?: string
parentID: string
parentName: string
inheritedVariables: HoppCollectionVariable[]
diff --git a/packages/hoppscotch-common/src/helpers/utils/inheritedCollectionVarTransformer.ts b/packages/hoppscotch-common/src/helpers/utils/inheritedCollectionVarTransformer.ts
index c50dc0ae..53e47de0 100644
--- a/packages/hoppscotch-common/src/helpers/utils/inheritedCollectionVarTransformer.ts
+++ b/packages/hoppscotch-common/src/helpers/utils/inheritedCollectionVarTransformer.ts
@@ -5,16 +5,16 @@ import { CurrentValueService } from "~/services/current-environment-value.servic
import { getService } from "~/modules/dioc"
import { HoppCollectionVariable } from "@hoppscotch/data"
+//collection variables current value and secret value
+const secretEnvironmentService = getService(SecretEnvironmentService)
+const currentEnvironmentValueService = getService(CurrentValueService)
+
const getCurrentValue = (
isSecret: boolean,
varIndex: number,
collectionID: string,
showSecret: boolean = false
) => {
- //collection variables current value and secret value
- const secretEnvironmentService = getService(SecretEnvironmentService)
- const currentEnvironmentValueService = getService(CurrentValueService)
-
if (isSecret && showSecret) {
return secretEnvironmentService.getSecretEnvironmentVariable(
collectionID,
@@ -28,16 +28,19 @@ const getCurrentValue = (
}
/**
- * Function to transform inherited collection variables into an array of `AggregateEnvironment` objects.
+ * Transforms inherited collection variables into a normalized array of `AggregateEnvironment` objects.
+ * Ensures no duplicate keys exist — the last encountered value overrides earlier ones.
+ *
* @param variables - The inherited collection variables to transform.
- * @param showSecret - Whether to show secret values in the transformed variables.
- * @returns An array of `AggregateEnvironment` objects representing the transformed collection variables.
+ * @param showSecret - Whether to reveal secret values or mask them.
+ * @returns A de-duplicated array of `AggregateEnvironment` objects.
*/
export const transformInheritedCollectionVariablesToAggregateEnv = (
variables: HoppInheritedProperty["variables"],
showSecret: boolean = true
): AggregateEnvironment[] => {
- return variables.flatMap(({ parentID, inheritedVariables }) =>
+ // Flatten the inherited variables into a single array
+ const flattened = variables.flatMap(({ parentID, inheritedVariables }) =>
inheritedVariables.map(
({ currentValue, initialValue, key, secret }, index) => ({
key,
@@ -50,6 +53,14 @@ export const transformInheritedCollectionVariablesToAggregateEnv = (
})
)
)
+
+ // Later values override earlier ones
+ const mapByKey = new Map()
+ flattened.forEach((variable) => {
+ mapByKey.set(variable.key, variable)
+ })
+
+ return Array.from(mapByKey.values())
}
/**
diff --git a/packages/hoppscotch-common/src/newstore/collections.ts b/packages/hoppscotch-common/src/newstore/collections.ts
index cce3267f..08c6e3d4 100644
--- a/packages/hoppscotch-common/src/newstore/collections.ts
+++ b/packages/hoppscotch-common/src/newstore/collections.ts
@@ -21,6 +21,10 @@ import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { CurrentValueService } from "~/services/current-environment-value.service"
+//collection variables current value and secret value
+const secretEnvironmentService = getService(SecretEnvironmentService)
+const currentEnvironmentValueService = getService(CurrentValueService)
+
const defaultRESTCollectionState = {
state: [
makeCollection({
@@ -80,10 +84,6 @@ const getCurrentValue = (
collectionID: string,
showSecret: boolean
) => {
- //collection variables current value and secret value
- const secretEnvironmentService = getService(SecretEnvironmentService)
- const currentEnvironmentValueService = getService(CurrentValueService)
-
if (env && env.secret && showSecret) {
return secretEnvironmentService.getSecretEnvironmentVariable(
collectionID,
@@ -167,7 +167,6 @@ export function cascadeParentCollectionForProperties(
const parentFolderHeaders = parentFolder.headers as
| HoppRESTHeaders
| GQLHeader[]
-
const parentFolderVariables =
parentFolder.variables as HoppCollectionVariable[]
@@ -182,7 +181,6 @@ export function cascadeParentCollectionForProperties(
inheritedAuth: auth.inheritedAuth,
}
}
-
if (parentFolderAuth?.authType !== "inherit") {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
@@ -195,24 +193,17 @@ export function cascadeParentCollectionForProperties(
if (parentFolderHeaders) {
const activeHeaders = parentFolderHeaders.filter((h) => h.active)
activeHeaders.forEach((header) => {
- const index = headers.findIndex(
+ const idx = headers.findIndex(
(h) => h.inheritedHeader?.key === header.key
)
const currentPath = [...path.slice(0, i + 1)].join("/")
- if (index !== -1) {
- // Replace the existing header with the same key
- headers[index] = {
- parentID: currentPath,
- parentName: parentFolder.name,
- inheritedHeader: header,
- }
- } else {
- headers.push({
- parentID: currentPath,
- parentName: parentFolder.name,
- inheritedHeader: header,
- })
+ const headerObj = {
+ parentID: currentPath,
+ parentName: parentFolder.name,
+ inheritedHeader: header,
}
+ if (idx !== -1) headers[idx] = headerObj
+ else headers.push(headerObj)
})
}
@@ -220,6 +211,7 @@ export function cascadeParentCollectionForProperties(
const currentPath = [...path.slice(0, i + 1)].join("/")
variables.push({
+ parentPath: currentPath,
parentID: parentFolder._ref_id ?? parentFolder.id ?? currentPath,
parentName: parentFolder.name,
inheritedVariables: populateValues(
diff --git a/packages/hoppscotch-common/src/newstore/environments.ts b/packages/hoppscotch-common/src/newstore/environments.ts
index 219aa9e7..6013dba7 100644
--- a/packages/hoppscotch-common/src/newstore/environments.ts
+++ b/packages/hoppscotch-common/src/newstore/environments.ts
@@ -14,7 +14,6 @@ import DispatchingStore, {
} from "~/newstore/DispatchingStore"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
-import { RESTTabService } from "~/services/tab/rest"
export type SelectedEnvironmentIndex =
| { type: "NO_ENV_SELECTED" }
@@ -438,19 +437,6 @@ export const aggregateEnvs$: Observable = combineLatest(
[currentEnvironment$, globalEnv$]
).pipe(
map(([selectedEnv, globalEnv]) => {
- const restTabs = getService(RESTTabService)
-
- const currentTab = restTabs.currentActiveTab.value
-
- const currentTabRequest =
- currentTab.document.type === "example-response"
- ? currentTab.document.response.originalRequest
- : currentTab.document.request
-
- const requestVariables = currentTabRequest?.requestVariables
- ? currentTabRequest.requestVariables
- : []
-
const effectiveAggregateEnvs: AggregateEnvironment[] = []
// Ensure pre-defined variables are prioritised over other environment variables with the same name
@@ -466,18 +452,6 @@ export const aggregateEnvs$: Observable = combineLatest(
const aggregateEnvKeys = effectiveAggregateEnvs.map(({ key }) => key)
- requestVariables.forEach(({ key, value, active }) => {
- if (!aggregateEnvKeys.includes(key) && active) {
- effectiveAggregateEnvs.push({
- key,
- currentValue: value,
- initialValue: value,
- secret: false,
- sourceEnv: "RequestVariable",
- })
- }
- })
-
selectedEnv?.variables.forEach((variable) => {
const { key, secret } = variable
const currentValue =
@@ -520,19 +494,7 @@ export const aggregateEnvs$: Observable = combineLatest(
)
export function getAggregateEnvs() {
- const restTabs = getService(RESTTabService)
-
const currentEnv = getCurrentEnvironment()
- const currentTab = restTabs.currentActiveTab.value
-
- const currentTabRequest =
- currentTab.document.type === "example-response"
- ? currentTab.document.response.originalRequest
- : currentTab.document.request
-
- const requestVariables = currentTabRequest?.requestVariables
- ? currentTabRequest.requestVariables
- : []
return [
...HOPP_SUPPORTED_PREDEFINED_VARIABLES.map(({ key, getValue }) => {
@@ -545,22 +507,6 @@ export function getAggregateEnvs() {
}
}),
- ...requestVariables
- .map(({ key, value, active }) => {
- if (active) {
- return {
- key,
- currentValue: value,
- initialValue: value,
- sourceEnv: "RequestVariable",
- secret: false,
- }
- }
-
- return
- })
- .filter((v): v is AggregateEnvironment => v !== undefined),
-
...currentEnv.variables.map((x) => {
let currentValue = ""
if (!x.secret) {
@@ -592,22 +538,10 @@ export function getAggregateEnvs() {
}
export function getAggregateEnvsWithCurrentValue() {
- const restTabs = getService(RESTTabService)
-
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
const currentEnv = getCurrentEnvironment()
- const currentTab = restTabs.currentActiveTab.value
-
- const currentTabRequest =
- currentTab.document.type === "example-response"
- ? currentTab.document.response.originalRequest
- : currentTab.document.request
-
- const requestVariables = currentTabRequest?.requestVariables
- ? currentTabRequest.requestVariables
- : []
return [
...HOPP_SUPPORTED_PREDEFINED_VARIABLES.map(({ key, getValue }) => {
@@ -620,21 +554,6 @@ export function getAggregateEnvsWithCurrentValue() {
}
}),
- ...requestVariables
- .map(({ key, value, active }) => {
- if (active) {
- return {
- key,
- currentValue: value,
- initialValue: value,
- sourceEnv: "RequestVariable",
- secret: false,
- }
- }
- return
- })
- .filter((v): v is AggregateEnvironment => v !== undefined),
-
...currentEnv.variables.map((x, index) => {
let currentValue = x.currentValue
if (x.secret) {
@@ -683,97 +602,74 @@ export function getAggregateEnvsWithCurrentValue() {
export const aggregateEnvsWithCurrentValue$: Observable<
AggregateEnvironment[]
-> = combineLatest([currentEnvironment$, globalEnv$]).pipe(
- map(([selectedEnv, globalEnv]) => {
- const restTabs = getService(RESTTabService)
+> = (() => {
+ const secretEnvironmentService = getService(SecretEnvironmentService)
+ const currentEnvironmentValueService = getService(CurrentValueService)
- const secretEnvironmentService = getService(SecretEnvironmentService)
- const currentEnvironmentValueService = getService(CurrentValueService)
+ return combineLatest([currentEnvironment$, globalEnv$]).pipe(
+ map(([selectedEnv, globalEnv]) => {
+ const results: AggregateEnvironment[] = []
- const currentTab = restTabs.currentActiveTab.value
-
- const currentTabRequest =
- currentTab.document.type === "example-response"
- ? currentTab.document.response.originalRequest
- : currentTab.document.request
-
- const requestVariables = currentTabRequest?.requestVariables
- ? currentTabRequest.requestVariables
- : []
-
- const results: AggregateEnvironment[] = []
-
- // Ensure pre-defined variables are prioritised over other environment variables with the same name
- HOPP_SUPPORTED_PREDEFINED_VARIABLES.forEach(({ key, getValue }) => {
- results.push({
- key,
- currentValue: getValue(),
- initialValue: getValue(),
- secret: false,
- sourceEnv: selectedEnv?.name ?? "Global",
- })
- })
-
- requestVariables.map(({ key, value, active }) => {
- if (active) {
+ // Pre-defined variables
+ HOPP_SUPPORTED_PREDEFINED_VARIABLES.forEach(({ key, getValue }) => {
results.push({
key,
- currentValue: value,
- initialValue: value,
+ currentValue: getValue(),
+ initialValue: getValue(),
secret: false,
- sourceEnv: "RequestVariable",
+ sourceEnv: selectedEnv?.name ?? "Global",
})
- }
- })
-
- selectedEnv?.variables.map((x, index) => {
- let currentValue = x.currentValue
- if (x.secret) {
- currentValue =
- secretEnvironmentService.getSecretEnvironmentVariableValue(
- selectedEnv.id,
- index
- ) ?? ""
- }
- results.push({
- key: x.key,
- currentValue:
- currentEnvironmentValueService.getEnvironmentVariableValue(
- selectedEnv.id,
- index
- ) ?? currentValue,
- initialValue: x.initialValue,
- secret: x.secret,
- sourceEnv: selectedEnv.name,
})
- })
- globalEnv.variables.map((x, index) => {
- let currentValue = x.currentValue
- if (x.secret) {
- currentValue =
- secretEnvironmentService.getSecretEnvironmentVariableValue(
- "Global",
- index
- ) ?? ""
- }
- results.push({
- key: x.key,
- currentValue:
- currentEnvironmentValueService.getEnvironmentVariableValue(
- "Global",
- index
- ) ?? currentValue,
- initialValue: x.initialValue,
- secret: x.secret,
- sourceEnv: "Global",
+ selectedEnv?.variables.map((x, index) => {
+ let currentValue = x.currentValue
+ if (x.secret) {
+ currentValue =
+ secretEnvironmentService.getSecretEnvironmentVariableValue(
+ selectedEnv.id,
+ index
+ ) ?? ""
+ }
+ results.push({
+ key: x.key,
+ currentValue:
+ currentEnvironmentValueService.getEnvironmentVariableValue(
+ selectedEnv.id,
+ index
+ ) ?? currentValue,
+ initialValue: x.initialValue,
+ secret: x.secret,
+ sourceEnv: selectedEnv.name,
+ })
})
- })
- return results
- }),
- distinctUntilChanged(isEqual)
-)
+ globalEnv.variables.map((x, index) => {
+ let currentValue = x.currentValue
+ if (x.secret) {
+ currentValue =
+ secretEnvironmentService.getSecretEnvironmentVariableValue(
+ "Global",
+ index
+ ) ?? ""
+ }
+ results.push({
+ key: x.key,
+ currentValue:
+ currentEnvironmentValueService.getEnvironmentVariableValue(
+ "Global",
+ index
+ ) ?? currentValue,
+ initialValue: x.initialValue,
+ secret: x.secret,
+ sourceEnv: "Global",
+ })
+ })
+
+ return results
+ }),
+ distinctUntilChanged(isEqual)
+ )
+})()
export function getCurrentEnvironment(): Environment {
if (
diff --git a/packages/hoppscotch-common/src/services/inspection/index.ts b/packages/hoppscotch-common/src/services/inspection/index.ts
index 01ab4ded..aa206306 100644
--- a/packages/hoppscotch-common/src/services/inspection/index.ts
+++ b/packages/hoppscotch-common/src/services/inspection/index.ts
@@ -62,6 +62,7 @@ export interface InspectorResult {
action?: {
text: string
apply: () => void
+ showAction?: boolean
}
doc?: {
text: string
diff --git a/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts b/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts
index 11bc3d9f..947b16fd 100644
--- a/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts
+++ b/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts
@@ -28,9 +28,7 @@ import { transformInheritedCollectionVariablesToAggregateEnv } from "~/helpers/u
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
-const isENVInString = (str: string) => {
- return HOPP_ENVIRONMENT_REGEX.test(str)
-}
+const isENVInString = (str: string) => HOPP_ENVIRONMENT_REGEX.test(str)
/**
* This inspector is responsible for inspecting the environment variables of a input.
@@ -64,19 +62,28 @@ export class EnvironmentInspectorService extends Service implements Inspector {
}
/**
- * Validates the environment variables in the target array
+ * Looks for environment variables in an array of strings.
+ * Reports variables that are referenced but not defined.
* @param target The target array to validate
* @param locations The location where results are to be displayed
* @returns The results array containing the results of the validation
*/
private validateEnvironmentVariables = (
- target: any[],
+ target: string[],
locations: InspectorLocation
) => {
const newErrors: InspectorResult[] = []
-
const currentTab = this.restTabs.currentActiveTab.value
+ // Get the current request or example-response request
+ const currentTabRequest =
+ currentTab.document.type === "request"
+ ? currentTab.document.request
+ : currentTab.document.type === "example-response"
+ ? currentTab.document.response.originalRequest
+ : null
+
+ // inherited collection-level variables
const collectionVariables =
currentTab.document.type === "request" ||
currentTab.document.type === "example-response"
@@ -85,70 +92,82 @@ export class EnvironmentInspectorService extends Service implements Inspector {
)
: []
- const environmentVariables = [
- ...this.aggregateEnvsWithValue.value,
- ...collectionVariables,
- ]
+ // request variables (active only)
+ const requestVariables =
+ currentTabRequest?.requestVariables
+ .filter((v) => v.active)
+ .map(({ key, value }) => ({
+ key,
+ currentValue: value,
+ initialValue: value,
+ sourceEnv: "RequestVariable",
+ secret: false,
+ })) ?? []
+ // combine everything into one list
+ const environmentVariables = [
+ ...requestVariables,
+ ...collectionVariables,
+ ...this.aggregateEnvsWithValue.value,
+ ]
const envKeys = environmentVariables.map((e) => e.key)
+ // Scan each string for <> patterns
target.forEach((element, index) => {
- if (isENVInString(element)) {
- const extractedEnv = element.match(HOPP_ENVIRONMENT_REGEX)
+ if (!isENVInString(element)) return
+ const matches = element.match(HOPP_ENVIRONMENT_REGEX)
+ matches?.forEach((exEnv) => {
+ const formattedExEnv = exEnv.slice(2, -2)
+ const itemLocation: InspectorLocation = {
+ type: locations.type,
+ position:
+ locations.type === "url" ||
+ locations.type === "body" ||
+ locations.type === "response" ||
+ locations.type === "body-content-type-header"
+ ? "key"
+ : locations.position,
+ index,
+ key: element,
+ }
- if (extractedEnv) {
- extractedEnv.forEach((exEnv: string) => {
- const formattedExEnv = exEnv.slice(2, -2)
- const itemLocation: InspectorLocation = {
- type: locations.type,
- position:
- locations.type === "url" ||
- locations.type === "body" ||
- locations.type === "response" ||
- locations.type === "body-content-type-header"
- ? "key"
- : locations.position,
- index: index,
- key: element,
- }
- if (!envKeys.includes(formattedExEnv)) {
- newErrors.push({
- id: `environment-not-found-${newErrors.length}`,
- text: {
- type: "text",
- text: this.t("inspections.environment.not_found", {
- environment: exEnv,
- }),
- },
- icon: markRaw(IconPlusCircle),
- action: {
- text: this.t("inspections.environment.add_environment"),
- apply: () => {
- invokeAction("modals.environment.add", {
- envName: formattedExEnv,
- variableName: "",
- })
- },
- },
- severity: 3,
- isApplicable: true,
- locations: itemLocation,
- doc: {
- text: this.t("action.learn_more"),
- link: "https://docs.hoppscotch.io/documentation/features/inspections",
- },
- })
- }
+ // If the variable doesn't exist, add an inspection
+ if (!envKeys.includes(formattedExEnv)) {
+ newErrors.push({
+ id: `environment-not-found-${newErrors.length}`,
+ text: {
+ type: "text",
+ text: this.t("inspections.environment.not_found", {
+ environment: exEnv,
+ }),
+ },
+ icon: markRaw(IconPlusCircle),
+ action: {
+ text: this.t("inspections.environment.add_environment"),
+ apply: () =>
+ invokeAction("modals.environment.add", {
+ envName: formattedExEnv,
+ variableName: "",
+ }),
+ showAction: true,
+ },
+ severity: 3,
+ isApplicable: true,
+ locations: itemLocation,
+ doc: {
+ text: this.t("action.learn_more"),
+ link: "https://docs.hoppscotch.io/documentation/features/inspections",
+ },
})
}
- }
+ })
})
return newErrors
}
/**
- * Transforms the environment list to a list with unique keys with value
+ * Keeps only unique environment variables and prefers ones with values.
* @param envs The environment list to be transformed
* @returns The transformed environment list with keys with value
*/
@@ -160,7 +179,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
envs.forEach((env) => {
if (envsMap.has(env.key)) {
const existingEnv = envsMap.get(env.key)
-
+ // Replace if existing is empty and this one has a value
if (existingEnv?.currentValue === "" && env.currentValue !== "") {
envsMap.set(env.key, env)
}
@@ -173,243 +192,222 @@ export class EnvironmentInspectorService extends Service implements Inspector {
}
/**
- * Checks if the environment variables in the target have empty current value or initial value.
+ * Looks for variables that exist but are empty (no value or secret).
+ * Suggests adding a value for them.
* @param target The target array to validate
* @param locations The location where results are to be displayed
* @returns The results array containing the results of the validation
*/
private validateEmptyEnvironmentVariables = (
- target: any[],
+ target: string[],
locations: InspectorLocation
) => {
const newErrors: InspectorResult[] = []
target.forEach((element, index) => {
- if (isENVInString(element)) {
- const extractedEnv = element.match(HOPP_ENVIRONMENT_REGEX)
+ if (!isENVInString(element)) return
+ const matches = element.match(HOPP_ENVIRONMENT_REGEX)
+ matches?.forEach((exEnv) => {
+ const formattedExEnv = exEnv.slice(2, -2)
+ const currentSelectedEnvironment = getCurrentEnvironment()
+ const currentTab = this.restTabs.currentActiveTab.value
- if (extractedEnv) {
- extractedEnv.forEach((exEnv: string) => {
- const formattedExEnv = exEnv.slice(2, -2)
- const currentSelectedEnvironment = getCurrentEnvironment()
+ // Get current request or example
+ const currentTabRequest =
+ currentTab.document.type === "request"
+ ? currentTab.document.request
+ : currentTab.document.type === "example-response"
+ ? currentTab.document.response.originalRequest
+ : null
- const currentTab = this.restTabs.currentActiveTab.value
- const collectionVariables =
- currentTab.document.type === "request" ||
- currentTab.document.type === "example-response"
- ? transformInheritedCollectionVariablesToAggregateEnv(
- currentTab.document.inheritedProperties?.variables ?? [],
- false
- )
- : []
+ // request variables (active only)
+ const requestVariables =
+ currentTabRequest?.requestVariables
+ .filter((v) => v.active)
+ .map(({ key, value }) => ({
+ key,
+ currentValue: value,
+ initialValue: value,
+ sourceEnv: "RequestVariable",
+ secret: false,
+ })) ?? []
- const environmentVariables =
- this.filterNonEmptyEnvironmentVariables([
- ...this.aggregateEnvsWithValue.value,
- ...collectionVariables,
- ])
-
- environmentVariables.forEach((env) => {
- let tooltipSourceEnvID = "Global"
-
- if (env?.sourceEnv === "Global") {
- tooltipSourceEnvID = "Global"
- } else {
- tooltipSourceEnvID =
- env?.sourceEnv === "CollectionVariable"
- ? env.sourceEnvID!
- : currentSelectedEnvironment.id
- }
-
- const hasSecretEnv = this.secretEnvs.hasSecretValue(
- tooltipSourceEnvID,
- env.key
+ // inherited collection variables
+ const collectionVariables =
+ currentTab.document.type === "request" ||
+ currentTab.document.type === "example-response"
+ ? transformInheritedCollectionVariablesToAggregateEnv(
+ currentTab.document.inheritedProperties?.variables ?? [],
+ false
)
+ : []
- const hasValue =
- this.currentEnvs.hasValue(
- env.sourceEnv !== "Global"
- ? currentSelectedEnvironment.id
- : "Global",
- env.key
- ) ||
- env.currentValue !== "" ||
- env.initialValue !== ""
+ // Merge all variables
+ const environmentVariables = this.filterNonEmptyEnvironmentVariables([
+ ...requestVariables,
+ ...collectionVariables,
+ ...this.aggregateEnvsWithValue.value,
+ ])
- if (env.key === formattedExEnv) {
- if (env.secret ? !hasSecretEnv : !hasValue) {
- const itemLocation: InspectorLocation = {
- type: locations.type,
- position:
- locations.type === "url" ||
- locations.type === "body" ||
- locations.type === "response" ||
- locations.type === "body-content-type-header"
- ? "key"
- : locations.position,
- index: index,
- key: element,
- }
+ // Check each variable for missing values
+ environmentVariables.forEach((env) => {
+ const sourceEnvID =
+ env.sourceEnv === "Global"
+ ? "Global"
+ : env.sourceEnv === "CollectionVariable"
+ ? env.sourceEnvID!
+ : currentSelectedEnvironment.id
- const currentEnvironmentType = getSelectedEnvironmentType()
+ const hasSecretEnv = this.secretEnvs.hasSecretValue(
+ sourceEnvID,
+ env.key
+ )
- let invokeActionType:
- | "modals.my.environment.edit"
- | "modals.team.environment.edit"
- | "modals.global.environment.update" =
- "modals.my.environment.edit"
+ const hasValue =
+ this.currentEnvs.hasValue(
+ env.sourceEnv !== "Global"
+ ? currentSelectedEnvironment.id
+ : "Global",
+ env.key
+ ) ||
+ env.currentValue !== "" ||
+ env.initialValue !== ""
- if (env.sourceEnv === "Global") {
- invokeActionType = "modals.global.environment.update"
- } else if (currentEnvironmentType === "MY_ENV") {
- invokeActionType = "modals.my.environment.edit"
- } else if (currentEnvironmentType === "TEAM_ENV") {
- invokeActionType = "modals.team.environment.edit"
+ if (env.key !== formattedExEnv) return
+
+ // Flag variables that are empty
+ if (env.secret ? !hasSecretEnv : !hasValue) {
+ const itemLocation: InspectorLocation = {
+ type: locations.type,
+ position:
+ locations.type === "url" ||
+ locations.type === "body" ||
+ locations.type === "response" ||
+ locations.type === "body-content-type-header"
+ ? "key"
+ : locations.position,
+ index,
+ key: element,
+ }
+
+ // Pick the right modal to open for editing
+ const currentEnvironmentType = getSelectedEnvironmentType()
+ const invokeActionType:
+ | "modals.my.environment.edit"
+ | "modals.team.environment.edit"
+ | "modals.global.environment.update" =
+ env.sourceEnv === "Global"
+ ? "modals.global.environment.update"
+ : currentEnvironmentType === "TEAM_ENV"
+ ? "modals.team.environment.edit"
+ : "modals.my.environment.edit"
+
+ newErrors.push({
+ id: `environment-empty-${newErrors.length}`,
+ text: {
+ type: "text",
+ text: this.t("inspections.environment.empty_value", {
+ variable: exEnv,
+ }),
+ },
+ icon: markRaw(IconPlusCircle),
+ action: {
+ text: this.t("inspections.environment.add_environment_value"),
+ apply: () => {
+ // If it's a request variable, open the requestVariables tab
+ if (
+ env.sourceEnv === "RequestVariable" &&
+ currentTab.document.type === "request"
+ ) {
+ currentTab.document.optionTabPreference = "requestVariables"
} else {
- invokeActionType = "modals.my.environment.edit"
+ invokeAction(invokeActionType, {
+ envName:
+ env.sourceEnv === "Global"
+ ? "Global"
+ : currentSelectedEnvironment.name,
+ variableName: formattedExEnv,
+ isSecret: env.secret,
+ })
}
-
- newErrors.push({
- id: `environment-empty-${newErrors.length}`,
- text: {
- type: "text",
- text: this.t("inspections.environment.empty_value", {
- variable: exEnv,
- }),
- },
- icon: markRaw(IconPlusCircle),
- action: {
- text: this.t(
- "inspections.environment.add_environment_value"
- ),
- apply: () => {
- if (
- env.sourceEnv === "RequestVariable" &&
- currentTab.document.type === "request"
- ) {
- currentTab.document.optionTabPreference =
- "requestVariables"
- } else {
- invokeAction(invokeActionType, {
- envName:
- env.sourceEnv === "Global"
- ? "Global"
- : currentSelectedEnvironment.name,
- variableName: formattedExEnv,
- isSecret: env.secret,
- })
- }
- },
- },
- severity: 2,
- isApplicable: true,
- locations: itemLocation,
- doc: {
- text: this.t("action.learn_more"),
- link: "https://docs.hoppscotch.io/documentation/features/inspections",
- },
- })
- }
- }
+ },
+ showAction: env.sourceEnv !== "CollectionVariable", // skip collection vars for now
+ },
+ severity: 2,
+ isApplicable: true,
+ locations: itemLocation,
+ doc: {
+ text: this.t("action.learn_more"),
+ link: "https://docs.hoppscotch.io/documentation/features/inspections",
+ },
})
- })
- }
- }
+ }
+ })
+ })
})
return newErrors
}
+ /**
+ * Runs all inspections for a given request and returns a computed list of results.
+ */
getInspections(
req: Readonly[>
) {
return computed(() => {
const results: InspectorResult[] = []
-
if (!req.value) return results
- const headers = req.value.headers
-
- const params = req.value.params
-
- /**
- * Validate the environment variables in the URL
- */
- const url = req.value.endpoint
+ const { endpoint, headers, params } = req.value
+ // URL check
results.push(
- ...this.validateEnvironmentVariables([url], {
- type: "url",
- })
- )
- results.push(
- ...this.validateEmptyEnvironmentVariables([url], {
- type: "url",
- })
+ ...this.validateEnvironmentVariables([endpoint], { type: "url" }),
+ ...this.validateEmptyEnvironmentVariables([endpoint], { type: "url" })
)
- /**
- * Validate the environment variables in the headers
- */
- const headerKeys = Object.values(headers).map((header) => header.key)
+ // Header keys and values
+ const headerKeys = Object.values(headers).map((h) => h.key)
+ const headerValues = Object.values(headers).map((h) => h.value)
results.push(
...this.validateEnvironmentVariables(headerKeys, {
type: "header",
position: "key",
- })
- )
- results.push(
+ }),
...this.validateEmptyEnvironmentVariables(headerKeys, {
type: "header",
position: "key",
- })
- )
-
- const headerValues = Object.values(headers).map((header) => header.value)
-
- results.push(
+ }),
...this.validateEnvironmentVariables(headerValues, {
type: "header",
position: "value",
- })
- )
- results.push(
+ }),
...this.validateEmptyEnvironmentVariables(headerValues, {
type: "header",
position: "value",
})
)
- /**
- * Validate the environment variables in the parameters
- */
- const paramsKeys = Object.values(params).map((param) => param.key)
+ // Parameter keys and values
+ const paramKeys = Object.values(params).map((p) => p.key)
+ const paramValues = Object.values(params).map((p) => p.value)
results.push(
- ...this.validateEnvironmentVariables(paramsKeys, {
+ ...this.validateEnvironmentVariables(paramKeys, {
type: "parameter",
position: "key",
- })
- )
- results.push(
- ...this.validateEmptyEnvironmentVariables(paramsKeys, {
+ }),
+ ...this.validateEmptyEnvironmentVariables(paramKeys, {
type: "parameter",
position: "key",
- })
- )
-
- const paramsValues = Object.values(params).map((param) => param.value)
-
- results.push(
- ...this.validateEnvironmentVariables(paramsValues, {
+ }),
+ ...this.validateEnvironmentVariables(paramValues, {
type: "parameter",
position: "value",
- })
- )
-
- results.push(
- ...this.validateEmptyEnvironmentVariables(paramsValues, {
+ }),
+ ...this.validateEmptyEnvironmentVariables(paramValues, {
type: "parameter",
position: "value",
})
diff --git a/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts b/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts
index 91d823e6..21750714 100644
--- a/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts
+++ b/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts
@@ -317,6 +317,7 @@ const HoppInheritedPropertySchema = z
variables: z
.array(
z.object({
+ parentPath: z.optional(z.string()),
parentID: z.string(),
parentName: z.string(),
inheritedVariables: z.array(CollectionVariable),
]