Compare commits

...

34 Commits

Author SHA1 Message Date
jamesgeorge007
a3e941271e chore: address lint errors 2024-03-07 12:24:33 +05:30
jamesgeorge007
e7fded89d5 refactor: organize imports 2024-03-07 12:24:33 +05:30
jamesgeorge007
40c8d7e4b7 feat: persist bulk mode line wrap setting for request variables
- Show line wrap toggle only if entries are present under http request headers & URL encoded params body.
- Organize imports.
2024-03-07 12:24:33 +05:30
nivedin
1d93d9dabe refactor: filter non empty value for env presidence 2024-03-07 12:24:33 +05:30
nivedin
e9f1dc7ba1 chore: code refactors 2024-03-07 12:24:33 +05:30
nivedin
1e906bbec3 fix: code refactor and handle secret var in req var 2024-03-07 12:24:33 +05:30
nivedin
7776668e40 chore: align reqvariable tab to end 2024-03-07 12:24:33 +05:30
nivedin
3d38b7678b chore: aligh button in error placeholder 2024-03-07 12:24:33 +05:30
nivedin
1ca215d887 chore: add parent container for req variable codemirror 2024-03-07 12:24:33 +05:30
nivedin
aec04a13e3 chore: diable context menu for empty text 2024-03-07 12:24:33 +05:30
nivedin
9a2058bbec chore: update env hover colours 2024-03-07 12:24:33 +05:30
nivedin
1c65147a77 fix: autocomplete env bug 2024-03-07 12:24:33 +05:30
nivedin
207eea538f chore: check for active request variables 2024-03-07 12:24:33 +05:30
nivedin
e30b59e1b5 refactor: update environments colour schema 2024-03-07 12:24:33 +05:30
nivedin
9704b3324b chore: update env autocomplete 2024-03-07 12:24:33 +05:30
nivedin
386bb453d4 refactor: update environment variables precedence order 2024-03-07 12:24:33 +05:30
nivedin
175641246e refactor: update components for embeds flow 2024-03-07 12:24:33 +05:30
nivedin
fb615d2d2b chore: add env autocomplete to components 2024-03-07 12:24:33 +05:30
nivedin
7685409ee6 chore: update env tooltip updation mechanism 2024-03-07 12:24:33 +05:30
nivedin
e01818444e chore: add env autocomplete in smartenvinput 2024-03-07 12:24:33 +05:30
nivedin
52220d9a2e chore: add action for inscpector and tooltip env 2024-03-07 12:24:33 +05:30
nivedin
82d687f665 refactor: update insomnia import 2024-03-07 12:24:33 +05:30
nivedin
d44083a380 refactor: update openapi import 2024-03-07 12:24:33 +05:30
nivedin
7589c57e86 refactor: update postman import for request variable 2024-03-07 12:24:33 +05:30
nivedin
c8009aec77 chore: update test cases in cli 2024-03-07 12:24:33 +05:30
nivedin
7114f4fd43 chore: update test cases in app 2024-03-07 12:24:33 +05:30
nivedin
ca1a4ec31b refactor: update codegen and curlparser 2024-03-07 12:24:33 +05:30
nivedin
359633102e refactor: add check for request variables for env inspector 2024-03-07 12:24:33 +05:30
nivedin
b278691a9d chore: update history 2024-03-07 12:24:33 +05:30
nivedin
c27e1be230 refactor: update env tooltip 2024-03-07 12:24:33 +05:30
nivedin
11e8ba7bb3 refactor: add request variables to request runner flow 2024-03-07 12:24:33 +05:30
nivedin
2a0000cfc4 chore: remove unused file 2024-03-07 12:24:33 +05:30
nivedin
0ed063724e feat: add request variable 2024-03-07 12:24:33 +05:30
nivedin
170ec15821 refactor: add v2 request schema 2024-03-07 12:24:33 +05:30
54 changed files with 1273 additions and 506 deletions

View File

@@ -17,7 +17,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestD",
"params": [],
@@ -40,7 +40,8 @@
"body": {
"contentType": null,
"body": null
}
},
"requestVariables": []
}
],
"auth": {
@@ -52,7 +53,7 @@
],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestC",
"params": [],
@@ -67,7 +68,8 @@
"body": {
"contentType": null,
"body": null
}
},
"requestVariables": []
}
],
"auth": {
@@ -88,7 +90,7 @@
],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestB",
"params": [],
@@ -104,6 +106,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -116,7 +119,7 @@
],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestA",
"params": [],
@@ -132,6 +135,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -158,7 +162,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestB",
"params": [],
@@ -174,6 +178,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -186,7 +191,7 @@
],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestA",
"params": [],
@@ -202,6 +207,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -218,4 +224,4 @@
"token": "BearerToken"
}
}
]
]

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "<<URL>>",
"name": "test1",
"params": [],
@@ -16,7 +16,8 @@
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -5,7 +5,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "",
"params": [],
@@ -23,10 +23,11 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": [],
},
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -44,7 +45,8 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -2,9 +2,9 @@
{
"v": 1,
"folders": [],
"requests":
"requests":
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "fail",
"params": [],
@@ -22,10 +22,11 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": [],
},
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -43,7 +44,8 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -2,9 +2,9 @@
{
"v": 1,
"folders": [],
"requests":
"requests":
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "fail",
"params": [],
@@ -22,7 +22,8 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -5,7 +5,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "",
"params": [],
@@ -23,10 +23,11 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": []
},
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -44,7 +45,8 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "sample-req",
@@ -13,7 +13,8 @@
"headers": [],
"endpoint": "https://echo.hoppscotch.io",
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
"preRequestScript": "pw.env.set(\"variable\", \"value\");"
"preRequestScript": "pw.env.set(\"variable\", \"value\");",
"requestVariables": []
}
],
"auth": { "authType": "inherit", "authActive": true },

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"name": "test-request",
"endpoint": "https://echo.hoppscotch.io",
"method": "POST",
@@ -19,7 +19,8 @@
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
},
"preRequestScript": "",
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});"
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});",
"requestVariables": []
}
],
"auth": {

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-headers",
@@ -17,12 +17,13 @@
"active": true
}
],
"requestVariables": [],
"endpoint": "<<baseURL>>/headers",
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"v": "2",
"auth": { "authType": "none", "authActive": true },
"body": {
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
@@ -32,12 +33,13 @@
"method": "POST",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/post",
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
},
{
"v": "1",
"v": "2",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-query-params",
@@ -50,12 +52,13 @@
}
],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/get",
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
},
{
"v": "1",
"v": "2",
"auth": {
"authType": "basic",
"password": "<<secretBasicAuthPassword>>",
@@ -67,12 +70,13 @@
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
"preRequestScript": ""
},
{
"v": "1",
"v": "2",
"auth": {
"token": "<<secretBearerToken>>",
"authType": "bearer",
@@ -85,18 +89,20 @@
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/bearer",
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
},
{
"v": "1",
"v": "2",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-fallback",
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>",
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
"preRequestScript": ""

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"auth": {
"authType": "none",
"authActive": true
@@ -16,6 +16,7 @@
"name": "test-secret-headers",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [
{
"key": "Secret-Header-Key",
@@ -28,7 +29,7 @@
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"v": "2",
"auth": {
"authType": "none",
"authActive": true
@@ -40,6 +41,7 @@
"name": "test-secret-headers-overrides",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [
{
"key": "Secret-Header-Key",
@@ -52,7 +54,7 @@
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"v": "2",
"auth": {
"authType": "none",
"authActive": true
@@ -64,13 +66,14 @@
"name": "test-secret-body",
"method": "POST",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/post",
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
},
{
"v": "1",
"v": "2",
"auth": {
"authType": "none",
"authActive": true
@@ -88,13 +91,14 @@
"active": true
}
],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/get",
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
},
{
"v": "1",
"v": "2",
"auth": {
"authType": "basic",
"password": "<<secretBasicAuthPassword>>",
@@ -108,13 +112,14 @@
"name": "test-secret-basic-auth",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
},
{
"v": "1",
"v": "2",
"auth": {
"token": "<<secretBearerToken>>",
"authType": "bearer",
@@ -129,6 +134,7 @@
"name": "test-secret-bearer-auth",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/bearer",
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "2",
"endpoint": "https://httpbin.org/post",
"name": "req",
"params": [],
@@ -22,7 +22,8 @@
"body": {
"contentType": "application/json",
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
}
},
"requestVariables": []
}
],
"auth": { "authType": "inherit", "authActive": false },

View File

@@ -563,12 +563,22 @@ details[open] summary .indicator {
.env-highlight {
@apply text-accentContrast;
&.env-found {
@apply bg-accentDark;
@apply hover:bg-accent;
&.request-variable-highlight {
@apply bg-amber-500;
@apply hover:bg-amber-600;
}
&.env-not-found {
&.environment-variable-highlight {
@apply bg-green-500;
@apply hover:bg-green-600;
}
&.global-variable-highlight {
@apply bg-blue-500;
@apply hover:bg-blue-600;
}
&.environment-not-found-highlight {
@apply bg-red-500;
@apply hover:bg-red-600;
}

View File

@@ -239,6 +239,7 @@
"pending_invites": "There are no pending invites for this team",
"profile": "Login to view your profile",
"protocols": "Protocols are empty",
"request_variables": "This request does not have any request variables",
"schema": "Connect to a GraphQL endpoint to view schema",
"secret_environments": "Secrets are not synced to Hoppscotch",
"shared_requests": "Shared requests are empty",
@@ -560,6 +561,7 @@
"raw_body": "Raw Request Body",
"rename": "Rename Request",
"renamed": "Request renamed",
"request_variables": "Request variables",
"run": "Run",
"save": "Save",
"save_as": "Save as",

View File

@@ -32,9 +32,13 @@
{{ tab.document.request.method }}
</span>
<div
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
class="flex items-center flex-1 flex-shrink-0 min-w-0 truncate rounded-r"
>
{{ tab.document.request.endpoint }}
<SmartEnvInput
v-model="tab.document.request.endpoint"
:readonly="true"
:envs="tabRequestVariables"
/>
</div>
</div>
<div class="flex mt-2 space-x-2 sm:mt-0">
@@ -69,6 +73,7 @@
v-model="tab.document.request"
v-model:option-tab="selectedOptionTab"
:properties="properties"
:envs="tabRequestVariables"
/>
<HttpResponse
v-if="tab.document.response"
@@ -117,6 +122,15 @@ const sharedRequestURL = computed(() => {
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
})
const tabRequestVariables = computed(() => {
return tab.value.document.request.requestVariables.map(({ key, value }) => ({
key,
value,
secret: false,
sourceEnv: "RequestVariable",
}))
})
const { subscribeToStream } = useStreamSubscriber()
const newSendRequest = async () => {

View File

@@ -150,7 +150,7 @@
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">
<div v-if="auth.authType === 'basic'">
<HttpAuthorizationBasic v-model="auth" />
<HttpAuthorizationBasic v-model="auth" :envs="envs" />
</div>
<div v-if="auth.authType === 'inherit'" class="p-4">
<span v-if="inheritedProperties?.auth">
@@ -169,17 +169,26 @@
</div>
<div v-if="auth.authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.token" placeholder="Token" />
<SmartEnvInput
v-model="auth.token"
placeholder="Token"
:auto-complete-env="true"
:envs="envs"
/>
</div>
</div>
<div v-if="auth.authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.token" placeholder="Token" />
<SmartEnvInput
v-model="auth.token"
placeholder="Token"
:envs="envs"
/>
</div>
<HttpOAuth2Authorization v-model="auth" />
<HttpOAuth2Authorization v-model="auth" :envs="envs" />
</div>
<div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" />
<HttpAuthorizationApiKey v-model="auth" :envs="envs" />
</div>
</div>
<div
@@ -215,6 +224,7 @@ import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { onMounted } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { AggregateEnvironment } from "~/newstore/environments"
const t = useI18n()
@@ -225,6 +235,7 @@ const props = defineProps<{
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -100,10 +100,12 @@
<HttpBodyParameters
v-if="body.contentType === 'multipart/form-data'"
v-model="body"
:envs="envs"
/>
<HttpURLEncodedParams
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
v-model="body"
:envs="envs"
/>
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
<HoppSmartPlaceholder
@@ -141,6 +143,7 @@ import IconExternalLink from "~icons/lucide/external-link"
import IconInfo from "~icons/lucide/info"
import IconRefreshCW from "~icons/lucide/refresh-cw"
import { RESTOptionTabs } from "./RequestOptions.vue"
import { AggregateEnvironment } from "~/newstore/environments"
const colorMode = useColorMode()
const t = useI18n()
@@ -148,6 +151,7 @@ const t = useI18n()
const props = defineProps<{
body: HoppRESTReqBody
headers: HoppRESTHeader[]
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -65,6 +65,8 @@
<SmartEnvInput
v-model="entry.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
:auto-complete-env="true"
:envs="envs"
@change="
updateBodyParam(index, {
key: $event,
@@ -87,6 +89,8 @@
<SmartEnvInput
v-model="entry.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:auto-complete-env="true"
:envs="envs"
@change="
updateBodyParam(index, {
key: entry.key,
@@ -190,11 +194,13 @@ import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { AggregateEnvironment } from "~/newstore/environments"
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
const props = defineProps<{
modelValue: Body
envs: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -188,11 +188,25 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
const requestCode = computed(() => {
const aggregateEnvs = getAggregateEnvs()
const requestVariables = request.value.requestVariables.map(
(requestVariable) => {
if (requestVariable.active)
return {
key: requestVariable.key,
value: requestVariable.value,
secret: false,
}
return {}
}
)
const env: Environment = {
v: 1,
id: "env",
name: "Env",
variables: aggregateEnvs,
variables: [
...(requestVariables as Environment["variables"]),
...aggregateEnvs,
],
}
const effectiveRequest = getEffectiveRESTRequest(request.value, env)
@@ -212,6 +226,12 @@ const requestCode = computed(() => {
active: true,
})),
endpoint: effectiveRequest.effectiveFinalURL,
requestVariables: effectiveRequest.effectiveFinalRequestVariables.map(
(requestVariable) => ({
...requestVariable,
active: true,
})
),
})
)

View File

@@ -26,7 +26,7 @@
@click="clearContent()"
/>
<HoppButtonSecondary
v-if="bulkMode"
v-if="bulkHeaders"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': WRAP_LINES }"
@@ -92,6 +92,8 @@
:auto-complete-source="commonHeaders"
:env-index="index"
:inspection-results="getInspectorResult(headerKeyResults, index)"
:auto-complete-env="true"
:envs="envs"
@change="
updateHeader(index, {
id: header.id,
@@ -108,6 +110,8 @@
getInspectorResult(headerValueResults, index)
"
:env-index="index"
:auto-complete-env="true"
:envs="envs"
@change="
updateHeader(index, {
id: header.id,
@@ -329,7 +333,11 @@ import {
getComputedHeaders,
getComputedAuthHeaders,
} from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import {
AggregateEnvironment,
aggregateEnvs$,
getAggregateEnvs,
} from "~/newstore/environments"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
@@ -359,6 +367,7 @@ const props = defineProps<{
modelValue: HoppRESTRequest
isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -6,31 +6,52 @@
:styles="
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
"
:auto-complete-env="true"
placeholder="OpenID Connect Discovery URL"
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="authURL"
placeholder="Authorization URL"
:auto-complete-env="true"
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
></SmartEnvInput>
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="accessTokenURL"
placeholder="Access Token URL"
:auto-complete-env="true"
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
<SmartEnvInput
v-model="clientID"
:auto-complete-env="true"
placeholder="Client ID"
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="clientSecret" placeholder="Client Secret" />
<SmartEnvInput
v-model="clientSecret"
:auto-complete-env="true"
placeholder="Client Secret"
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="scope" placeholder="Scope" />
<SmartEnvInput
v-model="scope"
:auto-complete-env="true"
placeholder="Scope"
:envs="envs"
/>
</div>
<div class="p-2">
<HoppButtonSecondary
@@ -62,6 +83,7 @@ const toast = useToast()
const props = defineProps<{
modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -87,6 +87,8 @@
:inspection-results="
getInspectorResult(parameterKeyResults, index)
"
:auto-complete-env="true"
:envs="envs"
@change="
updateParam(index, {
id: param.id,
@@ -102,6 +104,8 @@
:inspection-results="
getInspectorResult(parameterValueResults, index)
"
:auto-complete-env="true"
:envs="envs"
@change="
updateParam(index, {
id: param.id,
@@ -209,6 +213,7 @@ import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { AggregateEnvironment } from "~/newstore/environments"
const colorMode = useColorMode()
@@ -242,6 +247,7 @@ useCodemirror(
const props = defineProps<{
modelValue: HoppRESTParam[]
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -56,6 +56,7 @@
v-model="tab.document.request.endpoint"
:placeholder="`${t('request.url')}`"
:auto-complete-source="userHistories"
:auto-complete-env="true"
:inspection-results="tabResults"
@paste="onPasteUrl($event)"
@enter="newSendRequest"

View File

@@ -5,48 +5,51 @@
render-inactive-tabs
>
<HoppSmartTab
v-if="properties ? properties.includes('params') : true"
v-if="properties?.includes('params') ?? true"
:id="'params'"
:label="`${t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`"
:info="`${newActiveParamsCount}`"
>
<HttpParameters v-model="request.params" />
<HttpParameters v-model="request.params" :envs="envs" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('bodyParams') : true"
v-if="properties?.includes('bodyParams') ?? true"
:id="'bodyParams'"
:label="`${t('tab.body')}`"
>
<HttpBody
v-model:headers="request.headers"
v-model:body="request.body"
:envs="envs"
@change-tab="changeOptionTab"
/>
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('headers') : true"
v-if="properties?.includes('headers') ?? true"
:id="'headers'"
:label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`"
:info="`${newActiveHeadersCount}`"
>
<HttpHeaders
v-model="request"
:inherited-properties="inheritedProperties"
:envs="envs"
@change-tab="changeOptionTab"
/>
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('authorization') : true"
v-if="properties?.includes('authorization') ?? true"
:id="'authorization'"
:label="`${t('tab.authorization')}`"
>
<HttpAuthorization
v-model="request.auth"
:inherited-properties="inheritedProperties"
:envs="envs"
/>
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('preRequestScript') : true"
v-if="properties?.includes('preRequestScript') ?? true"
:id="'preRequestScript'"
:label="`${t('tab.pre_request_script')}`"
:indicator="
@@ -58,7 +61,7 @@
<HttpPreRequestScript v-model="request.preRequestScript" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('tests') : true"
v-if="properties?.includes('tests') ?? true"
:id="'tests'"
:label="`${t('tab.tests')}`"
:indicator="
@@ -67,6 +70,15 @@
>
<HttpTests v-model="request.testScript" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties?.includes('requestVariables') ?? true"
:id="'requestVariables'"
:label="`${t('tab.variables')}`"
:info="`${newActiveRequestVariablesCount}`"
:align-last="true"
>
<HttpRequestVariables v-model="request.requestVariables" />
</HoppSmartTab>
</HoppSmartTabs>
</template>
@@ -77,6 +89,7 @@ import { useVModel } from "@vueuse/core"
import { computed } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { AggregateEnvironment } from "~/newstore/environments"
const VALID_OPTION_TABS = [
"params",
@@ -85,6 +98,7 @@ const VALID_OPTION_TABS = [
"authorization",
"preRequestScript",
"tests",
"requestVariables",
] as const
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
@@ -98,6 +112,7 @@ const props = withDefaults(
optionTab: RESTOptionTabs
properties?: string[]
inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[]
}>(),
{
optionTab: "params",
@@ -116,22 +131,27 @@ const changeOptionTab = (e: RESTOptionTabs) => {
selectedOptionTab.value = e
}
const newActiveParamsCount$ = computed(() => {
const e = request.value.params.filter(
(x) => x.active && (x.key !== "" || x.value !== "")
const newActiveParamsCount = computed(() => {
const count = request.value.params.filter(
(x) => x.active && (x.key || x.value)
).length
if (e === 0) return null
return `${e}`
return count ? count : null
})
const newActiveHeadersCount$ = computed(() => {
const e = request.value.headers.filter(
(x) => x.active && (x.key !== "" || x.value !== "")
const newActiveHeadersCount = computed(() => {
const count = request.value.headers.filter(
(x) => x.active && (x.key || x.value)
).length
if (e === 0) return null
return `${e}`
return count ? count : null
})
const newActiveRequestVariablesCount = computed(() => {
const count = request.value.requestVariables.filter(
(x) => x.active && (x.key || x.value)
).length
return count ? count : null
})
defineActionHandler("request.open-tab", ({ tab }) => {

View File

@@ -0,0 +1,412 @@
<template>
<div class="flex flex-1 flex-col">
<div
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
>
<label class="truncate font-semibold text-secondaryLight">
{{ t("request.request_variables") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/rest-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearContent()"
/>
<HoppButtonSecondary
v-if="bulkVariables"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText"
@click.prevent="
toggleNestedSetting('WRAP_LINES', 'httpRequestVariables')
"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
:icon="IconEdit"
:class="{ '!text-accent': bulkMode }"
@click="bulkMode = !bulkMode"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
:disabled="bulkMode"
@click="addVariable"
/>
</div>
</div>
<div v-if="bulkMode" class="h-full relative">
<div ref="bulkEditor" class="absolute inset-0"></div>
</div>
<div v-else>
<draggable
v-model="workingRequestVariables"
item-key="id"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: variable, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<HoppButtonSecondary
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
content:
index !== workingRequestVariables?.length - 1
? t('action.drag_to_reorder')
: null,
}"
:icon="IconGripVertical"
class="opacity-0"
:class="{
'draggable-handle cursor-grab group-hover:opacity-100':
index !== workingRequestVariables?.length - 1,
}"
tabindex="-1"
/>
<SmartEnvInput
v-model="variable.key"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
@change="
updateVariable(index, {
id: variable.id,
key: $event,
value: variable.value,
active: variable.active,
})
"
/>
<SmartEnvInput
v-model="variable.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateVariable(index, {
id: variable.id,
key: variable.key,
value: $event,
active: variable.active,
})
"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
variable.hasOwnProperty('active')
? variable.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:icon="
variable.hasOwnProperty('active')
? variable.active
? IconCheckCircle
: IconCircle
: IconCheckCircle
"
color="green"
@click="
updateVariable(index, {
id: variable.id,
key: variable.key,
value: variable.value,
active: variable.hasOwnProperty('active')
? !variable.active
: false,
})
"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteVariable(index)"
/>
</div>
</template>
</draggable>
<HoppSmartPlaceholder
v-if="workingRequestVariables.length === 0"
:src="`/images/states/${colorMode.value}/add_files.svg`"
:alt="`${t('empty.request_variables')}`"
:text="t('empty.request_variables')"
>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
:icon="IconPlus"
filled
@click="addVariable"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>
</template>
<script lang="ts" setup>
import { throwError } from "@functional/error"
import {
HoppRESTRequestVariable,
RawKeyValueEntry,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
} from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import { flow, pipe } from "fp-ts/function"
import { cloneDeep, isEqual } from "lodash-es"
import { reactive, ref, watch } from "vue"
import draggable from "vuedraggable-es"
import { useCodemirror } from "~/composables/codemirror"
import { useI18n } from "~/composables/i18n"
import { useNestedSetting } from "~/composables/settings"
import { useColorMode } from "~/composables/theming"
import { useToast } from "~/composables/toast"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { objRemoveKey } from "~/helpers/functional/object"
import { toggleNestedSetting } from "~/newstore/settings"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconCircle from "~icons/lucide/circle"
import IconEdit from "~icons/lucide/edit"
import IconGripVertical from "~icons/lucide/grip-vertical"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconPlus from "~icons/lucide/plus"
import IconTrash from "~icons/lucide/trash"
import IconTrash2 from "~icons/lucide/trash-2"
import IconWrapText from "~icons/lucide/wrap-text"
const colorMode = useColorMode()
const t = useI18n()
const toast = useToast()
const bulkMode = ref(false)
const bulkEditor = ref<any | null>(null)
const bulkVariables = ref("")
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpRequestVariables")
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const props = defineProps<{
modelValue: HoppRESTRequestVariable[]
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: Array<HoppRESTRequestVariable>): void
}>()
// The functional requestVariable list (the requestVariable actually applied to the session)
const requestVariables = useVModel(props, "modelValue", emit)
useCodemirror(
bulkEditor,
bulkVariables,
reactive({
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: WRAP_LINES,
},
linter,
completer: null,
environmentHighlights: true,
})
)
const idTicker = ref(0)
const workingRequestVariables = ref<
Array<HoppRESTRequestVariable & { id: number }>
>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Sync logic between params and working/bulk params
watch(
requestVariables,
(newRequestVariableList) => {
// Sync should overwrite working params
const filteredWorkingRequestVariables: HoppRESTRequestVariable[] = pipe(
workingRequestVariables.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkRequestVariables = pipe(
parseRawKeyValueEntriesE(bulkVariables.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newRequestVariableList, filteredWorkingRequestVariables)) {
workingRequestVariables.value = pipe(
newRequestVariableList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newRequestVariableList, filteredBulkRequestVariables)) {
bulkVariables.value = rawKeyValueEntriesToString(newRequestVariableList)
}
},
{ immediate: true }
)
watch(workingRequestVariables, (newWorkingRequestVariables) => {
const fixedRequestVariables = pipe(
newWorkingRequestVariables,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(requestVariables.value, fixedRequestVariables)) {
requestVariables.value = cloneDeep(fixedRequestVariables)
}
})
watch(bulkVariables, (newBulkParams) => {
const filteredBulkRequestVariables = pipe(
parseRawKeyValueEntriesE(newBulkParams),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(requestVariables.value, filteredBulkRequestVariables)) {
requestVariables.value = filteredBulkRequestVariables
}
})
// Rule: Working Request variable always have last element is always an empty param
watch(workingRequestVariables, (variableList) => {
if (
variableList.length > 0 &&
variableList[variableList.length - 1].key !== ""
) {
workingRequestVariables.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
const addVariable = () => {
workingRequestVariables.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateVariable = (index: number, variable: any & { id: number }) => {
workingRequestVariables.value = workingRequestVariables.value.map((h, i) =>
i === index ? variable : h
)
}
const deleteVariable = (index: number) => {
const requestVariablesBeforeDeletion = cloneDeep(
workingRequestVariables.value
)
if (
!(
requestVariablesBeforeDeletion.length > 0 &&
index === requestVariablesBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingRequestVariables.value = requestVariablesBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingRequestVariables.value = pipe(
workingRequestVariables.value,
A.deleteAt(index),
O.getOrElseW(() =>
throwError("Working Request Variable Deletion Out of Bounds")
)
)
}
const clearContent = () => {
// set params list to the initial state
workingRequestVariables.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkVariables.value = ""
}
</script>

View File

@@ -21,7 +21,7 @@
@click="clearContent()"
/>
<HoppButtonSecondary
v-if="bulkMode"
v-if="bulkUrlEncodedParams"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': WRAP_LINES }"
@@ -84,6 +84,8 @@
<SmartEnvInput
v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
:auto-complete-env="true"
:envs="envs"
@change="
updateUrlEncodedParam(index, {
id: param.id,
@@ -96,6 +98,8 @@
<SmartEnvInput
v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:auto-complete-env="true"
:envs="envs"
@change="
updateUrlEncodedParam(index, {
id: param.id,
@@ -200,6 +204,7 @@ import { throwError } from "~/helpers/functional/error"
import { useVModel } from "@vueuse/core"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { AggregateEnvironment } from "~/newstore/environments"
type Body = HoppRESTReqBody & {
contentType: "application/x-www-form-urlencoded"
@@ -207,6 +212,7 @@ type Body = HoppRESTReqBody & {
const props = defineProps<{
modelValue: Body
envs: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -1,9 +1,19 @@
<template>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.key" placeholder="Key" />
<SmartEnvInput
v-model="auth.key"
:auto-complete-env="true"
placeholder="Key"
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.value" placeholder="Value" />
<SmartEnvInput
v-model="auth.value"
:auto-complete-env="true"
placeholder="Value"
:envs="envs"
/>
</div>
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
@@ -65,11 +75,13 @@ import { useI18n } from "@composables/i18n"
import { HoppRESTAuthAPIKey } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { ref } from "vue"
import { AggregateEnvironment } from "~/newstore/environments"
const t = useI18n()
const props = defineProps<{
modelValue: HoppRESTAuthAPIKey
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -3,12 +3,16 @@
<SmartEnvInput
v-model="auth.username"
:placeholder="t('authorization.username')"
:auto-complete-env="true"
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="auth.password"
:placeholder="t('authorization.password')"
:auto-complete-env="true"
:envs="envs"
/>
</div>
</template>
@@ -17,11 +21,13 @@
import { useI18n } from "@composables/i18n"
import { HoppRESTAuthBasic } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { AggregateEnvironment } from "~/newstore/environments"
const t = useI18n()
const props = defineProps<{
modelValue: HoppRESTAuthBasic
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -21,28 +21,24 @@
</span>
</div>
<div class="flex flex-col space-y-2 py-4">
<span>
<HoppSmartItem
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
:icon="IconChrome"
label="Chrome"
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasChromeExtInstalled"
outline
/>
</span>
<span>
<HoppSmartItem
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
:icon="IconFirefox"
label="Firefox"
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasFirefoxExtInstalled"
outline
/>
</span>
<HoppSmartItem
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
:icon="IconChrome"
label="Chrome"
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasChromeExtInstalled"
outline
/>
<HoppSmartItem
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
:icon="IconFirefox"
label="Firefox"
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasFirefoxExtInstalled"
outline
/>
</div>
<div class="space-y-4 py-4">
<div class="flex items-center">

View File

@@ -290,7 +290,12 @@ const widgets: Widget[] = [
},
]
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "requestVariables"
type EmbedOption = {
selectedTab: EmbedTabs

View File

@@ -56,7 +56,12 @@ import { useI18n } from "~/composables/i18n"
const t = useI18n()
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "requestVariables"
type EmbedOption = {
selectedTab: EmbedTabs

View File

@@ -173,6 +173,11 @@ const embedOptions = ref<EmbedOption>({
label: t("tab.authorization"),
enabled: false,
},
{
value: "requestVariables",
label: t("tab.variables"),
enabled: false,
},
],
theme: "system",
})
@@ -223,7 +228,12 @@ const currentUser = useReadonlyStream(
const step = ref(1)
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "requestVariables"
type EmbedOption = {
selectedTab: EmbedTabs
@@ -369,6 +379,11 @@ const displayCustomizeRequestModal = (
label: t("tab.authorization"),
enabled: false,
},
{
value: "requestVariables",
label: t("tab.variables"),
enabled: false,
},
],
theme: "system",
}

View File

@@ -28,7 +28,7 @@
</button>
</div>
<div
class="flex"
class="flex overflow-x-scroll"
:class="[{ 'border-b pt-2 ': !noActiveTab }, embedColours]"
>
<span
@@ -57,7 +57,12 @@ import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
type Tabs = "params" | "bodyParams" | "headers" | "authorization"
type Tabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "requestVariables"
type EmbedOption = {
selectedTab: Tabs

View File

@@ -79,29 +79,27 @@ import { history, historyKeymap } from "@codemirror/commands"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { useReadonlyStream } from "@composables/stream"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
} from "~/newstore/environments"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { platform } from "~/platform"
import { onClickOutside, useDebounceFn } from "@vueuse/core"
import { InspectorResult } from "~/services/inspection"
import { invokeAction } from "~/helpers/actions"
import { Environment } from "@hoppscotch/data"
import { useI18n } from "~/composables/i18n"
import IconEye from "~icons/lucide/eye"
import IconEyeoff from "~icons/lucide/eye-off"
import { CompletionContext, autocompletion } from "@codemirror/autocomplete"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { syntaxTree } from "@codemirror/language"
const t = useI18n()
type Env = Environment["variables"][number] & { source: string }
const props = withDefaults(
defineProps<{
modelValue?: string
placeholder?: string
styles?: string
envs?: Env[] | null
envs?: AggregateEnvironment[] | null
focus?: boolean
selectTextOnMount?: boolean
environmentHighlights?: boolean
@@ -110,6 +108,7 @@ const props = withDefaults(
inspectionResults?: InspectorResult[] | undefined
contextMenuEnabled?: boolean
secret?: boolean
autoCompleteEnv?: boolean
}>(),
{
modelValue: "",
@@ -124,6 +123,7 @@ const props = withDefaults(
inspectionResults: undefined,
contextMenuEnabled: true,
secret: false,
autoCompleteEnvSource: false,
}
)
@@ -353,29 +353,62 @@ watch(
let clipboardEv: ClipboardEvent | null = null
let pastedValue: string | null = null
const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
AggregateEnvironment[]
>
const tabs = useService(RESTTabService)
const envVars = computed(() => {
return props.envs
? props.envs.map((x) => {
if (x.secret) {
return {
key: x.key,
sourceEnv: "source" in x ? x.source : null,
value: "********",
}
}
return {
key: x.key,
value: x.value,
sourceEnv: "source" in x ? x.source : null,
}
})
: aggregateEnvs.value
if (props.envs) {
return props.envs.map((x) => {
const { key, secret } = x
const value = secret ? "********" : x.value
const sourceEnv = "sourceEnv" in x ? x.sourceEnv : null
return {
key,
value,
sourceEnv,
secret,
}
})
}
return [
...tabs.currentActiveTab.value.document.request.requestVariables.map(
({ active, key, value }) =>
active
? {
key,
value,
sourceEnv: "RequestVariable",
secret: false,
}
: ({} as AggregateEnvironment)
),
...aggregateEnvs.value,
]
})
function envAutoCompletion(context: CompletionContext) {
const options = (envVars.value ?? [])
.map((env) => ({
label: env?.key ? `<<${env.key}>>` : "",
info: env?.value ?? "",
apply: env?.key ? `<<${env.key}>>` : "",
}))
.filter((x) => x)
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos)
const tagBefore = /<<\w*$/.exec(textBefore)
if (!tagBefore && !context.explicit) return null
return {
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
options: options,
validFor: /^(<<\w*)?$/,
}
}
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
function handleTextSelection() {
@@ -469,6 +502,12 @@ const getExtensions = (readonly: boolean): Extension => {
}
},
}),
props.autoCompleteEnv
? autocompletion({
activateOnTyping: true,
override: [envAutoCompletion],
})
: [],
ViewPlugin.fromClass(
class {
update(update: ViewUpdate) {

View File

@@ -243,7 +243,7 @@ export function useCodemirror(
if (from === to) return
const text = view.value?.state.doc.sliceString(from, to)
const { top, left } = view.value?.coordsAtPos(from)
if (text) {
if (text?.trim()) {
invokeAction("contextmenu.open", {
position: {
top,

View File

@@ -1,293 +0,0 @@
import {
FormDataKeyValue,
HoppRESTAuth,
HoppRESTHeader,
HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest,
RESTReqSchemaVersion,
ValidContentTypes,
} from "@hoppscotch/data"
import { BehaviorSubject, combineLatest, map } from "rxjs"
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
import { HoppRESTResponse } from "./types/HoppRESTResponse"
export class RESTRequest {
public v$ = new BehaviorSubject<typeof RESTReqSchemaVersion>(
RESTReqSchemaVersion
)
public name$ = new BehaviorSubject("Untitled")
public endpoint$ = new BehaviorSubject("https://echo.hoppscotch.io/")
public params$ = new BehaviorSubject<HoppRESTParam[]>([])
public headers$ = new BehaviorSubject<HoppRESTHeader[]>([])
public method$ = new BehaviorSubject("GET")
public auth$ = new BehaviorSubject<HoppRESTAuth>({
authType: "none",
authActive: true,
})
public preRequestScript$ = new BehaviorSubject("")
public testScript$ = new BehaviorSubject("")
public body$ = new BehaviorSubject<HoppRESTReqBody>({
contentType: null,
body: null,
})
public response$ = new BehaviorSubject<HoppRESTResponse | null>(null)
get request$() {
// any of above changes construct requests
return combineLatest([
this.v$,
this.name$,
this.endpoint$,
this.params$,
this.headers$,
this.method$,
this.auth$,
this.preRequestScript$,
this.testScript$,
this.body$,
]).pipe(
map(
([
v,
name,
endpoint,
params,
headers,
method,
auth,
preRequestScript,
testScript,
body,
]) => ({
v,
name,
endpoint,
params,
headers,
method,
auth,
preRequestScript,
testScript,
body,
})
)
)
}
get contentType$() {
return this.body$.pipe(map((body) => body.contentType))
}
get bodyContent$() {
return this.body$.pipe(map((body) => body.body))
}
get headersCount$() {
return this.headers$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
.length
)
)
}
get paramsCount$() {
return this.params$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
.length
)
)
}
setName(name: string) {
this.name$.next(name)
}
setEndpoint(newURL: string) {
this.endpoint$.next(newURL)
}
setMethod(newMethod: string) {
this.method$.next(newMethod)
}
setParams(entries: HoppRESTParam[]) {
this.params$.next(entries)
}
addParam(newParam: HoppRESTParam) {
const newParams = this.params$.value.concat(newParam)
this.params$.next(newParams)
}
updateParam(index: number, updatedParam: HoppRESTParam) {
const newParams = this.params$.value.map((param, i) =>
i === index ? updatedParam : param
)
this.params$.next(newParams)
}
deleteParam(index: number) {
const newParams = this.params$.value.filter((_, i) => i !== index)
this.params$.next(newParams)
}
deleteAllParams() {
this.params$.next([])
}
setHeaders(entries: HoppRESTHeader[]) {
this.headers$.next(entries)
}
addHeader(newHeader: HoppRESTHeader) {
const newHeaders = this.headers$.value.concat(newHeader)
this.headers$.next(newHeaders)
}
updateHeader(index: number, updatedHeader: HoppRESTHeader) {
const newHeaders = this.headers$.value.map((header, i) =>
i === index ? updatedHeader : header
)
this.headers$.next(newHeaders)
}
deleteHeader(index: number) {
const newHeaders = this.headers$.value.filter((_, i) => i !== index)
this.headers$.next(newHeaders)
}
deleteAllHeaders() {
this.headers$.next([])
}
setContentType(newContentType: ValidContentTypes | null) {
// TODO: persist body evenafter switching content typees
this.body$.next(applyBodyTransition(this.body$.value, newContentType))
}
setBody(newBody: string | FormDataKeyValue[] | null) {
const body = { ...this.body$.value }
body.body = newBody
this.body$.next({ ...body })
}
addFormDataEntry(entry: FormDataKeyValue) {
if (this.body$.value.contentType !== "multipart/form-data") return {}
const body: HoppRESTReqBody = {
contentType: "multipart/form-data",
body: [...this.body$.value.body, entry],
}
this.body$.next(body)
}
deleteFormDataEntry(index: number) {
// Only perform update if the current content-type is formdata
if (this.body$.value.contentType !== "multipart/form-data") return {}
const body: HoppRESTReqBody = {
contentType: "multipart/form-data",
body: [...this.body$.value.body.filter((_, i) => i !== index)],
}
this.body$.next(body)
}
updateFormDataEntry(index: number, entry: FormDataKeyValue) {
// Only perform update if the current content-type is formdata
if (this.body$.value.contentType !== "multipart/form-data") return {}
const body: HoppRESTReqBody = {
contentType: "multipart/form-data",
body: [
...this.body$.value.body.map((oldEntry, i) =>
i === index ? entry : oldEntry
),
],
}
this.body$.next(body)
}
deleteAllFormDataEntries() {
// Only perform update if the current content-type is formdata
if (this.body$.value.contentType !== "multipart/form-data") return {}
const body: HoppRESTReqBody = {
contentType: "multipart/form-data",
body: [],
}
this.body$.next(body)
}
setRequestBody(newBody: HoppRESTReqBody) {
this.body$.next(newBody)
}
setAuth(newAuth: HoppRESTAuth) {
this.auth$.next(newAuth)
}
setPreRequestScript(newScript: string) {
this.preRequestScript$.next(newScript)
}
setTestScript(newScript: string) {
this.testScript$.next(newScript)
}
updateResponse(response: HoppRESTResponse | null) {
this.response$.next(response)
}
setRequest(request: HoppRESTRequest) {
this.v$.next(RESTReqSchemaVersion)
this.name$.next(request.name)
this.endpoint$.next(request.endpoint)
this.params$.next(request.params)
this.headers$.next(request.headers)
this.method$.next(request.method)
this.auth$.next(request.auth)
this.preRequestScript$.next(request.preRequestScript)
this.testScript$.next(request.testScript)
this.body$.next(request.body)
}
getRequest() {
return {
v: this.v$.value,
name: this.name$.value,
endpoint: this.endpoint$.value,
params: this.params$.value,
headers: this.headers$.value,
method: this.method$.value,
auth: this.auth$.value,
preRequestScript: this.preRequestScript$.value,
testScript: this.testScript$.value,
body: this.body$.value,
}
}
resetRequest() {
this.v$.next(RESTReqSchemaVersion)
this.name$.next("")
this.endpoint$.next("")
this.params$.next([])
this.headers$.next([])
this.method$.next("GET")
this.auth$.next({
authType: "none",
authActive: false,
})
this.preRequestScript$.next("")
this.testScript$.next("")
this.body$.next({
contentType: null,
body: null,
})
}
}

View File

@@ -1,4 +1,8 @@
import { Environment } from "@hoppscotch/data"
import {
Environment,
HoppRESTHeaders,
HoppRESTRequestVariable,
} from "@hoppscotch/data"
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
import { runTestScript } from "@hoppscotch/js-sandbox/web"
import * as A from "fp-ts/Array"
@@ -65,10 +69,17 @@ const getTestableBody = (
return x
}
const combineEnvVariables = (envs: {
global: Environment["variables"]
selected: Environment["variables"]
}) => [...envs.selected, ...envs.global]
const combineEnvVariables = (variables: {
environments: {
selected: Environment["variables"]
global: Environment["variables"]
}
requestVariables: Environment["variables"]
}) => [
...variables.requestVariables,
...variables.environments.selected,
...variables.environments.global,
]
export const executedResponses$ = new Subject<
HoppRESTResponse & { type: "success" | "fail " }
@@ -122,6 +133,38 @@ const updateEnvironmentsWithSecret = (
return updatedEnv
}
/**
* Transforms the environment list to a list with unique keys with value
* @param envs The environment list to be transformed
* @returns The transformed environment list with keys with value
*/
const filterNonEmptyEnvironmentVariables = (
envs: Environment["variables"]
): Environment["variables"] => {
const envsMap = new Map<string, Environment["variables"][number]>()
envs.forEach((env) => {
if (env.secret) {
envsMap.set(env.key, env)
} else if (envsMap.has(env.key)) {
const existingEnv = envsMap.get(env.key)
if (
existingEnv &&
"value" in existingEnv &&
existingEnv.value === "" &&
env.value !== ""
) {
envsMap.set(env.key, env)
}
} else {
envsMap.set(env.key, env)
}
})
return Array.from(envsMap.values())
}
export function runRESTRequest$(
tab: Ref<HoppTab<HoppRESTDocument>>
): [
@@ -175,15 +218,40 @@ export function runRESTRequest$(
requestHeaders = [...tab.value.document.request.headers]
}
const finalRequestVariables =
tab.value.document.request.requestVariables.map(
(v: HoppRESTRequestVariable) => {
if (v.active) {
return {
key: v.key,
value: v.value,
secret: false,
}
}
return []
}
)
const finalRequest = {
...tab.value.document.request,
auth: requestAuth ?? { authType: "none", authActive: false },
headers: requestHeaders,
headers: requestHeaders as HoppRESTHeaders,
}
const finalEnvs = {
requestVariables: finalRequestVariables as Environment["variables"],
environments: envs.right,
}
const finalEnvsWithNonEmptyValues = filterNonEmptyEnvironmentVariables(
combineEnvVariables(finalEnvs)
)
const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
id: "env-id",
v: 1,
name: "Env",
variables: combineEnvVariables(envs.right),
variables: finalEnvsWithNonEmptyValues,
})
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)

View File

@@ -38,6 +38,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -141,6 +142,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -158,6 +160,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -181,6 +184,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -209,6 +213,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -239,6 +244,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -268,6 +274,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -291,6 +298,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -312,6 +320,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -335,6 +344,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -366,6 +376,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -430,6 +441,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -458,6 +470,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -515,6 +528,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -560,6 +574,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -607,6 +622,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -621,6 +637,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -645,6 +662,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -662,6 +680,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -685,6 +704,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -702,6 +722,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -725,6 +746,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -747,6 +769,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -767,6 +790,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -788,6 +812,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -808,6 +833,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -839,6 +865,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
]

View File

@@ -181,5 +181,6 @@ export const parseCurlCommand = (curlCommand: string) => {
testScript: defaultRESTReq.testScript,
auth,
body: finalBody,
requestVariables: defaultRESTReq.requestVariables,
})
}

View File

@@ -13,7 +13,7 @@ import { StreamSubscriberFunc } from "@composables/stream"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
getAggregateEnvsWithSecrets,
getAggregateEnvs,
getCurrentEnvironment,
getSelectedEnvironmentType,
} from "~/newstore/environments"
@@ -23,15 +23,45 @@ import IconUsers from "~icons/lucide/users?raw"
import IconEdit from "~icons/lucide/edit?raw"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
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_ENV_HIGHLIGHT_FOUND = "env-found"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "env-not-found"
const HOPP_REQUEST_VARIABLE_HIGHLIGHT = "request-variable-highlight"
const HOPP_ENVIRONMENT_HIGHLIGHT = "environment-variable-highlight"
const HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT = "global-variable-highlight"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "environment-not-found-highlight"
const secretEnvironmentService = getService(SecretEnvironmentService)
const restTabs = getService(RESTTabService)
/**
* Transforms the environment list to a list with unique keys with value
* @param envs The environment list to be transformed
* @returns The transformed environment list with keys with value
*/
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?.value === "" && env.value !== "") {
envsMap.set(env.key, env)
}
} else {
envsMap.set(env.key, env)
}
})
return Array.from(envsMap.values())
}
const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
hoverTooltip(
@@ -67,7 +97,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const parsedEnvKey = text.slice(start - from, end - from)
const tooltipEnv = aggregateEnvs.find((env) => env.key === parsedEnvKey)
const envsWithNoEmptyValues =
filterNonEmptyEnvironmentVariables(aggregateEnvs)
const tooltipEnv = envsWithNoEmptyValues.find(
(env) => env.key === parsedEnvKey
)
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
@@ -95,7 +130,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const result = parseTemplateStringE(envValue, aggregateEnvs)
const finalEnv = E.isLeft(result) ? "error" : result.right
let finalEnv = E.isLeft(result) ? "error" : result.right
// If the request variable has an secret variable
// parseTemplateStringE is passed the secret value which has value undefined
// So, we need to check if the result is undefined and then set the finalEnv to ******
if (finalEnv === "undefined") finalEnv = "******"
const selectedEnvType = getSelectedEnvironmentType()
@@ -123,11 +163,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
invokeActionType = "modals.my.environment.edit"
}
invokeAction(invokeActionType, {
envName: tooltipEnv?.sourceEnv !== "Global" ? envName : "Global",
variableName: parsedEnvKey,
isSecret: tooltipEnv?.secret,
})
if (tooltipEnv?.sourceEnv === "RequestVariable") {
restTabs.currentActiveTab.value.document.optionTabPreference =
"requestVariables"
} else {
invokeAction(invokeActionType, {
envName: tooltipEnv?.sourceEnv === "Global" ? "Global" : envName,
variableName: parsedEnvKey,
isSecret: tooltipEnv?.secret,
})
}
})
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
tooltip.appendChild(editIcon)
@@ -165,11 +210,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
)
function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
const className = aggregateEnvs.find(
let className = HOPP_ENV_HIGHLIGHT_NOT_FOUND
const envSource = aggregateEnvs.find(
(k: { key: string }) => k.key === env.slice(2, -2)
)
? HOPP_ENV_HIGHLIGHT_FOUND
: HOPP_ENV_HIGHLIGHT_NOT_FOUND
)?.sourceEnv
if (envSource === "RequestVariable")
className = HOPP_REQUEST_VARIABLE_HIGHLIGHT
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}`,
@@ -185,7 +235,10 @@ const getMatchDecorator = (aggregateEnvs: AggregateEnvironment[]) =>
export const environmentHighlightStyle = (
aggregateEnvs: AggregateEnvironment[]
) => {
const decorator = getMatchDecorator(aggregateEnvs)
const envsWithNoEmptyValues =
filterNonEmptyEnvironmentVariables(aggregateEnvs)
const decorator = getMatchDecorator(envsWithNoEmptyValues)
return ViewPlugin.define(
(view) => ({
@@ -209,10 +262,45 @@ export class HoppEnvironmentPlugin {
subscribeToStream: StreamSubscriberFunc,
private editorView: Ref<EditorView | undefined>
) {
this.envs = getAggregateEnvsWithSecrets()
const aggregateEnvs = getAggregateEnvs()
const currentTab = restTabs.currentActiveTab.value
watch(
currentTab.document.request,
(reqVariables) => {
this.envs = [
...reqVariables.requestVariables.map(({ key, value }) => ({
key,
value,
sourceEnv: "RequestVariable",
secret: false,
})),
...aggregateEnvs,
]
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
},
{ immediate: true, deep: true }
)
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
this.envs = envs
this.envs = [
...currentTab.document.request.requestVariables.map(
({ key, value }) => ({
key,
value,
sourceEnv: "RequestVariable",
secret: false,
})
),
...envs,
]
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
@@ -251,7 +339,7 @@ export class HoppReactiveEnvPlugin {
]),
})
},
{ immediate: true }
{ immediate: true, deep: true }
)
}

View File

@@ -8,6 +8,7 @@ import {
knownContentTypes,
makeCollection,
makeRESTRequest,
HoppRESTRequestVariable,
} from "@hoppscotch/data"
import * as A from "fp-ts/Array"
@@ -28,14 +29,27 @@ type UnwrapPromise<T extends Promise<any>> = T extends Promise<infer Y>
type InsomniaDoc = UnwrapPromise<ReturnType<typeof convert>>
type InsomniaResource = ImportRequest
// insomnia-importers v3.6.0 doesn't provide a type for path parameters and they have deprecated the library
type InsomniaPathParameter = {
name: string
value: string
}
type InsomniaFolderResource = ImportRequest & { _type: "request_group" }
type InsomniaRequestResource = ImportRequest & { _type: "request" }
type InsomniaRequestResource = ImportRequest & {
_type: "request"
} & {
pathParameters?: InsomniaPathParameter[]
}
const parseInsomniaDoc = (content: string) =>
TO.tryCatch(() => convert(content))
const replacePathVarTemplating = (expression: string) =>
expression.replaceAll(/:([^/]+)/g, "<<$1>>")
const replaceVarTemplating = (expression: string) =>
replaceInsomniaTemplating(expression)
pipe(expression, replacePathVarTemplating, replaceInsomniaTemplating)
const getFoldersIn = (
folder: InsomniaFolderResource | null,
@@ -177,6 +191,15 @@ const getHoppReqParams = (req: InsomniaRequestResource): HoppRESTParam[] =>
active: !(param.disabled ?? false),
})) ?? []
const getHoppReqVariables = (
req: InsomniaRequestResource
): HoppRESTRequestVariable[] =>
req.pathParameters?.map((variable) => ({
key: replaceVarTemplating(variable.name),
value: replaceVarTemplating(variable.value ?? ""),
active: true,
})) ?? []
const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
makeRESTRequest({
name: req.name ?? "Untitled Request",
@@ -189,6 +212,8 @@ const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
preRequestScript: "",
testScript: "",
requestVariables: getHoppReqVariables(req),
})
const getHoppFolder = (

View File

@@ -16,6 +16,7 @@ import {
makeRESTRequest,
HoppCollection,
makeCollection,
HoppRESTRequestVariable,
} from "@hoppscotch/data"
import { pipe, flow } from "fp-ts/function"
import * as A from "fp-ts/Array"
@@ -82,6 +83,27 @@ const parseOpenAPIParams = (params: OpenAPIParamsType[]): HoppRESTParam[] =>
)
)
const parseOpenAPIVariables = (
variables: OpenAPIParamsType[]
): HoppRESTRequestVariable[] =>
pipe(
variables,
A.filterMap(
flow(
O.fromPredicate((param) => param.in === "path"),
O.map(
(param) =>
<HoppRESTRequestVariable>{
key: param.name,
value: "", // TODO: Can we do anything more ? (parse default values maybe)
active: true,
}
)
)
)
)
const parseOpenAPIHeaders = (params: OpenAPIParamsType[]): HoppRESTHeader[] =>
pipe(
params,
@@ -577,6 +599,10 @@ const convertPathToHoppReqs = (
preRequestScript: "",
testScript: "",
requestVariables: parseOpenAPIVariables(
(info.parameters as OpenAPIParamsType[] | undefined) ?? []
),
})
}),

View File

@@ -5,6 +5,7 @@ import {
QueryParam,
RequestAuthDefinition,
VariableDefinition,
Variable,
} from "postman-collection"
import {
HoppRESTAuth,
@@ -18,6 +19,7 @@ import {
ValidContentTypes,
knownContentTypes,
FormDataKeyValue,
HoppRESTRequestVariable,
} from "@hoppscotch/data"
import { pipe, flow } from "fp-ts/function"
import * as S from "fp-ts/string"
@@ -91,6 +93,25 @@ const getHoppReqParams = (item: Item): HoppRESTParam[] => {
)
}
const getHoppReqVariables = (item: Item) => {
return pipe(
item.request.url.variables.all(),
A.filter(
(variable): variable is Variable =>
variable.key !== undefined &&
variable.key !== null &&
variable.key.length > 0
),
A.map((variable) => {
return <HoppRESTRequestVariable>{
key: replacePMVarTemplating(variable.key ?? ""),
value: replacePMVarTemplating(variable.value ?? ""),
active: !variable.disabled,
}
})
)
}
type PMRequestAuthDef<
AuthType extends
RequestAuthDefinition["type"] = RequestAuthDefinition["type"],
@@ -280,6 +301,7 @@ const getHoppRequest = (item: Item): HoppRESTRequest => {
params: getHoppReqParams(item),
auth: getHoppReqAuth(item),
body: getHoppReqBody(item),
requestVariables: getHoppReqVariables(item),
// TODO: Decide about this
preRequestScript: "",

View File

@@ -14,8 +14,8 @@ import { SecretEnvironmentService } from "~/services/secret-environment.service"
const secretEnvironmentService = getService(SecretEnvironmentService)
const unsecretEnvironments = (
global: Environment["variables"],
selected: Environment
selected: Environment,
global: Environment["variables"]
) => {
const resolvedGlobalWithSecrets = global.map((globalVar, index) => {
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
@@ -65,8 +65,8 @@ const unsecretEnvironments = (
export const getCombinedEnvVariables = () => {
const reformedVars = unsecretEnvironments(
getGlobalVariables(),
getCurrentEnvironment()
getCurrentEnvironment(),
getGlobalVariables()
)
return {
global: cloneDeep(reformedVars.global),

View File

@@ -17,4 +17,5 @@ export const getDefaultRESTRequest = (): HoppRESTRequest => ({
contentType: null,
body: null,
},
requestVariables: [],
})

View File

@@ -22,7 +22,6 @@ import {
import { arrayFlatMap, arraySort } from "../functional/array"
import { toFormData } from "../functional/formData"
import { tupleWithSameKeysToRecord } from "../functional/record"
import { getGlobalVariables } from "~/newstore/environments"
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
/**
@@ -34,6 +33,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalHeaders: { key: string; value: string }[]
effectiveFinalParams: { key: string; value: string }[]
effectiveFinalBody: FormData | string | null
effectiveFinalRequestVariables: { key: string; value: string }[]
}
/**
@@ -313,38 +313,53 @@ export function getEffectiveRESTRequest(
request: HoppRESTRequest,
environment: Environment
): EffectiveHoppRESTRequest {
const envVariables = [...environment.variables, ...getGlobalVariables()]
const effectiveFinalHeaders = pipe(
getComputedHeaders(request, envVariables).map((h) => h.header),
getComputedHeaders(request, environment.variables).map((h) => h.header),
A.concat(request.headers),
A.filter((x) => x.active && x.key !== ""),
A.map((x) => ({
active: true,
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
key: parseTemplateString(x.key, environment.variables),
value: parseTemplateString(x.value, environment.variables),
}))
)
const effectiveFinalParams = pipe(
getComputedParams(request, envVariables).map((p) => p.param),
getComputedParams(request, environment.variables).map((p) => p.param),
A.concat(request.params),
A.filter((x) => x.active && x.key !== ""),
A.map((x) => ({
active: true,
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
key: parseTemplateString(x.key, environment.variables),
value: parseTemplateString(x.value, environment.variables),
}))
)
const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
const effectiveFinalRequestVariables = pipe(
request.requestVariables,
A.filter((x) => x.active && x.key !== ""),
A.map((x) => ({
active: true,
key: parseTemplateString(x.key, environment.variables),
value: parseTemplateString(x.value, environment.variables),
}))
)
const effectiveFinalBody = getFinalBodyFromRequest(
request,
environment.variables
)
return {
...request,
effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
effectiveFinalURL: parseTemplateString(
request.endpoint,
environment.variables
),
effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
effectiveFinalRequestVariables,
}
}

View File

@@ -353,6 +353,7 @@ executedResponses$.subscribe((res) => {
params: res.req.params,
preRequestScript: res.req.preRequestScript,
testScript: res.req.testScript,
requestVariables: res.req.requestVariables,
v: res.req.v,
},
responseMeta: {

View File

@@ -38,6 +38,7 @@ export type SettingsDef = {
httpUrlEncoded: boolean
httpPreRequest: boolean
httpTest: boolean
httpRequestVariables: boolean
graphqlQuery: boolean
graphqlResponseBody: boolean
graphqlHeaders: boolean
@@ -79,6 +80,7 @@ export const getDefaultSettings = (): SettingsDef => ({
httpUrlEncoded: true,
httpPreRequest: true,
httpTest: true,
httpRequestVariables: true,
graphqlQuery: true,
graphqlResponseBody: true,
graphqlHeaders: false,

View File

@@ -10,6 +10,7 @@ import { Ref, markRaw } from "vue"
import IconPlusCircle from "~icons/lucide/plus-circle"
import { HoppRESTRequest } from "@hoppscotch/data"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
getCurrentEnvironment,
getSelectedEnvironmentType,
@@ -18,6 +19,7 @@ import { invokeAction } from "~/helpers/actions"
import { computed } from "vue"
import { useStreamStatic } from "~/composables/stream"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { RESTTabService } from "~/services/tab/rest"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
@@ -41,6 +43,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
private readonly inspection = this.bind(InspectionService)
private readonly secretEnvs = this.bind(SecretEnvironmentService)
private readonly restTabs = this.bind(RESTTabService)
private aggregateEnvsWithSecrets = useStreamStatic(
aggregateEnvsWithSecrets$,
@@ -68,7 +71,14 @@ export class EnvironmentInspectorService extends Service implements Inspector {
) => {
const newErrors: InspectorResult[] = []
const envKeys = this.aggregateEnvsWithSecrets.value.map((e) => e.key)
const currentTab = this.restTabs.currentActiveTab.value
const environmentVariables = [
...currentTab.document.request.requestVariables,
...this.aggregateEnvsWithSecrets.value,
]
const envKeys = environmentVariables.map((e) => e.key)
target.forEach((element, index) => {
if (isENVInString(element)) {
@@ -124,6 +134,31 @@ export class EnvironmentInspectorService extends Service implements Inspector {
return newErrors
}
/**
* Transforms the environment list to a list with unique keys with value
* @param envs The environment list to be transformed
* @returns The transformed environment list with keys with value
*/
private 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?.value === "" && env.value !== "") {
envsMap.set(env.key, env)
}
} else {
envsMap.set(env.key, env)
}
})
return Array.from(envsMap.values())
}
/**
* Checks if the environment variables in the target array are empty
* @param target The target array to validate
@@ -145,7 +180,19 @@ export class EnvironmentInspectorService extends Service implements Inspector {
const formattedExEnv = exEnv.slice(2, -2)
const currentSelectedEnvironment = getCurrentEnvironment()
this.aggregateEnvsWithSecrets.value.forEach((env) => {
const currentTab = this.restTabs.currentActiveTab.value
const environmentVariables =
this.filterNonEmptyEnvironmentVariables([
...currentTab.document.request.requestVariables.map((env) => ({
...env,
secret: false,
sourceEnv: "RequestVariable",
})),
...this.aggregateEnvsWithSecrets.value,
])
environmentVariables.forEach((env) => {
const hasSecretEnv = this.secretEnvs.hasSecretValue(
env.sourceEnv !== "Global"
? currentSelectedEnvironment.id
@@ -199,14 +246,19 @@ export class EnvironmentInspectorService extends Service implements Inspector {
"inspections.environment.add_environment_value"
),
apply: () => {
invokeAction(invokeActionType, {
envName:
env.sourceEnv !== "Global"
? currentSelectedEnvironment.name
: "Global",
variableName: formattedExEnv,
isSecret: env.secret,
})
if (env.sourceEnv === "RequestVariable") {
currentTab.document.optionTabPreference =
"requestVariables"
} else {
invokeAction(invokeActionType, {
envName:
env.sourceEnv === "Global"
? "Global"
: currentSelectedEnvironment.name,
variableName: formattedExEnv,
isSecret: env.secret,
})
}
},
},
severity: 2,

View File

@@ -25,7 +25,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
folders: [],
requests: [
{
v: "1",
v: "2",
endpoint: "https://echo.hoppscotch.io",
name: "Echo test",
params: [],
@@ -35,6 +35,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
preRequestScript: "",
testScript: "",
body: { contentType: null, body: null },
requestVariables: [],
},
],
auth: { authType: "none", authActive: true },
@@ -136,7 +137,8 @@ export const REST_HISTORY_MOCK: RESTHistoryEntry[] = [
params: [],
preRequestScript: "",
testScript: "",
v: "1",
requestVariables: [],
v: "2",
},
responseMeta: { duration: 807, statusCode: 200 },
star: false,
@@ -192,7 +194,7 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
tabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
doc: {
request: {
v: "1",
v: "2",
endpoint: "https://echo.hoppscotch.io",
name: "Echo test",
params: [],
@@ -202,6 +204,7 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
preRequestScript: "",
testScript: "",
body: { contentType: null, body: null },
requestVariables: [],
},
isDirty: false,
saveContext: {

View File

@@ -48,21 +48,22 @@ const SettingsDefSchema = z.object({
WRAP_LINES: z.optional(
z.object({
httpRequestBody: z.boolean(),
httpResponseBody: z.boolean(),
httpHeaders: z.boolean(),
httpParams: z.boolean(),
httpUrlEncoded: z.boolean(),
httpPreRequest: z.boolean(),
httpTest: z.boolean(),
graphqlQuery: z.boolean(),
graphqlResponseBody: z.boolean(),
graphqlHeaders: z.boolean(),
graphqlVariables: z.boolean(),
graphqlSchema: z.boolean(),
importCurl: z.boolean(),
codeGen: z.boolean(),
cookie: z.boolean(),
httpRequestBody: z.boolean().catch(true),
httpResponseBody: z.boolean().catch(true),
httpHeaders: z.boolean().catch(true),
httpParams: z.boolean().catch(true),
httpUrlEncoded: z.boolean().catch(true),
httpPreRequest: z.boolean().catch(true),
httpTest: z.boolean().catch(true),
httpRequestVariables: z.boolean().catch(true),
graphqlQuery: z.boolean().catch(true),
graphqlResponseBody: z.boolean().catch(true),
graphqlHeaders: z.boolean().catch(false),
graphqlVariables: z.boolean().catch(false),
graphqlSchema: z.boolean().catch(true),
importCurl: z.boolean().catch(true),
codeGen: z.boolean().catch(true),
cookie: z.boolean().catch(true),
})
),
})
@@ -514,6 +515,7 @@ const validRestOperations = [
"authorization",
"preRequestScript",
"tests",
"requestVariables",
] as const
export const REST_TAB_STATE_SCHEMA = z

View File

@@ -3,6 +3,7 @@ import * as S from "fp-ts/string"
import cloneDeep from "lodash/cloneDeep"
import V0_VERSION from "./v/0"
import V1_VERSION from "./v/1"
import V2_VERSION from "./v/2"
import { createVersionedEntity, InferredEntity } from "verzod"
import { lodashIsEqualEq, mapThenEq, undefinedEq } from "../utils/eq"
import {
@@ -11,6 +12,7 @@ import {
HoppRESTHeaders,
HoppRESTParams,
} from "./v/1"
import { HoppRESTRequestVariables } from "./v/2"
import { z } from "zod"
export * from "./content-types"
@@ -28,16 +30,19 @@ export {
HoppRESTHeaders,
} from "./v/1"
export { HoppRESTRequestVariables } from "./v/2"
const versionedObject = z.object({
// v is a stringified number
v: z.string().regex(/^\d+$/).transform(Number),
})
export const HoppRESTRequest = createVersionedEntity({
latestVersion: 1,
latestVersion: 2,
versionMap: {
0: V0_VERSION,
1: V1_VERSION,
2: V2_VERSION,
},
getVersion(data) {
// For V1 onwards we have the v string storing the number
@@ -73,12 +78,18 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
name: S.Eq,
preRequestScript: S.Eq,
testScript: S.Eq,
requestVariables: mapThenEq(
(arr) => arr.filter((v: any) => v.key !== "" && v.value !== ""),
lodashIsEqualEq
),
})
export const RESTReqSchemaVersion = "1"
export const RESTReqSchemaVersion = "2"
export type HoppRESTParam = HoppRESTRequest["params"][number]
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
export type HoppRESTRequestVariable =
HoppRESTRequest["requestVariables"][number]
export const isEqualHoppRESTRequest = HoppRESTRequestEq.equals
@@ -144,6 +155,14 @@ export function safelyExtractRESTRequest(
req.headers = result.data
}
}
if ("requestVariables" in x) {
const result = HoppRESTRequestVariables.safeParse(x.requestVariables)
if (result.success) {
req.requestVariables = result.data
}
}
}
return req
@@ -160,7 +179,7 @@ export function makeRESTRequest(
export function getDefaultRESTRequest(): HoppRESTRequest {
return {
v: "1",
v: "2",
endpoint: "https://echo.hoppscotch.io",
name: "Untitled",
params: [],
@@ -176,6 +195,7 @@ export function getDefaultRESTRequest(): HoppRESTRequest {
contentType: null,
body: null,
},
requestVariables: [],
}
}

View File

@@ -141,7 +141,7 @@ export const HoppRESTHeaders = z.array(
export type HoppRESTHeaders = z.infer<typeof HoppRESTHeaders>
const V1_SCHEMA = z.object({
export const V1_SCHEMA = z.object({
v: z.literal("1"),
id: z.optional(z.string()), // Firebase Firestore ID
@@ -158,7 +158,7 @@ const V1_SCHEMA = z.object({
body: HoppRESTReqBody,
})
function parseRequestBody(
export function parseRequestBody(
x: z.infer<typeof V0_SCHEMA>
): z.infer<typeof V1_SCHEMA>["body"] {
return {

View File

@@ -0,0 +1,50 @@
import { defineVersion } from "verzod"
import { z } from "zod"
import {
HoppRESTAuth,
HoppRESTHeaders,
HoppRESTParams,
HoppRESTReqBody,
} from "./1"
import { V1_SCHEMA } from "./1"
export const HoppRESTRequestVariables = z.array(
z.object({
key: z.string().catch(""),
value: z.string().catch(""),
active: z.boolean().catch(true),
})
)
export type HoppRESTRequestVariables = z.infer<typeof HoppRESTRequestVariables>
const V2_SCHEMA = z.object({
v: z.literal("2"),
id: z.optional(z.string()), // Firebase Firestore ID
name: z.string(),
method: z.string(),
endpoint: z.string(),
params: HoppRESTParams,
headers: HoppRESTHeaders,
preRequestScript: z.string().catch(""),
testScript: z.string().catch(""),
auth: HoppRESTAuth,
body: HoppRESTReqBody,
requestVariables: HoppRESTRequestVariables,
})
export default defineVersion({
initial: false,
schema: V2_SCHEMA,
up(old: z.infer<typeof V1_SCHEMA>) {
return {
...old,
v: "2",
requestVariables: [],
} as z.infer<typeof V2_SCHEMA>
},
})

View File

@@ -133,6 +133,7 @@ function exportedCollectionToHoppCollection(
params,
preRequestScript,
testScript,
requestVariables,
}) => ({
id,
v,
@@ -145,6 +146,7 @@ function exportedCollectionToHoppCollection(
params,
preRequestScript,
testScript,
requestVariables,
})
),
auth: data.auth,