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