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>
This commit is contained in:
Nivedin 2025-08-26 17:24:23 +05:30 committed by GitHub
parent a0c2635000
commit 9504369ce1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 609 additions and 538 deletions

View file

@ -51,9 +51,12 @@
<icon-lucide-arrow-up-right class="svg-icons" />
</HoppSmartLink>
</span>
<span v-if="inspector.action" class="flex space-x-2 p-2">
<span
v-if="inspector.action ? inspector.action.showAction : true"
class="flex space-x-2 p-2"
>
<HoppButtonSecondary
:label="inspector.action.text"
:label="inspector.action?.text"
outline
filled
@click="

View file

@ -108,7 +108,7 @@
emit('export-data', node.data.data.data)
"
@remove-collection="emit('remove-collection', node.id)"
@drop-event="dropEvent($event, node.id)"
@drop-event="dropEvent($event, node.id, getPath(node.id, false))"
@drag-event="dragEvent($event, node.id)"
@update-collection-order="
updateCollectionOrder($event, {
@ -210,8 +210,12 @@
node.data.type === 'folders' &&
emit('remove-folder', node.data.data.data.id)
"
@drop-event="dropEvent($event, node.data.data.data.id)"
@drag-event="dragEvent($event, node.data.data.data.id)"
@drop-event="
dropEvent($event, node.data.data.data.id, getPath(node.id, false))
"
@drag-event="
dragEvent($event, node.data.data.data.id, getPath(node.id, true))
"
@update-collection-order="
updateCollectionOrder($event, {
destinationCollectionIndex: node.data.data.data.id,
@ -640,6 +644,8 @@ const emit = defineEmits<{
payload: {
collectionIndexDragged: string
destinationCollectionIndex: string
destinationParentPath?: string
currentParentIndex?: string
}
): void
(
@ -678,9 +684,9 @@ const emit = defineEmits<{
): void
}>()
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,
})
}
}

View file

@ -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)

View file

@ -94,6 +94,7 @@
import {
GQLHeader,
HoppCollection,
HoppCollectionVariable,
HoppGQLAuth,
HoppGQLRequest,
HoppRESTAuth,
@ -261,11 +262,13 @@ const convertToInheritedProperties = (
): {
auth: HoppRESTAuth | HoppGQLAuth
headers: Array<HoppRESTHeader | GQLHeader>
variables: HoppCollectionVariable[]
} => {
const collectionLevelAuthAndHeaders = data
? (JSON.parse(data) as {
auth: HoppRESTAuth | HoppGQLAuth
headers: Array<HoppRESTHeader | GQLHeader>
variables: HoppCollectionVariable[]
})
: null
@ -276,9 +279,12 @@ const convertToInheritedProperties = (
authActive: true,
}
const variables = collectionLevelAuthAndHeaders?.variables ?? []
return {
auth,
headers,
variables,
}
}

View file

@ -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) {

View file

@ -5,5 +5,6 @@ subscription TeamCollectionMoved($teamID: ID!) {
parent {
id
}
data
}
}

View file

@ -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
}

View file

@ -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<string, AggregateEnvironment>()
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 = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
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<EditorView | undefined>
) {
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),

View file

@ -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)
)

View file

@ -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) =>

View file

@ -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<TeamCollection[]>([])
this.loadingCollections$ = new BehaviorSubject<string[]>([])
@ -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(

View file

@ -18,6 +18,7 @@ export type HoppInheritedProperty = {
inheritedHeader: HoppRESTHeader | GQLHeader
}[]
variables: {
parentPath?: string
parentID: string
parentName: string
inheritedVariables: HoppCollectionVariable[]

View file

@ -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<string, AggregateEnvironment>()
flattened.forEach((variable) => {
mapByKey.set(variable.key, variable)
})
return Array.from(mapByKey.values())
}
/**

View file

@ -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(

View file

@ -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<AggregateEnvironment[]> = 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<AggregateEnvironment[]> = 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<AggregateEnvironment[]> = 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 <AggregateEnvironment>{
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 <AggregateEnvironment>{
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 (

View file

@ -62,6 +62,7 @@ export interface InspectorResult {
action?: {
text: string
apply: () => void
showAction?: boolean
}
doc?: {
text: string

View file

@ -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 <<VAR>> 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<Ref<HoppRESTRequest | HoppRESTResponseOriginalRequest>>
) {
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",
})

View file

@ -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),