feat: collection variables (#5325)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Nivedin 2025-08-22 14:39:42 +05:30 committed by GitHub
parent ad974dbd5b
commit 2d2e65369f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 2696 additions and 773 deletions

View file

@ -0,0 +1,102 @@
{
"id": "cmeicx49r00xylb1jxektmknk",
"_ref_id": "coll_meicx3z7_a1cb5e72-cd1b-414b-adc2-7d601ca0936d",
"v": 10,
"name": "coll-with-variables",
"folders": [
{
"id": "cmeicy6fu00xzlb1jqmmqbjdm",
"_ref_id": "coll_meie14lh_818ea8a2-9839-4a1c-8cce-cc7565b5f594",
"v": 10,
"name": "folder-1",
"folders": [],
"requests": [
{
"v": "15",
"id": "cmeicyhnn00y1lb1j8d80g7ys",
"name": "request-1",
"method": "GET",
"endpoint": "https://echo.hoppscotch.io",
"params": [
{
"key": "collection-var-1",
"value": "<<collection-variable-1>>",
"active": true,
"description": ""
},
{
"key": "collection-var-2",
"value": "<<collection-variable-2>>",
"active": true,
"description": ""
},
{
"key": "folder-var-1",
"value": "<<folder-variable-1>>",
"active": true,
"description": ""
},
{
"key": "folder-var-2",
"value": "<<folder-variable-2>>",
"active": true,
"description": ""
}
],
"headers": [],
"preRequestScript": "",
"testScript": "export {};\npw.test('Correctly inherits collection variables from the parent collection and folder', () =>\n{\n\n pw.expect([\n \"collection-var-1-initial-value\",\n \"collection-var-1-current-value\"\n ]).toInclude(pw.response.body.args[\"collection-var-1\"])\n\n pw.expect([\n \"collection-var-2-initial-value\",\n \"collection-var-2-current-value\"\n ]).toInclude(pw.response.body.args[\"collection-var-2\"])\n\n pw.expect([\n \"folder-variable-1-initial-value\",\n \"folder-variable-1-current-value\"\n ]).toInclude(pw.response.body.args[\"folder-var-1\"])\n\n pw.expect([\n \"folder-variable-2-initial-value\",\n \"folder-variable-2-current-value\"\n ]).toInclude(pw.response.body.args[\"folder-var-2\"])\n\n});",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"contentType": null,
"body": null
},
"requestVariables": [],
"responses": {}
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": [],
"variables": [
{
"key": "folder-variable-1",
"secret": false,
"currentValue": "",
"initialValue": "folder-variable-1-initial-value"
},
{
"key": "folder-variable-2",
"secret": false,
"currentValue": "",
"initialValue": "folder-variable-2-initial-value"
}
]
}
],
"requests": [],
"auth": {
"authType": "none",
"authActive": true
},
"headers": [],
"variables": [
{
"key": "collection-variable-1",
"secret": false,
"currentValue": "",
"initialValue": "collection-var-1-initial-value"
},
{
"key": "collection-variable-2",
"secret": false,
"currentValue": "",
"initialValue": "collection-var-2-initial-value"
}
]
}

View file

@ -1,7 +1,7 @@
{
"v": 6,
"id": "cm9wmuzj46s3imbs891pdamv4",
"name": "Multiple child collections with authorization & headers set at each level",
"name": "Multiple child collections with authorization, headers and variables set at each level",
"folders": [
{
"v": 6,
@ -688,5 +688,13 @@
"description": ""
}
],
"variables": [
{
"key": "collection-variable",
"currentValue": "collection-variable-value",
"initialValue": "collection-variable-value",
"secret": false
}
],
"_ref_id": "coll_m9wn4jl9_aa8a3bc2-a96f-4cac-86f3-2df4bb355cc8"
}

View file

@ -86,5 +86,6 @@
"authActive": true
},
"headers": [],
"variables": [],
"_ref_id": "coll_mbhuxoci_a8fc710e-04c1-489c-a183-7f16946a7225"
}

View file

@ -11,29 +11,29 @@ import {
WorkspaceEnvironment,
} from "../../../utils/workspace-access";
export const WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: WorkspaceCollection[] =
export const WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK: WorkspaceCollection[] =
[
{
id: "clx1ldkzs005t10f8rp5u60q7",
data: '{"auth":{"token":"BearerToken","authType":"bearer","authActive":true},"headers":[{"key":"X-Test-Header","value":"Set at root collection","active":true,"description":""}]}',
data: '{"auth":{"token":"BearerToken","authType":"bearer","authActive":true},"headers":[{"key":"X-Test-Header","value":"Set at root collection","active":true,"description":""}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "CollectionA",
parentID: null,
folders: [
{
id: "clx1ldkzs005v10f86b9wx4yc",
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[]}',
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[],"variables":[]}',
title: "FolderA",
parentID: "clx1ldkzs005t10f8rp5u60q7",
folders: [
{
id: "clx1ldkzt005x10f8i0u5lzgj",
data: '{"auth":{"key":"key","addTo":"HEADERS","value":"test-key","authType":"api-key","authActive":true},"headers":[{"key":"X-Test-Header","value":"Overriden at FolderB","active":true}]}',
data: '{"auth":{"key":"key","addTo":"HEADERS","value":"test-key","authType":"api-key","authActive":true},"headers":[{"key":"X-Test-Header","value":"Overriden at FolderB","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "FolderB",
parentID: "clx1ldkzs005v10f86b9wx4yc",
folders: [
{
id: "clx1ldkzu005z10f880zx17bg",
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[]}',
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[],"variables":[]}',
title: "FolderC",
parentID: "clx1ldkzt005x10f8i0u5lzgj",
folders: [],
@ -85,7 +85,7 @@ export const WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Workspa
},
];
export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppCollection[] =
export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK: HoppCollection[] =
[
{
v: CollectionSchemaVersion,
@ -142,6 +142,7 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
authActive: true,
},
headers: [],
variables: [],
},
],
requests: [
@ -181,6 +182,14 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
],
requests: [
@ -211,6 +220,7 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
authActive: true,
},
headers: [],
variables: [],
},
],
requests: [
@ -250,27 +260,35 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
];
export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: WorkspaceCollection[] =
export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK: WorkspaceCollection[] =
[
{
id: "clx1f86hv000010f8szcfya0t",
data: '{"auth":{"authType":"basic","password":"testpass","username":"testuser","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value set at the root collection","active":true},{"key":"Inherited-Header","value":"Inherited header at all levels","active":true}]}',
data: '{"auth":{"authType":"basic","password":"testpass","username":"testuser","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value set at the root collection","active":true},{"key":"Inherited-Header","value":"Inherited header at all levels","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title:
"Multiple child collections with authorization & headers set at each level",
"Multiple child collections with authorization, headers and variables set at each level",
parentID: null,
folders: [
{
id: "clx1fjgah000110f8a5bs68gd",
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-1","active":true}]}',
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-1","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-1",
parentID: "clx1f86hv000010f8szcfya0t",
folders: [
{
id: "clx1fjwmm000410f8l1gkkr1a",
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[{"key":"key","value":"Set at folder-11","active":true}]}',
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[{"key":"key","value":"Set at folder-11","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-11",
parentID: "clx1fjgah000110f8a5bs68gd",
folders: [],
@ -287,7 +305,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
},
{
id: "clx1fjyxm000510f8pv90dt43",
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-12","active":true},{"key":"key","value":"Set at folder-12","active":true}]}',
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-12","active":true},{"key":"key","value":"Set at folder-12","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-12",
parentID: "clx1fjgah000110f8a5bs68gd",
folders: [],
@ -304,7 +322,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
},
{
id: "clx1fk1cv000610f88kc3aupy",
data: '{"auth":{"token":"test-token","authType":"bearer","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-13","active":true},{"key":"key","value":"Set at folder-13","active":true}]}',
data: '{"auth":{"token":"test-token","authType":"bearer","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-13","active":true},{"key":"key","value":"Set at folder-13","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-13",
parentID: "clx1fjgah000110f8a5bs68gd",
folders: [],
@ -333,13 +351,13 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
},
{
id: "clx1fjk9o000210f8j0573pls",
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-2","active":true}]}',
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-2","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-2",
parentID: "clx1f86hv000010f8szcfya0t",
folders: [
{
id: "clx1fk516000710f87sfpw6bo",
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[{"key":"key","value":"Set at folder-21","active":true}]}',
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[{"key":"key","value":"Set at folder-21","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-21",
parentID: "clx1fjk9o000210f8j0573pls",
folders: [],
@ -356,7 +374,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
},
{
id: "clx1fk72t000810f8gfwkpi5y",
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-22","active":true},{"key":"key","value":"Set at folder-22","active":true}]}',
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-22","active":true},{"key":"key","value":"Set at folder-22","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-22",
parentID: "clx1fjk9o000210f8j0573pls",
folders: [],
@ -373,7 +391,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
},
{
id: "clx1fk95g000910f8bunhaoo8",
data: '{"auth":{"token":"test-token","authType":"bearer","password":"testpass","username":"testuser","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-23","active":true},{"key":"key","value":"Set at folder-23","active":true}]}',
data: '{"auth":{"token":"test-token","authType":"bearer","password":"testpass","username":"testuser","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-23","active":true},{"key":"key","value":"Set at folder-23","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-23",
parentID: "clx1fjk9o000210f8j0573pls",
folders: [],
@ -402,13 +420,13 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
},
{
id: "clx1fjmlq000310f86o4d3w2o",
data: '{"auth":{"key":"testuser","addTo":"HEADERS","value":"testpass","authType":"basic","password":"testpass","username":"testuser","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-3","active":true}]}',
data: '{"auth":{"key":"testuser","addTo":"HEADERS","value":"testpass","authType":"basic","password":"testpass","username":"testuser","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-3","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-3",
parentID: "clx1f86hv000010f8szcfya0t",
folders: [
{
id: "clx1iwq0p003e10f8u8zg0p85",
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[{"key":"key","value":"Set at folder-31","active":true}]}',
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[{"key":"key","value":"Set at folder-31","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-31",
parentID: "clx1fjmlq000310f86o4d3w2o",
folders: [],
@ -425,7 +443,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
},
{
id: "clx1izut7003m10f894ip59zg",
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-32","active":true},{"key":"key","value":"Set at folder-32","active":true}]}',
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-32","active":true},{"key":"key","value":"Set at folder-32","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-32",
parentID: "clx1fjmlq000310f86o4d3w2o",
folders: [],
@ -442,7 +460,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
},
{
id: "clx1j2ka9003q10f8cdbzpgpg",
data: '{"auth":{"token":"test-token","authType":"bearer","password":"testpass","username":"testuser","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-33","active":true},{"key":"key","value":"Set at folder-33","active":true}]}',
data: '{"auth":{"token":"test-token","authType":"bearer","password":"testpass","username":"testuser","authActive":true},"headers":[{"key":"Custom-Header","value":"Custom header value overriden at folder-33","active":true},{"key":"key","value":"Set at folder-33","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-33",
parentID: "clx1fjmlq000310f86o4d3w2o",
folders: [],
@ -485,17 +503,17 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppCollection[] =
[
{
v: 9,
v: 10,
id: "clx1f86hv000010f8szcfya0t",
name: "Multiple child collections with authorization & headers set at each level",
name: "Multiple child collections with authorization, headers and variables set at each level",
folders: [
{
v: 9,
v: 10,
id: "clx1fjgah000110f8a5bs68gd",
name: "folder-1",
folders: [
{
v: 9,
v: 10,
id: "clx1fjwmm000410f8l1gkkr1a",
name: "folder-11",
folders: [],
@ -535,9 +553,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: 9,
v: 10,
id: "clx1fjyxm000510f8pv90dt43",
name: "folder-12",
folders: [],
@ -593,9 +619,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: 9,
v: 10,
id: "clx1fk1cv000610f88kc3aupy",
name: "folder-13",
folders: [],
@ -669,6 +703,14 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
],
requests: [
@ -705,14 +747,22 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: 9,
v: 10,
id: "clx1fjk9o000210f8j0573pls",
name: "folder-2",
folders: [
{
v: 9,
v: 10,
id: "clx1fk516000710f87sfpw6bo",
name: "folder-21",
folders: [],
@ -750,9 +800,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: 9,
v: 10,
id: "clx1fk72t000810f8gfwkpi5y",
name: "folder-22",
folders: [],
@ -808,9 +866,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: 9,
v: 10,
id: "clx1fk95g000910f8bunhaoo8",
name: "folder-23",
folders: [],
@ -871,6 +937,14 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
],
requests: [
@ -913,14 +987,23 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: 9,
v: 10,
id: "clx1fjmlq000310f86o4d3w2o",
name: "folder-3",
folders: [
{
v: 9,
v: 10,
id: "clx1iwq0p003e10f8u8zg0p85",
name: "folder-31",
folders: [],
@ -958,9 +1041,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: 9,
v: 10,
id: "clx1izut7003m10f894ip59zg",
name: "folder-32",
folders: [],
@ -1016,9 +1107,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: 9,
v: 10,
id: "clx1j2ka9003q10f8cdbzpgpg",
name: "folder-33",
folders: [],
@ -1079,6 +1178,14 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
],
requests: [
@ -1134,6 +1241,14 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
],
requests: [
@ -1179,16 +1294,24 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
];
// Collections with `data` field set to `null` at certain levels
export const WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK: WorkspaceCollection[] =
export const WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK: WorkspaceCollection[] =
[
{
id: "clx1kxvao005m10f8luqivrf1",
data: null,
title: "Collection with no authorization/headers set",
title: "Collection with no authorization/headers/variables set",
parentID: null,
folders: [
{
@ -1210,7 +1333,7 @@ export const WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK:
},
{
id: "clx1kym98005o10f8qg17t9o2",
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Set at folder-2","active":true}]}',
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Set at folder-2","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-2",
parentID: "clx1kxvao005m10f8luqivrf1",
folders: [],
@ -1235,7 +1358,7 @@ export const WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK:
},
{
id: "clx1l2eaz005s10f8loetbbeb",
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Set at folder-4","active":true}]}',
data: '{"auth":{"authType":"none","authActive":true},"headers":[{"key":"Custom-Header","value":"Set at folder-4","active":true}],"variables":[{"key":"collection-variable","currentValue":"collection-variable-value","initialValue":"collection-variable-value","secret":false}]}',
title: "folder-4",
parentID: "clx1kxvao005m10f8luqivrf1",
folders: [],
@ -1246,12 +1369,12 @@ export const WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK:
},
];
export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK: HoppCollection[] =
export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK: HoppCollection[] =
[
{
v: CollectionSchemaVersion,
id: "clx1kxvao005m10f8luqivrf1",
name: "Collection with no authorization/headers set",
name: "Collection with no authorization/headers/variables set",
folders: [
{
v: CollectionSchemaVersion,
@ -1284,6 +1407,7 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
authActive: true,
},
headers: [],
variables: [],
},
{
v: CollectionSchemaVersion,
@ -1323,6 +1447,14 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
{
v: CollectionSchemaVersion,
@ -1335,6 +1467,7 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
authActive: true,
},
headers: [],
variables: [],
},
{
v: CollectionSchemaVersion,
@ -1354,6 +1487,14 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
description: "",
},
],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
},
],
requests: [],
@ -1362,6 +1503,7 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
authActive: true,
},
headers: [],
variables: [],
},
];

View file

@ -5,18 +5,18 @@ import {
transformWorkspaceEnvironment,
} from "../../utils/workspace-access";
import {
TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK,
TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK,
TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK,
TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK,
TRANSFORMED_ENVIRONMENT_V0_FORMAT_MOCK,
TRANSFORMED_ENVIRONMENT_V1_FORMAT_MOCK,
TRANSFORMED_ENVIRONMENT_V2_FORMAT_MOCK,
TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK,
WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK,
WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK,
WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK,
WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK,
WORKSPACE_ENVIRONMENT_V0_FORMAT_MOCK,
WORKSPACE_ENVIRONMENT_V1_FORMAT_MOCK,
WORKSPACE_ENVIRONMENT_V2_FORMAT_MOCK,
WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK,
WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK,
} from "./fixtures/workspace-access.mock";
describe("workspace-access", () => {
@ -24,15 +24,17 @@ describe("workspace-access", () => {
test("Successfully transforms collection data with deeply nested collections and authorization/headers set at each level to the `HoppCollection` format", () => {
expect(
transformWorkspaceCollections(
WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK
WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK
)
).toEqual(TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK);
).toEqual(
TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK
);
});
test("Successfully transforms collection data with multiple child collections and authorization/headers set at each level to the `HoppCollection` format", () => {
expect(
transformWorkspaceCollections(
WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK
WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK
)
).toEqual(TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK);
});
@ -40,10 +42,10 @@ describe("workspace-access", () => {
test("Adds the default value for `auth` & `header` fields while transforming collections without authorization/headers set at certain levels", () => {
expect(
transformWorkspaceCollections(
WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK
)
).toEqual(
TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK
);
});
});

View file

@ -1,4 +1,9 @@
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import {
Environment,
HoppCollection,
HoppCollectionVariable,
HoppRESTRequest,
} from "@hoppscotch/data";
import { z } from "zod";
import { TestReport } from "../interfaces/response";
@ -37,5 +42,6 @@ export type ProcessRequestParams = {
envs: HoppEnvs;
path: string;
delay: number;
legacySandbox: boolean;
legacySandbox?: boolean;
collectionVariables?: HoppCollectionVariable[];
};

View file

@ -115,12 +115,18 @@ const processCollection = async (
for (const request of collection.requests) {
const _request = preProcessRequest(request as HoppRESTRequest, collection);
const requestPath = `${path}/${_request.name}`;
const collectionVariables = collection.variables.filter(
(variable) => variable.key && variable.key.trim() !== ""
);
const processRequestParams: ProcessRequestParams = {
path: requestPath,
request: _request,
envs,
delay,
legacySandbox,
collectionVariables,
};
// Request processing initiated message.
@ -161,6 +167,20 @@ const processCollection = async (
updatedFolder.headers.push(...filteredHeaders);
}
if (updatedFolder.variables?.length) {
// Filter out variable entries present in the parent collection under the same name
// This ensures the folder variables take precedence over the collection variables
const filteredVariables = collection.variables.filter(
(collectionVariableEntries) => {
return !updatedFolder.variables.some(
(folderVariableEntries) =>
folderVariableEntries.key === collectionVariableEntries.key
);
}
);
updatedFolder.variables.push(...filteredVariables);
}
await processCollection(
updatedFolder,
`${path}/${updatedFolder.name}`,

View file

@ -1,5 +1,6 @@
import {
EnvironmentVariable,
HoppCollectionVariable,
HoppRESTHeader,
HoppRESTParam,
HoppRESTRequestVariables,
@ -269,11 +270,13 @@ export const getResourceContents = async (
*
* @param {HoppRESTRequestVariables} requestVariables - Incoming request variables.
* @param {EnvironmentVariable[]} environmentVariables - Incoming environment variables.
* @param {HoppCollectionVariable[]} collectionVariables - Optional collection variables to be included.
* @returns {EnvironmentVariable[]} The resolved list of variables that conforms to the shape of environment variables.
*/
export const getResolvedVariables = (
requestVariables: HoppRESTRequestVariables,
environmentVariables: EnvironmentVariable[]
environmentVariables: EnvironmentVariable[],
collectionVariables: HoppCollectionVariable[] = []
): EnvironmentVariable[] => {
// Transforming request variables to the shape of environment variables
const activeRequestVariables = requestVariables
@ -287,11 +290,21 @@ export const getResolvedVariables = (
const requestVariableKeys = activeRequestVariables.map(({ key }) => key);
// Request variables have higher priority, hence filtering out environment variables with the same keys
const filteredEnvironmentVariables = environmentVariables.filter(
// Request variables have higher priority, hence filtering out collection variables with the same keys
const filteredCollectionVariables = collectionVariables.filter(
({ key }) => !requestVariableKeys.includes(key)
);
const collectionVariableKeys = filteredCollectionVariables.map(
({ key }) => key
);
// Filtering out environment variables that have keys present in request or collection variables
const filteredEnvironmentVariables = environmentVariables.filter(
({ key }) =>
![...requestVariableKeys, ...collectionVariableKeys].includes(key)
);
// Setting currentValue to initialValue for environment variables
// because the exported file might not have the currentValue field
const processedEnvironmentVariables = filteredEnvironmentVariables.map(
@ -304,5 +317,19 @@ export const getResolvedVariables = (
})
);
return [...activeRequestVariables, ...processedEnvironmentVariables];
const processedCollectionVariables = filteredCollectionVariables.map(
({ key, initialValue, currentValue, secret }) => ({
key,
initialValue,
currentValue:
currentValue && currentValue !== "" ? currentValue : initialValue,
secret,
})
);
return [
...activeRequestVariables,
...processedCollectionVariables,
...processedEnvironmentVariables,
];
};

View file

@ -7,6 +7,7 @@ import {
parseTemplateString,
parseTemplateStringE,
generateJWTToken,
HoppCollectionVariable,
} from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
@ -46,7 +47,8 @@ import { calculateHawkHeader } from "@hoppscotch/data";
export const preRequestScriptRunner = (
request: HoppRESTRequest,
envs: HoppEnvs,
legacySandbox: boolean
legacySandbox: boolean,
collectionVariables?: HoppCollectionVariable[]
): TE.TaskEither<
HoppCLIError,
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
@ -67,7 +69,7 @@ export const preRequestScriptRunner = (
),
TE.chainW((env) =>
TE.tryCatch(
() => getEffectiveRESTRequest(request, env),
() => getEffectiveRESTRequest(request, env, collectionVariables),
(reason) => error({ code: "PRE_REQUEST_SCRIPT_ERROR", data: reason })
)
),
@ -93,7 +95,8 @@ export const preRequestScriptRunner = (
*/
export async function getEffectiveRESTRequest(
request: HoppRESTRequest,
environment: Environment
environment: Environment,
collectionVariables?: HoppCollectionVariable[]
): Promise<
E.Either<
HoppCLIError,
@ -104,7 +107,8 @@ export async function getEffectiveRESTRequest(
const resolvedVariables = getResolvedVariables(
request.requestVariables,
envVariables
envVariables,
collectionVariables
);
// Parsing final headers with applied ENVs.

View file

@ -202,7 +202,8 @@ export const processRequest =
params: ProcessRequestParams
): T.Task<{ envs: HoppEnvs; report: RequestReport }> =>
async () => {
const { envs, path, request, delay, legacySandbox } = params;
const { envs, path, request, delay, legacySandbox, collectionVariables } =
params;
// Initialising updatedEnvs with given parameter envs, will eventually get updated.
const result = {
@ -236,7 +237,8 @@ export const processRequest =
const preRequestRes = await preRequestScriptRunner(
request,
processedEnvs,
legacySandbox
legacySandbox,
collectionVariables
)();
if (E.isLeft(preRequestRes)) {
printPreRequestRunner.fail();

View file

@ -3,6 +3,7 @@ import {
Environment,
EnvironmentSchemaVersion,
HoppCollection,
HoppCollectionVariable,
HoppRESTAuth,
HoppRESTHeaders,
HoppRESTRequest,
@ -173,12 +174,17 @@ export const transformWorkspaceCollections = (
return collections.map((collection) => {
const { id, title, data, requests, folders } = collection;
const parsedData: { auth?: HoppRESTAuth; headers?: HoppRESTHeaders } = data
? JSON.parse(data)
: {};
const parsedData: {
auth?: HoppRESTAuth;
headers?: HoppRESTHeaders;
variables: HoppCollectionVariable[];
} = data ? JSON.parse(data) : {};
const { auth = { authType: "inherit", authActive: true }, headers = [] } =
parsedData;
const {
auth = { authType: "inherit", authActive: true },
headers = [],
variables = [],
} = parsedData;
const transformedAuth = transformAuth(auth);
@ -186,6 +192,10 @@ export const transformWorkspaceCollections = (
header.description ? header : { ...header, description: "" }
);
const filteredCollectionVariables = variables.filter(
(variable) => variable.key.trim() !== ""
);
// The response doesn't include a way to infer the schema version, so it's set to the latest version
// Any relevant migrations have to be accounted here
// `ref_id` field isn't necessary being applicable only to personal workspace and asociates with syncing
@ -197,6 +207,7 @@ export const transformWorkspaceCollections = (
requests: transformWorkspaceRequests(requests),
auth: transformedAuth,
headers: transformedHeaders,
variables: filteredCollectionVariables,
};
});
};

View file

@ -503,6 +503,11 @@ details[open] summary .indicator {
@apply hover:bg-amber-600;
}
&.collection-variable-highlight {
@apply bg-purple-500;
@apply hover:bg-purple-600;
}
&.environment-variable-highlight {
@apply bg-green-500;
@apply hover:bg-green-600;

View file

@ -1,6 +1,7 @@
// Base shared styles for all tippy themes
@mixin base-tippy-styles {
.cm-tooltip {
@apply z-[1000] #{!important};
.tippy-box {
@apply shadow-none #{!important};
@apply fixed;

View file

@ -416,6 +416,7 @@
"empty_schema": "No schema found",
"endpoint": "Endpoint cannot be empty",
"environments": "Environments are empty",
"collection_variables": "Collection variables are empty",
"folder": "Folder is empty",
"headers": "This request does not have any headers",
"history": "History is empty",

View file

@ -72,6 +72,7 @@ declare module 'vue' {
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CollectionsVariables: typeof import('./components/collections/Variables.vue')['default']
ConsoleItem: typeof import('./components/console/Item.vue')['default']
ConsolePanel: typeof import('./components/console/Panel.vue')['default']
ConsoleValue: typeof import('./components/console/Value.vue')['default']

View file

@ -58,8 +58,9 @@
</span>
</span>
</div>
<div v-if="!hasNoTeamAccess" class="flex">
<div class="flex">
<HoppButtonSecondary
v-if="!hasNoTeamAccess"
v-tippy="{ theme: 'tooltip' }"
:icon="IconFilePlus"
:title="t('request.add')"
@ -67,6 +68,7 @@
@click="emit('add-request')"
/>
<HoppButtonSecondary
v-if="!hasNoTeamAccess"
v-tippy="{ theme: 'tooltip' }"
:icon="IconFolderPlus"
:title="t('folder.new')"
@ -109,6 +111,7 @@
@keyup.escape="hide()"
>
<HoppSmartItem
v-if="!hasNoTeamAccess"
ref="requestAction"
:icon="IconFilePlus"
:label="t('request.new')"
@ -121,6 +124,7 @@
"
/>
<HoppSmartItem
v-if="!hasNoTeamAccess"
ref="folderAction"
:icon="IconFolderPlus"
:label="t('folder.new')"
@ -145,6 +149,7 @@
"
/>
<HoppSmartItem
v-if="!hasNoTeamAccess"
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
@ -157,6 +162,7 @@
"
/>
<HoppSmartItem
v-if="!hasNoTeamAccess"
ref="duplicateAction"
:icon="IconCopy"
:label="t('action.duplicate')"
@ -170,6 +176,7 @@
"
/>
<HoppSmartItem
v-if="!hasNoTeamAccess"
ref="exportAction"
:icon="IconDownload"
:label="t('export.title')"
@ -182,18 +189,6 @@
}
"
/>
<HoppSmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
emit('remove-collection')
hide()
}
"
/>
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
@ -206,6 +201,19 @@
}
"
/>
<HoppSmartItem
v-if="!hasNoTeamAccess"
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
emit('remove-collection')
hide()
}
"
/>
</div>
</template>
</tippy>

View file

@ -127,6 +127,7 @@ function translateToTeamCollectionFormat(x: HoppCollection) {
const data = {
auth: x.auth,
headers: x.headers,
variables: x.variables,
}
const obj = {
@ -419,7 +420,7 @@ const HoppInsomniaImporter: ImporterOrExporter = {
importSummary: currentImportSummary,
component: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json",
acceptedFileTypes: ".json, .yaml, .yml, .har",
description: "import.from_insomnia_import_summary",
onImportFromFile: async (content) => {
isInsomniaImporterInProgress.value = true

View file

@ -4,7 +4,7 @@
dialog
:title="t('collection.properties')"
:full-width-body="true"
styles="sm:max-w-2xl"
styles="sm:max-w-3xl"
@close="hideModal"
>
<template #body>
@ -13,7 +13,11 @@
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
render-inactive-tabs
>
<HoppSmartTab id="headers" :label="`${t('tab.headers')}`">
<HoppSmartTab
v-if="hasTeamWriteAccess"
id="headers"
:label="`${t('tab.headers')}`"
>
<HttpHeaders
v-model="editableCollection"
:is-collection-property="true"
@ -27,7 +31,11 @@
</div>
</HoppSmartTab>
<HoppSmartTab id="authorization" :label="`${t('tab.authorization')}`">
<HoppSmartTab
v-if="hasTeamWriteAccess"
id="authorization"
:label="`${t('tab.authorization')}`"
>
<HttpAuthorization
v-model="editableCollection.auth"
:is-collection-property="true"
@ -43,6 +51,19 @@
</div>
</HoppSmartTab>
<!-- Collection variables is only available for REST collections for now -->
<HoppSmartTab
v-if="source === 'REST'"
id="variables"
:label="`${t('tab.variables')}`"
>
<CollectionsVariables
v-model="editableCollection.variables"
:inherited-properties="editingProperties.inheritedProperties"
:has-team-write-access="hasTeamWriteAccess"
/>
</HoppSmartTab>
<HoppSmartTab
v-if="showDetails"
:id="'details'"
@ -117,23 +138,25 @@
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import {
GQLHeader,
HoppCollection,
HoppGQLAuth,
HoppRESTAuth,
HoppRESTHeaders,
} from "@hoppscotch/data"
import { refAutoReset, useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { clone } from "lodash-es"
import { computed, ref, watch } from "vue"
import { refAutoReset, useVModel } from "@vueuse/core"
import { clone } from "lodash-es"
import { useI18n } from "@composables/i18n"
import { useToast } from "~/composables/toast"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useService } from "dioc/vue"
import {
HoppCollection,
HoppCollectionVariable,
HoppRESTAuth,
HoppGQLAuth,
HoppRESTHeaders,
GQLHeader,
} from "@hoppscotch/data"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { PersistenceService } from "~/services/persistence"
import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy"
import IconHelpCircle from "~icons/lucide/help-circle"
@ -141,6 +164,7 @@ import { RESTOptionTabs } from "../http/RequestOptions.vue"
const persistenceService = useService(PersistenceService)
const t = useI18n()
const toast = useToast()
export type EditingProperties = {
collection: Partial<HoppCollection> | null
@ -148,12 +172,9 @@ export type EditingProperties = {
path: string
inheritedProperties?: HoppInheritedProperty
}
type HoppCollectionAuth = HoppRESTAuth | HoppGQLAuth
type HoppCollectionHeaders = HoppRESTHeaders | GQLHeader[]
const toast = useToast()
const props = withDefaults(
defineProps<{
show: boolean
@ -161,12 +182,14 @@ const props = withDefaults(
editingProperties: EditingProperties
source: "REST" | "GraphQL"
modelValue: string
showDetails: boolean
showDetails?: boolean
hasTeamWriteAccess?: boolean
}>(),
{
show: false,
loadingState: false,
showDetails: false,
hasTeamWriteAccess: true,
}
)
@ -182,104 +205,107 @@ const emit = defineEmits<{
const editableCollection = ref<{
headers: HoppCollectionHeaders
auth: HoppCollectionAuth
variables: HoppCollectionVariable[]
}>({
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
auth: { authType: "inherit", authActive: false },
variables: [],
})
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const activeTab = useVModel(props, "modelValue", emit)
const activeTabIsDetails = computed(() => activeTab.value === "details")
watch(
editableCollection,
async (updatedEditableCollection) => {
if (props.show && props.editingProperties) {
const unsavedCollectionProperties: EditingProperties = {
collection: updatedEditableCollection,
isRootCollection: props.editingProperties.isRootCollection ?? false,
path: props.editingProperties.path,
inheritedProperties: props.editingProperties.inheritedProperties,
}
await persistenceService.setLocalConfig(
"unsaved_collection_properties",
JSON.stringify(unsavedCollectionProperties)
)
}
},
{
deep: true,
const persistUnsavedChanges = async (
updated: typeof editableCollection.value
) => {
if (!props.show) return
await persistenceService.setLocalConfig(
"unsaved_collection_properties",
JSON.stringify({
collection: updated,
isRootCollection: props.editingProperties.isRootCollection ?? false,
path: props.editingProperties.path,
inheritedProperties: props.editingProperties.inheritedProperties,
})
)
}
const handleModalVisibility = async (show: boolean) => {
enforceTabAccessRules()
if (show && props.editingProperties.collection) {
loadEditableCollection()
} else {
resetEditableCollection()
await persistenceService.removeLocalConfig("unsaved_collection_properties")
}
)
}
const activeTab = useVModel(props, "modelValue", emit)
const enforceTabAccessRules = () => {
// `Details` tab doesn't exist for personal workspace, hence switching to the `Headers` tab
// The modal can appear empty while switching from a team workspace with `Details` as the active tab
if (activeTab.value === "details" && !props.showDetails)
activeTab.value = "headers"
// If the user doesn't have write access to the team, switch to `Variables` tab
// when the `Headers` or `Authorization` tab is active
if (
!props.hasTeamWriteAccess &&
["headers", "authorization"].includes(activeTab.value)
)
activeTab.value = "variables"
}
watch(
() => props.show,
async (show) => {
// `Details` tab doesn't exist for personal workspace, hence switching to the `Headers` tab
// The modal can appear empty while switching from a team workspace with `Details` as the active tab
if (activeTab.value === "details" && !props.showDetails) {
activeTab.value = "headers"
}
if (show && props.editingProperties.collection) {
editableCollection.value.auth = clone(
props.editingProperties.collection.auth as HoppCollectionAuth
)
editableCollection.value.headers = clone(
props.editingProperties.collection.headers as HoppCollectionHeaders
)
} else {
editableCollection.value = {
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
}
await persistenceService.removeLocalConfig(
"unsaved_collection_properties"
)
}
const loadEditableCollection = () => {
editableCollection.value = {
auth: clone(props.editingProperties.collection!.auth as HoppCollectionAuth),
headers: clone(
props.editingProperties.collection!.headers as HoppCollectionHeaders
),
variables: clone(props.editingProperties.collection!.variables || []),
}
)
}
const resetEditableCollection = () => {
editableCollection.value = {
headers: [],
auth: { authType: "inherit", authActive: false },
variables: [],
}
}
const saveEditedCollection = async () => {
if (!props.editingProperties) return
const finalCollection = clone(editableCollection.value)
const collection = {
emit("set-collection-properties", {
path: props.editingProperties.path,
collection: {
...props.editingProperties.collection,
...finalCollection,
...clone(editableCollection.value),
},
isRootCollection: props.editingProperties.isRootCollection,
}
emit("set-collection-properties", collection as EditingProperties)
} as EditingProperties)
await persistenceService.removeLocalConfig("unsaved_collection_properties")
}
watch(editableCollection, persistUnsavedChanges, { deep: true })
watch(() => props.show, handleModalVisibility)
const hideModal = async () => {
await persistenceService.removeLocalConfig("unsaved_collection_properties")
emit("hide-modal")
}
const changeOptionTab = (e: RESTOptionTabs) => {
activeTab.value = e
const changeOptionTab = (tab: RESTOptionTabs) => {
activeTab.value = tab
}
const copyCollectionID = () => {
copyToClipboard(props.editingProperties.path)
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
toast.success(t("state.copied_to_clipboard"))
}
</script>

View file

@ -128,18 +128,6 @@
}
"
/>
<HoppSmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
emit('remove-request')
hide()
}
"
/>
<HoppSmartItem
ref="shareAction"
:icon="IconShare2"
@ -152,6 +140,18 @@
}
"
/>
<HoppSmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
emit('remove-request')
hide()
}
"
/>
</div>
</template>
</tippy>

View file

@ -141,7 +141,7 @@ import {
} from "~/helpers/backend/mutations/TeamRequest"
import { Picked } from "~/helpers/types/HoppPicked"
import {
cascadeParentCollectionForHeaderAuth,
cascadeParentCollectionForProperties,
editGraphqlRequest,
editRESTRequest,
saveGraphqlRequestAs,
@ -357,15 +357,11 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
RESTTabs.currentActiveTab.value.document.inheritedProperties =
cascadeParentCollectionForProperties(
`${picked.value.collectionIndex}`,
"rest"
)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
@ -395,15 +391,8 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
RESTTabs.currentActiveTab.value.document.inheritedProperties =
cascadeParentCollectionForProperties(picked.value.folderPath, "rest")
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
@ -434,15 +423,8 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
RESTTabs.currentActiveTab.value.document.inheritedProperties =
cascadeParentCollectionForProperties(picked.value.folderPath, "rest")
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
@ -538,15 +520,8 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
GQLTabs.currentActiveTab.value.document.inheritedProperties =
cascadeParentCollectionForProperties(picked.value.folderPath, "graphql")
requestSaved("GQL")
} else if (picked.value.pickedType === "gql-my-folder") {
@ -573,15 +548,8 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
GQLTabs.currentActiveTab.value.document.inheritedProperties =
cascadeParentCollectionForProperties(picked.value.folderPath, "graphql")
requestSaved("GQL")
} else if (picked.value.pickedType === "gql-my-collection") {
@ -608,15 +576,11 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
GQLTabs.currentActiveTab.value.document.inheritedProperties =
cascadeParentCollectionForProperties(
`${picked.value.collectionIndex}`,
"graphql"
)
requestSaved("GQL")
}

View file

@ -0,0 +1,316 @@
<template>
<div class="flex flex-col flex-1">
<div class="flex flex-1 justify-between items-center pl-4">
<span class="truncate font-semibold text-secondaryLight">{{
t("environment.variable_list")
}}</span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/environments"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
</div>
<div class="flex flex-col border border-divider rounded">
<HoppSmartTabs v-model="selectedEnvOption" render-inactive-tabs>
<template #actions>
<div class="flex flex-1 items-center justify-between">
<HoppButtonSecondary
v-if="hasTeamWriteAccess"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="clearIcon"
@click="clearContent()"
/>
<HoppButtonSecondary
v-if="hasTeamWriteAccess"
v-tippy="{ theme: 'tooltip' }"
:icon="IconPlus"
:title="t('add.new')"
@click="addEnvironmentVariable"
/>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.escape="hide()"
>
<HoppSmartItem
v-if="hasTeamWriteAccess"
v-tippy="{ theme: 'tooltip' }"
:icon="IconCopyLeft"
:label="t('environment.replace_all_initial_with_current')"
@click="
() => {
vars.forEach((v) => {
v.initialValue = v.currentValue
})
hide()
}
"
/>
<HoppSmartItem
v-tippy="{ theme: 'tooltip' }"
:icon="IconCopyRight"
:label="t('environment.replace_all_current_with_initial')"
@click="
() => {
vars.forEach((v) => {
v.currentValue = v.initialValue
})
hide()
}
"
/>
</div>
</template>
</tippy>
</div>
</template>
<HoppSmartTab
v-for="tab in tabsData"
:id="tab.id"
:key="tab.id"
:label="tab.label"
>
<div class="divide-y divide-dividerLight">
<HoppSmartPlaceholder
v-if="tab.variables.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="tab.emptyStateLabel"
:text="tab.emptyStateLabel"
>
<template #body>
<HoppButtonSecondary
v-if="hasTeamWriteAccess"
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addEnvironmentVariable"
/>
</template>
</HoppSmartPlaceholder>
<template v-else>
<div
v-for="(env, index) in tab.variables"
:key="`${tab.id}-${index}`"
class="flex divide-x divide-dividerLight"
>
<input
v-model="env.key"
v-focus
class="flex flex-1 bg-transparent px-4 py-2 text-secondaryDark"
:placeholder="`${t('count.variable', {
count: index + 1,
})}`"
:name="'variable' + index"
:class="{
'opacity-25': !hasTeamWriteAccess,
}"
:disabled="!hasTeamWriteAccess"
/>
<div class="flex items-center flex-1">
<SmartEnvInput
v-model="env.initialValue"
:placeholder="`${t('count.initialValue', { count: index + 1 })}`"
:envs="liveEnvs"
:auto-complete-env="true"
:name="'initialValue' + index"
:secret="tab.isSecret"
:readonly="!hasTeamWriteAccess"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('environment.replace_initial_with_current')"
:icon="IconCopyLeft"
:disabled="!hasTeamWriteAccess"
@click="
() => {
env.initialValue = env.currentValue
}
"
/>
</div>
<div class="flex items-center flex-1">
<SmartEnvInput
v-model="env.currentValue"
:placeholder="`${t('count.currentValue', { count: index + 1 })}`"
:envs="liveEnvs"
:auto-complete-env="true"
:name="'currentValue' + index"
:secret="tab.isSecret"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('environment.replace_current_with_initial')"
:icon="IconCopyRight"
@click="
() => {
env.currentValue = env.initialValue
}
"
/>
</div>
<div v-if="hasTeamWriteAccess" class="flex">
<HoppButtonSecondary
id="variable"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="removeEnvironmentVariable(index)"
/>
</div>
</div>
</template>
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div>
</div>
</template>
<script lang="ts" setup>
import IconDone from "~icons/lucide/check"
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 IconCopyRight from "~icons/lucide/clipboard-paste"
import IconCopyLeft from "~icons/lucide/clipboard-copy"
import IconMoreVertical from "~icons/lucide/more-vertical"
import { computed, ComputedRef, Ref, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { useColorMode } from "~/composables/theming"
import { HoppCollectionVariable } from "@hoppscotch/data"
import { refAutoReset, useVModel } from "@vueuse/core"
import * as A from "fp-ts/Array"
import { pipe } from "fp-ts/function"
import { useReadonlyStream } from "~/composables/stream"
import {
AggregateEnvironment,
aggregateEnvsWithCurrentValue$,
} from "~/newstore/environments"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { transformInheritedCollectionVariablesToAggregateEnv } from "~/helpers/utils/inheritedCollectionVarTransformer"
const t = useI18n()
const toast = useToast()
const colorMode = useColorMode()
const props = defineProps<{
modelValue: HoppCollectionVariable[]
inheritedProperties?: HoppInheritedProperty
hasTeamWriteAccess: boolean
}>()
type SelectedEnv = "variables" | "secret"
const selectedEnvOption = ref<SelectedEnv>("variables")
const tippyActions = ref<HTMLDivElement | null>(null)
const vars = useVModel(props, "modelValue")
const secretVars = computed(() =>
pipe(
vars.value,
A.filter((e) => e.secret)
)
)
const nonSecretVars = computed(() =>
pipe(
vars.value,
A.filter((e) => !e.secret)
)
)
const tabsData: ComputedRef<
{
id: string
label: string
emptyStateLabel: string
isSecret: boolean
variables: HoppCollectionVariable[]
}[]
> = computed(() => {
return [
{
id: "variables",
label: t("environment.variables"),
emptyStateLabel: t("empty.collection_variables"),
isSecret: false,
variables: nonSecretVars.value,
},
{
id: "secret",
label: t("environment.secrets"),
emptyStateLabel: t("empty.secret_environments"),
isSecret: true,
variables: secretVars.value,
},
]
})
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
IconTrash2,
1000
)
const aggregateEnvs = useReadonlyStream(
aggregateEnvsWithCurrentValue$,
[]
) as Ref<AggregateEnvironment[]>
const liveEnvs = computed(() => {
const parentInheritedVariables =
transformInheritedCollectionVariablesToAggregateEnv(
props.inheritedProperties?.variables ?? []
)
return [...parentInheritedVariables, ...aggregateEnvs.value]
})
const addEnvironmentVariable = () => {
vars.value.push({
key: "",
currentValue: "",
initialValue: "",
secret: selectedEnvOption.value === "secret",
})
}
const clearContent = () => {
vars.value = vars.value.filter((e) =>
selectedEnvOption.value === "secret" ? !e.secret : e.secret
)
clearIcon.value = IconDone
toast.success(`${t("state.cleared")}`)
}
const removeEnvironmentVariable = (index: number) => {
vars.value.splice(index, 1)
}
</script>

View file

@ -339,7 +339,7 @@ const isSelected = computed(
const collectionIcon = computed(() => {
if (isSelected.value) return IconCheckCircle
else if (!showChildren.value && !props.isFiltered) return IconFolder
else if (!showChildren.value || props.isFiltered) return IconFolderOpen
else if (showChildren.value || props.isFiltered) return IconFolderOpen
return IconFolder
})

View file

@ -165,7 +165,7 @@ import {
graphqlCollections$,
addGraphqlFolder,
saveGraphqlRequestAs,
cascadeParentCollectionForHeaderAuth,
cascadeParentCollectionForProperties,
editGraphqlCollection,
editGraphqlFolder,
moveGraphqlRequest,
@ -402,11 +402,6 @@ const onAddRequest = ({ name, path }: { name: string; path: string }) => {
const insertionIndex = saveGraphqlRequestAs(path, newRequest)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
path,
"graphql"
)
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
@ -415,10 +410,7 @@ const onAddRequest = ({ name, path }: { name: string; path: string }) => {
},
request: newRequest,
isDirty: false,
inheritedProperties: {
auth,
headers,
},
inheritedProperties: cascadeParentCollectionForProperties(path, "graphql"),
})
platform.analytics?.logEvent({
@ -524,10 +516,6 @@ const selectRequest = ({
folderPath: folderPath,
requestIndex: requestIndex,
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath,
"graphql"
)
// Switch to that request if that request is open
if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id)
@ -541,10 +529,10 @@ const selectRequest = ({
},
request: cloneDeep(request),
isDirty: false,
inheritedProperties: {
auth,
headers,
},
inheritedProperties: cascadeParentCollectionForProperties(
folderPath,
"graphql"
),
})
}
@ -557,11 +545,6 @@ const dropRequest = ({
requestIndex: number
collectionIndex: number
}) => {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${collectionIndex}`,
"graphql"
)
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
@ -576,10 +559,8 @@ const dropRequest = ({
.length,
}
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
possibleTab.value.document.inheritedProperties =
cascadeParentCollectionForProperties(`${collectionIndex}`, "graphql")
}
moveGraphqlRequest(folderPath, requestIndex, `${collectionIndex}`)
@ -610,15 +591,10 @@ const editProperties = ({
let inheritedProperties = undefined
if (parentIndex) {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
inheritedProperties = cascadeParentCollectionForProperties(
parentIndex,
"graphql"
)
inheritedProperties = {
auth,
headers,
}
}
editingProperties.value = {
@ -647,18 +623,10 @@ const setCollectionProperties = (newCollection: {
editGraphqlFolder(path, collection)
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
path,
"graphql"
)
nextTick(() => {
updateInheritedPropertiesForAffectedRequests(
path,
{
auth,
headers,
},
cascadeParentCollectionForProperties(path, "graphql"),
"graphql"
)
})

View file

@ -198,7 +198,12 @@
v-model="collectionPropertiesModalActiveTab"
:show="showModalEditProperties"
:editing-properties="editingProperties"
:show-details="collectionsType.type === 'team-collections'"
:show-details="
collectionsType.type === 'team-collections' && hasTeamWriteAccess
"
:has-team-write-access="
collectionsType.type === 'team-collections' ? hasTeamWriteAccess : true
"
source="REST"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
@ -225,8 +230,13 @@ import {
makeCollection,
} from "@hoppscotch/data"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import { flow } from "fp-ts/function"
import { cloneDeep, debounce, isEqual } from "lodash-es"
import { PropType, computed, nextTick, onMounted, ref, watch } from "vue"
import { useReadonlyStream } from "~/composables/stream"
@ -272,7 +282,7 @@ import { Picked } from "~/helpers/types/HoppPicked"
import {
addRESTCollection,
addRESTFolder,
cascadeParentCollectionForHeaderAuth,
cascadeParentCollectionForProperties,
duplicateRESTCollection,
editRESTCollection,
editRESTFolder,
@ -301,6 +311,9 @@ import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { Collection as NodeCollection } from "./MyCollections.vue"
import { EditingProperties } from "./Properties.vue"
import { CollectionRunnerData } from "../http/test/RunnerModal.vue"
import { HoppCollectionVariable } from "@hoppscotch/data"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { CurrentValueService } from "~/services/current-environment-value.service"
const t = useI18n()
const toast = useToast()
@ -375,6 +388,10 @@ const draggingToRoot = ref(false)
const collectionMoveLoading = ref<string[]>([])
const requestMoveLoading = ref<string[]>([])
//collection variables current value and secret value
const secretEnvironmentService = useService(SecretEnvironmentService)
const currentEnvironmentValueService = useService(CurrentValueService)
// TeamList-Adapter
const workspaceService = useService(WorkspaceService)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
@ -393,7 +410,7 @@ const teamLoadingCollections = useReadonlyStream(
const teamEnvironmentAdapter = new TeamEnvironmentAdapter(undefined)
const {
cascadeParentCollectionForHeaderAuthForSearchResults,
cascadeParentCollectionForPropertiesForSearchResults,
searchTeams,
teamsSearchResults,
teamsSearchResultsLoading,
@ -774,6 +791,7 @@ const addNewRootCollection = (name: string) => {
authType: "none",
authActive: true,
},
variables: [],
})
)
@ -834,8 +852,6 @@ const onAddRequest = (requestName: string) => {
if (collectionsType.value.type === "my-collections") {
const insertionIndex = saveRESTRequestAs(path, newRequest)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest")
tabs.createNewTab({
type: "request",
request: newRequest,
@ -845,10 +861,7 @@ const onAddRequest = (requestName: string) => {
folderPath: path,
requestIndex: insertionIndex,
},
inheritedProperties: {
auth,
headers,
},
inheritedProperties: cascadeParentCollectionForProperties(path, "rest"),
})
platform.analytics?.logEvent({
@ -889,8 +902,7 @@ const onAddRequest = (requestName: string) => {
},
(result) => {
const { createRequestInCollection } = result
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path)
tabs.createNewTab({
type: "request",
request: newRequest,
@ -901,10 +913,8 @@ const onAddRequest = (requestName: string) => {
collectionID: path,
teamID: createRequestInCollection.collection.team.id,
},
inheritedProperties: {
auth,
headers,
},
inheritedProperties:
teamCollectionAdapter.cascadeParentCollectionForProperties(path),
})
modalLoadingState.value = false
@ -1569,6 +1579,17 @@ const onRemoveCollection = () => {
toast.success(t("state.deleted"))
displayConfirmModal(false)
// delete the secret collection variables
// and current collection variables value if the collection is removed
if (collectionToRemove) {
secretEnvironmentService.deleteSecretEnvironment(
collectionToRemove._ref_id ?? `${collectionIndex}`
)
currentEnvironmentValueService.deleteEnvironment(
collectionToRemove._ref_id ?? `${collectionIndex}`
)
}
} else if (hasTeamWriteAccess.value) {
const collectionID = editingCollectionID.value
@ -1584,6 +1605,13 @@ const onRemoveCollection = () => {
removeTeamCollectionOrFolder(collectionID).then(() => {
resetTeamRequestsContext()
// delete the secret collection variables
// and current collection variables value if the collection is removed
if (collectionID) {
secretEnvironmentService.deleteSecretEnvironment(collectionID)
currentEnvironmentValueService.deleteEnvironment(collectionID)
}
})
}
}
@ -1611,6 +1639,8 @@ const onRemoveFolder = () => {
emit("select", null)
}
const folderIndex = pathToLastIndex(folderPath)
const folderToRemove = folderPath
? navigateToFolderWithIndexPath(
restCollectionStore.value.state,
@ -1630,6 +1660,17 @@ const onRemoveFolder = () => {
toast.success(t("state.deleted"))
displayConfirmModal(false)
// delete the secret collection variables
// and current collection variables value if the collection is removed
if (folderToRemove) {
secretEnvironmentService.deleteSecretEnvironment(
folderToRemove.id ?? `${folderIndex}`
)
currentEnvironmentValueService.deleteEnvironment(
folderToRemove.id ?? `${folderIndex}`
)
}
} else if (hasTeamWriteAccess.value) {
const collectionID = editingCollectionID.value
@ -1645,6 +1686,13 @@ const onRemoveFolder = () => {
removeTeamCollectionOrFolder(collectionID).then(() => {
resetTeamRequestsContext()
// delete the secret collection variables
// and current collection variables value if the collection is removed
if (collectionID) {
secretEnvironmentService.deleteSecretEnvironment(collectionID)
currentEnvironmentValueService.deleteEnvironment(collectionID)
}
})
}
}
@ -1950,10 +1998,10 @@ const selectRequest = (selectedRequest: {
if (!collectionID) return
inheritedProperties =
cascadeParentCollectionForHeaderAuthForSearchResults(collectionID)
cascadeParentCollectionForPropertiesForSearchResults(collectionID)
} else {
inheritedProperties =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
teamCollectionAdapter.cascadeParentCollectionForProperties(folderPath)
}
const possibleTab = tabs.getTabRefWithSaveContext({
@ -1978,10 +2026,6 @@ const selectRequest = (selectedRequest: {
})
}
} else {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath,
"rest"
)
possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex: parseInt(requestIndex),
@ -2000,10 +2044,10 @@ const selectRequest = (selectedRequest: {
folderPath: folderPath!,
requestIndex: parseInt(requestIndex),
},
inheritedProperties: {
auth,
headers,
},
inheritedProperties: cascadeParentCollectionForProperties(
folderPath,
"rest"
),
})
}
}
@ -2045,6 +2089,10 @@ const selectResponse = (payload: {
requestIndex: parseInt(requestIndex),
exampleID: responseID,
},
inheritedProperties: cascadeParentCollectionForProperties(
folderPath,
"rest"
),
})
}
} else {
@ -2070,6 +2118,10 @@ const selectResponse = (payload: {
collectionID: folderPath,
exampleID: responseID,
},
inheritedProperties:
teamCollectionAdapter.cascadeParentCollectionForProperties(
folderPath
),
})
}
}
@ -2101,11 +2153,6 @@ const dropRequest = (payload: {
let possibleTab = null
if (collectionsType.value.type === "my-collections") {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
destinationCollectionIndex,
"rest"
)
possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
@ -2123,10 +2170,8 @@ const dropRequest = (payload: {
).length,
}
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
possibleTab.value.document.inheritedProperties =
cascadeParentCollectionForProperties(destinationCollectionIndex, "rest")
}
// When it's drop it's basically getting deleted from last folder. reordering last folder accordingly
@ -2164,10 +2209,6 @@ const dropRequest = (payload: {
requestMoveLoading.value.indexOf(requestIndex),
1
)
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(
destinationCollectionIndex
)
possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
@ -2179,10 +2220,10 @@ const dropRequest = (payload: {
originLocation: "team-collection",
requestID: requestIndex,
}
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
possibleTab.value.document.inheritedProperties =
teamCollectionAdapter.cascadeParentCollectionForProperties(
destinationCollectionIndex
)
}
toast.success(`${t("request.moved")}`)
}
@ -2303,16 +2344,11 @@ const dropCollection = (payload: {
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
const inheritedProperty = cascadeParentCollectionForProperties(
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
"rest"
)
const inheritedProperty = {
auth,
headers,
}
updateInheritedPropertiesForAffectedRequests(
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
inheritedProperty,
@ -2345,16 +2381,11 @@ const dropCollection = (payload: {
1
)
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(
const inheritedProperty =
teamCollectionAdapter.cascadeParentCollectionForProperties(
destinationCollectionIndex
)
const inheritedProperty = {
auth,
headers,
}
updateInheritedPropertiesForAffectedRequests(
`${destinationCollectionIndex}`,
inheritedProperty,
@ -2696,16 +2727,43 @@ const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
}
}
/**
* Used to get the current value of a variable
* It checks if the variable is a secret or not and returns the value accordingly.
* @param isSecret If the variable is a secret
* @param varIndex The index of the variable in the collection
* @param collectionID The ID of the collection
* @returns The current value of the variable, either from the secret environment or the current environment service
*/
const getCurrentValue = (
isSecret: boolean,
varIndex: number,
collectionID: string
) => {
if (isSecret) {
return secretEnvironmentService.getSecretEnvironmentVariable(
collectionID,
varIndex
)?.value
}
return currentEnvironmentValueService.getEnvironmentVariable(
collectionID,
varIndex
)?.currentValue
}
const editProperties = (payload: {
collectionIndex: string
collection: HoppCollection | TeamCollection
}) => {
const { collection, collectionIndex } = payload
const collectionId = collection.id ?? collectionIndex.split("/").pop()
if (collectionsType.value.type === "my-collections") {
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
let inheritedProperties = {
let inheritedProperties: HoppInheritedProperty = {
auth: {
parentID: "",
parentName: "",
@ -2714,34 +2772,42 @@ const editProperties = (payload: {
authActive: true,
},
},
headers: [
{
parentID: "",
parentName: "",
inheritedHeader: {},
},
],
} as HoppInheritedProperty
headers: [],
variables: [],
}
if (parentIndex) {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
inheritedProperties = cascadeParentCollectionForProperties(
parentIndex,
"rest"
)
inheritedProperties = {
auth,
headers,
}
}
const collectionVariables = pipe(
(collection as HoppCollection).variables ?? [],
A.mapWithIndex((index, e) => {
return {
...e,
currentValue:
getCurrentValue(
e.secret,
index,
(collection as HoppCollection)._ref_id ?? collectionId!
) ?? e.currentValue,
}
})
)
editingProperties.value = {
collection: collection as Partial<HoppCollection>,
collection: {
...collection,
variables: collectionVariables,
} as Partial<HoppCollection>,
isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex,
inheritedProperties,
}
} else if (hasTeamWriteAccess.value) {
} else {
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
const data = (collection as TeamCollection).data
@ -2757,25 +2823,39 @@ const editProperties = (payload: {
authActive: true,
} as HoppRESTAuth,
headers: [] as HoppRESTHeaders,
variables: [] as HoppCollectionVariable[],
folders: null,
requests: null,
}
if (parentIndex) {
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(parentIndex)
const { auth, headers, variables } =
teamCollectionAdapter.cascadeParentCollectionForProperties(parentIndex)
inheritedProperties = {
auth,
headers,
variables,
}
}
if (data) {
const collectionVariables = pipe(
(data.variables ?? []) as HoppCollectionVariable[],
A.mapWithIndex((index, e) => {
return {
...e,
currentValue:
getCurrentValue(e.secret, index, collectionId!) ?? e.currentValue,
}
})
)
coll = {
...coll,
auth: data.auth,
headers: data.headers as HoppRESTHeaders,
variables: collectionVariables as HoppCollectionVariable[],
}
}
@ -2803,6 +2883,65 @@ const setCollectionProperties = (newCollection: {
// Since path is being preserved, we extract the collectionId from path instead
const collectionId = collection.id ?? path.split("/").pop()
//setting current value and secret values to of collection variables
if (collection.variables) {
const filteredVariables = pipe(
collection.variables,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map((e) => e)
)
)
)
const secretVariables = pipe(
filteredVariables,
A.filterMapWithIndex((i, e) =>
e.secret
? O.some({
key: e.key,
value: e.currentValue,
varIndex: i,
})
: O.none
)
)
const nonSecretVariables = pipe(
filteredVariables,
A.filterMapWithIndex((i, e) =>
!e.secret
? O.some({
key: e.key,
currentValue: e.currentValue,
varIndex: i,
isSecret: e.secret ?? false,
})
: O.none
)
)
secretEnvironmentService.addSecretEnvironment(
collection._ref_id ?? collectionId!,
secretVariables
)
currentEnvironmentValueService.addEnvironment(
collection._ref_id ?? collectionId!,
nonSecretVariables
)
//set current value and secret values to empty string
collection.variables = pipe(
filteredVariables,
A.map((e) => ({
...e,
currentValue: "",
}))
)
}
if (collectionsType.value.type === "my-collections") {
if (isRootCollection) {
editRESTCollection(parseInt(path), collection)
@ -2810,16 +2949,14 @@ const setCollectionProperties = (newCollection: {
editRESTFolder(path, collection)
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest")
const inheritedProperty = cascadeParentCollectionForProperties(path, "rest")
nextTick(() => {
updateInheritedPropertiesForAffectedRequests(
path,
{
auth,
headers,
},
"rest"
inheritedProperty,
"rest",
collection._ref_id ?? collectionId!
)
})
toast.success(t("collection.properties_updated"))
@ -2827,6 +2964,7 @@ const setCollectionProperties = (newCollection: {
const data = {
auth: collection.auth,
headers: collection.headers,
variables: collection.variables,
}
pipe(
updateTeamCollection(collectionId, JSON.stringify(data), undefined),
@ -2843,15 +2981,13 @@ const setCollectionProperties = (newCollection: {
//This is a hack to update the inherited properties of the requests if there an tab opened
// since it takes a little bit of time to update the collection tree
setTimeout(() => {
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path)
const inheritedProperty =
teamCollectionAdapter.cascadeParentCollectionForProperties(path)
updateInheritedPropertiesForAffectedRequests(
path,
{
auth,
headers,
},
"rest"
inheritedProperty,
"rest",
collectionId
)
}, 200)
}
@ -2866,7 +3002,7 @@ const runCollectionHandler = (
) => {
if (payload.path && collectionsType.value.type === "team-collections") {
const inheritedProperties =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(payload.path)
teamCollectionAdapter.cascadeParentCollectionForProperties(payload.path)
if (inheritedProperties) {
collectionRunnerData.value = {

View file

@ -29,7 +29,7 @@ import {
import { GQLError } from "~/helpers/backend/GQLClient"
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { insomniaEnvImporter } from "~/helpers/import-export/import/insomniaEnv"
import { insomniaEnvImporter } from "~/helpers/import-export/import/insomnia/insomniaEnv"
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"

View file

@ -134,7 +134,7 @@
<input
v-model="env.key"
v-focus
class="flex flex-1 bg-transparent px-4 py-2"
class="flex flex-1 bg-transparent px-4 py-2 text-secondaryDark"
:placeholder="`${t('count.variable', {
count: index + 1,
})}`"
@ -150,6 +150,7 @@
:select-text-on-mount="
env.key ? env.key === editingVariableName : false
"
:auto-complete-env="true"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@ -173,6 +174,7 @@
:select-text-on-mount="
env.key ? env.key === editingVariableName : false
"
:auto-complete-env="true"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"

View file

@ -138,7 +138,7 @@
<input
v-model="env.key"
v-focus
class="flex flex-1 bg-transparent px-4 py-2"
class="flex flex-1 bg-transparent px-4 py-2 text-secondaryDark"
:placeholder="`${t('count.variable', {
count: index + 1,
})}`"

View file

@ -143,7 +143,7 @@
</draggable>
<draggable
v-model="inheritedProperties"
v-model="inheritedProperty"
item-key="id"
animation="250"
handle=".draggable-handle"
@ -533,9 +533,7 @@ const getComputedAuthHeaders = async (
if (req && req.headers.find((h) => h.key.toLowerCase() === "authorization"))
return []
if (!request) return []
if (!request.auth || !request.auth.authActive) return []
if (!request || !request.auth || !request.auth.authActive) return []
const headers: GQLHeader[] = []
@ -627,75 +625,61 @@ const computedHeaders = computedAsync(async () =>
}))
)
const inheritedProperties = computedAsync(async () => {
if (!props.inheritedProperties?.auth || !props.inheritedProperties.headers)
return []
//filter out headers that are already in the request headers
const inheritedHeaders = props.inheritedProperties.headers.filter(
(header) =>
!request.value.headers.some(
(requestHeader) => requestHeader.key === header.inheritedHeader?.key
)
)
const headers = inheritedHeaders
.filter(
(header) =>
header.inheritedHeader !== null &&
header.inheritedHeader !== undefined &&
header.inheritedHeader.active
)
.map((header, index) => {
const { key, value, active, description } = header.inheritedHeader
return {
inheritedFrom: props.inheritedProperties?.headers[index].parentName,
source: "headers",
id: `header-${index}`,
header: {
key,
value,
active,
description,
},
}
})
let auth = [] as {
const inheritedProperty = ref<
{
inheritedFrom: string
source: "auth"
source: "auth" | "headers"
id: string
header: {
key: string
value: string
active: boolean
}
header: GQLHeader
}[]
>([])
const [computedAuthHeader] = await getComputedAuthHeaders(
request.value,
props.inheritedProperties.auth.inheritedAuth as HoppGQLAuth
)
watch(
() => [props.inheritedProperties, request.value],
async () => {
if (!props.inheritedProperties) return
if (
computedAuthHeader &&
request.value.auth.authType === "inherit" &&
request.value.auth.authActive
) {
auth = [
{
inheritedFrom: props.inheritedProperties?.auth.parentName,
source: "auth",
id: `header-auth`,
header: computedAuthHeader,
},
]
}
//filter out headers that are already in the request headers
const inheritedHeaders = props.inheritedProperties.headers.filter(
(header) =>
!request.value.headers.some(
(requestHeader) =>
requestHeader.key === header.inheritedHeader?.key &&
requestHeader.active
)
)
inheritedProperty.value = inheritedHeaders.map((header, index) => ({
inheritedFrom: props.inheritedProperties!.headers[index].parentName!,
source: "headers",
id: `header-${index}`,
header: header.inheritedHeader,
}))
return [...headers, ...auth]
})
if (
props.inheritedProperties.auth &&
request.value.auth.authType === "inherit" &&
request.value.auth.authActive &&
!request.value.headers.some(
(requestHeader) =>
requestHeader.key === "Authorization" && requestHeader.active
)
) {
const [computedAuthHeader] = await getComputedAuthHeaders(
request.value,
props.inheritedProperties.auth.inheritedAuth as HoppGQLAuth
)
if (computedAuthHeader) {
inheritedProperty.value.push({
inheritedFrom: props.inheritedProperties.auth.parentName,
source: "auth",
id: `header-auth`,
header: computedAuthHeader,
})
}
}
},
{ immediate: true, deep: true }
)
const masking = ref(true)

View file

@ -156,7 +156,7 @@
</draggable>
<draggable
v-model="inheritedProperties"
v-model="inheritedProperty"
item-key="id"
animation="250"
handle=".draggable-handle"
@ -262,7 +262,7 @@ import { cloneDeep, isEqual } from "lodash-es"
import { reactive, Ref, ref, toRef, watch } from "vue"
import draggable from "vuedraggable-es"
import { computedAsync, useVModel } from "@vueuse/core"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { useNestedSetting } from "~/composables/settings"
import linter from "~/helpers/editor/linting/rawKeyValue"
@ -558,6 +558,15 @@ const computedHeaders: Ref<
}[]
> = ref([])
const inheritedProperty = ref<
{
inheritedFrom: string
source: "auth" | "headers"
id: string
header: HoppRESTHeader
}[]
>([])
const currentSelectedEnvironment = getCurrentEnvironment()
watch([props.modelValue, aggregateEnvs], async () => {
@ -583,77 +592,54 @@ watch([props.modelValue, aggregateEnvs], async () => {
}))
})
const inheritedProperties = computedAsync(async () => {
if (!props.inheritedProperties?.auth || !props.inheritedProperties.headers)
return []
watch(
() => [props.inheritedProperties, request.value],
async () => {
if (!props.inheritedProperties) return
//filter out headers that are already in the request headers
const inheritedHeaders = props.inheritedProperties.headers.filter(
(header) =>
!request.value.headers.some(
(requestHeader) => requestHeader.key === header.inheritedHeader?.key
)
)
const headers = inheritedHeaders
.filter(
//filter out headers that are already in the request headers
const inheritedHeaders = props.inheritedProperties.headers.filter(
(header) =>
header.inheritedHeader !== null &&
header.inheritedHeader !== undefined &&
header.inheritedHeader.active
!request.value.headers.some(
(requestHeader) =>
requestHeader.key === header.inheritedHeader?.key &&
requestHeader.active
)
)
.map((header, index) => {
const { key, value, active, description } = header.inheritedHeader
inheritedProperty.value = inheritedHeaders.map((header, index) => ({
inheritedFrom: props.inheritedProperties!.headers[index].parentName!,
source: "headers",
id: `header-${index}`,
header: header.inheritedHeader,
}))
return {
inheritedFrom: props.inheritedProperties?.headers[index].parentName,
source: "headers",
id: `header-${index}`,
header: {
key,
value,
active,
description,
},
if (
props.inheritedProperties.auth &&
request.value.auth.authType === "inherit" &&
request.value.auth.authActive &&
!request.value.headers.some(
(requestHeader) =>
requestHeader.key === "Authorization" && requestHeader.active
)
) {
const [computedAuthHeader] = await getComputedAuthHeaders(
aggregateEnvs.value,
request.value,
props.inheritedProperties.auth.inheritedAuth,
false
)
if (computedAuthHeader) {
inheritedProperty.value.push({
inheritedFrom: props.inheritedProperties.auth.parentName,
source: "auth",
id: `header-auth`,
header: computedAuthHeader,
})
}
})
let auth = [] as {
inheritedFrom: string
source: "auth"
id: string
header: {
key: string
value: string
active: boolean
}
}[]
const [computedAuthHeader] = await getComputedAuthHeaders(
aggregateEnvs.value,
request.value,
props.inheritedProperties.auth.inheritedAuth,
false
)
if (
computedAuthHeader &&
request.value.auth.authType === "inherit" &&
request.value.auth.authActive
) {
auth = [
{
inheritedFrom: props.inheritedProperties?.auth.parentName,
source: "auth",
id: `header-auth`,
header: computedAuthHeader,
},
]
}
return [...headers, ...auth]
})
},
{ immediate: true, deep: true }
)
const masking = ref(true)

View file

@ -62,7 +62,7 @@
v-if="showDescription"
:value="description"
:placeholder="t('count.description')"
class="flex flex-1 px-4 bg-transparent"
class="flex flex-1 px-4 bg-transparent text-secondaryDark"
type="text"
:class="{ 'opacity-50': !entityActive }"
@update:value="emit('update:description', $event.target.value)"

View file

@ -163,6 +163,7 @@ const tryExampleResponse = () => {
params,
requestVariables,
},
inheritedProperties: tab.value.document.inheritedProperties,
})
}

View file

@ -143,6 +143,7 @@ import {
} from "~/helpers/runner/adapter"
import { getErrorMessage } from "~/helpers/runner/collection-tree"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import { transformInheritedCollectionVariablesToAggregateEnv } from "~/helpers/utils/inheritedCollectionVarTransformer"
import {
getRESTCollectionByRefId,
getRESTCollectionInheritedProps,
@ -269,21 +270,28 @@ const runTests = async () => {
}
)
const parentVariables = transformInheritedCollectionVariablesToAggregateEnv(
tab.value.document.inheritedProperties?.variables ?? []
)
resolvedCollection = {
...collection.value,
auth: requestAuth,
headers: requestHeaders as HoppRESTHeader[],
variables: parentVariables,
}
} else {
const { auth, headers } = collectionInheritedProps ?? {
const { auth, headers, variables } = collectionInheritedProps ?? {
auth: { authActive: true, authType: "none" },
headers: [],
variables: [],
}
resolvedCollection = {
...collection.value,
auth,
headers,
variables,
}
}

View file

@ -101,6 +101,7 @@ import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { syntaxTree } from "@codemirror/language"
import { uniqueID } from "~/helpers/utils/uniqueID"
import { transformInheritedCollectionVariablesToAggregateEnv } from "~/helpers/utils/inheritedCollectionVarTransformer"
const t = useI18n()
@ -120,6 +121,7 @@ const props = withDefaults(
contextMenuEnabled?: boolean
secret?: boolean
autoCompleteEnv?: boolean
autoCompleteEnvSource?: AggregateEnvironment[] | null
}>(),
{
modelValue: "",
@ -135,7 +137,8 @@ const props = withDefaults(
inspectionResults: undefined,
contextMenuEnabled: true,
secret: false,
autoCompleteEnvSource: false,
autoCompleteEnv: false,
autoCompleteEnvSource: null,
}
)
@ -405,33 +408,21 @@ const envVars = computed(() => {
})
}
const requestVariables =
const collectionVariables =
tabs.currentActiveTab.value.document.type === "request" ||
tabs.currentActiveTab.value.document.type === "example-response"
? tabs.currentActiveTab.value.document.response.originalRequest
.requestVariables
: tabs.currentActiveTab.value.document.type === "request"
? tabs.currentActiveTab.value.document.request.requestVariables
: []
? transformInheritedCollectionVariablesToAggregateEnv(
tabs.currentActiveTab.value.document.inheritedProperties?.variables ??
[],
false
)
: []
// Transform request variables to match the env format
return [
...requestVariables.map(({ active, key, value }) =>
active
? {
key,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
}
: ({} as AggregateEnvironment)
),
...aggregateEnvs.value,
]
return [...collectionVariables, ...aggregateEnvs.value]
})
function envAutoCompletion(context: CompletionContext) {
const options = (envVars.value ?? [])
const options = (props.autoCompleteEnvSource ?? envVars.value ?? [])
.map((env) => ({
label: env?.key ? `<<${env.key}>>` : "",
info: env?.currentValue ?? "",
@ -559,6 +550,7 @@ const getExtensions = (readonly: boolean): Extension => {
? autocompletion({
activateOnTyping: true,
override: [envAutoCompletion],
tooltipClass: () => "tooltip-autocomplete",
})
: [],
@ -694,6 +686,12 @@ watch(
)
</script>
<style lang="scss">
.tooltip-autocomplete {
@apply z-[1001] #{!important};
}
</style>
<style lang="scss" scoped>
.autocomplete-wrapper {
@apply relative;

View file

@ -1,5 +1,6 @@
import {
Environment,
HoppCollectionVariable,
HoppRESTHeaders,
HoppRESTRequest,
HoppRESTRequestVariable,
@ -58,6 +59,7 @@ import {
OutgoingSandboxPostRequestWorkerMessage,
OutgoingSandboxPreRequestWorkerMessage,
} from "./workers/sandbox.worker"
import { transformInheritedCollectionVariablesToAggregateEnv } from "./utils/inheritedCollectionVarTransformer"
const sandboxWorker = new Worker(
new URL("./workers/sandbox.worker.ts", import.meta.url),
@ -117,8 +119,10 @@ export const combineEnvVariables = (variables: {
temp?: Environment["variables"]
}
requestVariables: Environment["variables"]
collectionVariables: Environment["variables"]
}) => [
...variables.requestVariables,
...variables.collectionVariables,
...(variables.environments.temp ?? []),
...variables.environments.selected,
...variables.environments.global,
@ -423,6 +427,16 @@ export function runRESTRequest$(
}
)
const collectionVariables =
transformInheritedCollectionVariablesToAggregateEnv(
tab.value.document.inheritedProperties?.variables || []
).map(({ key, initialValue, currentValue, secret }) => ({
key,
initialValue,
currentValue,
secret,
}))
const finalRequest = {
...tab.value.document.request,
auth: requestAuth ?? { authType: "none", authActive: false },
@ -430,8 +444,9 @@ export function runRESTRequest$(
}
const finalEnvs = {
requestVariables: finalRequestVariables as Environment["variables"],
environments: preRequestScriptResult.right.envs,
requestVariables: finalRequestVariables as Environment["variables"],
collectionVariables,
}
const finalEnvsWithNonEmptyValues = filterNonEmptyEnvironmentVariables(
@ -569,7 +584,8 @@ function updateEnvsAfterTestScript(runResult: E.Right<SandboxTestResult>) {
export function runTestRunnerRequest(
request: HoppRESTRequest,
persistEnv = true
persistEnv = true,
inheritedVariables: HoppCollectionVariable[] = []
): Promise<
| E.Left<"script_fail">
| E.Right<{
@ -609,6 +625,7 @@ export function runTestRunnerRequest(
temp: !persistEnv ? getTemporaryVariables() : [],
},
requestVariables: finalRequestVariables,
collectionVariables: inheritedVariables,
})
),
})

View file

@ -1,5 +1,7 @@
import {
CollectionVariable,
HoppCollection,
HoppCollectionVariable,
HoppRESTAuth,
HoppRESTHeaders,
HoppRESTRequest,
@ -30,7 +32,11 @@ type TeamCollectionJSON = {
data: string
}
type CollectionDataProps = { auth: HoppRESTAuth; headers: HoppRESTHeaders }
type CollectionDataProps = {
auth: HoppRESTAuth
headers: HoppRESTHeaders
variables: HoppCollectionVariable[]
}
export const BACKEND_PAGE_SIZE = 10
@ -109,6 +115,7 @@ const parseCollectionData = (
const defaultDataProps: CollectionDataProps = {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
if (!data) {
@ -137,9 +144,15 @@ const parseCollectionData = (
defaultDataProps.headers
)
const variables = parseWithDefaultValue<CollectionDataProps["variables"]>(
z.array(CollectionVariable).safeParse(parsedData?.variables),
defaultDataProps.variables
)
return {
auth,
headers,
variables,
}
}
@ -147,7 +160,7 @@ const parseCollectionData = (
const teamCollectionJSONToHoppRESTColl = (
coll: TeamCollectionJSON
): HoppCollection => {
const { auth, headers } = parseCollectionData(coll.data)
const { auth, headers, variables } = parseCollectionData(coll.data)
return makeCollection({
name: coll.name,
@ -155,6 +168,7 @@ const teamCollectionJSONToHoppRESTColl = (
requests: coll.requests,
auth,
headers,
variables,
})
}
@ -214,9 +228,10 @@ export const teamCollToHoppRESTColl = (
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
const { auth, headers } = parseCollectionData(data)
const { auth, headers, variables } = parseCollectionData(data)
return makeCollection({
id: coll.id,
@ -225,6 +240,7 @@ export const teamCollToHoppRESTColl = (
requests: coll.requests?.map((x) => x.request) ?? [],
auth: auth ?? { authType: "inherit", authActive: true },
headers: headers ?? [],
variables: variables ?? [],
})
}

View file

@ -169,7 +169,8 @@ function removeDuplicatesAndKeepLast(arr: HoppInheritedProperty["headers"]) {
export function updateInheritedPropertiesForAffectedRequests(
path: string,
inheritedProperties: HoppInheritedProperty,
type: "rest" | "graphql"
type: "rest" | "graphql",
collectionId?: string
) {
const tabService =
type === "rest" ? getService(RESTTabService) : getService(GQLTabService)
@ -226,6 +227,22 @@ export function updateInheritedPropertiesForAffectedRequests(
tab.value.document.inheritedProperties.headers = mergedHeaders
}
if (tab.value.document.inheritedProperties?.variables && collectionId) {
const tabInheritedVariables =
tab.value.document.inheritedProperties.variables.filter(
(variable) => variable.parentID !== collectionId
)
// filter out the variables with the parentID as the path in the inheritedProperties
const inheritedVariables = inheritedProperties.variables.filter(
(variable) => variable.parentID === collectionId
)
const finalVariables = [...inheritedVariables, ...tabInheritedVariables]
tab.value.document.inheritedProperties.variables = finalVariables
}
})
}

View file

@ -27,6 +27,7 @@ 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"
@ -36,6 +37,7 @@ 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"
const HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT = "global-variable-highlight"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "environment-not-found-highlight"
@ -138,10 +140,20 @@ 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"
? tooltipEnv.sourceEnvID!
: currentSelectedEnvironment.id
}
const hasSecretStored = secretEnvironmentService.hasSecretValue(
tooltipEnv?.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
tooltipSourceEnvID,
tooltipEnv?.key ?? ""
)
@ -198,7 +210,9 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
? IconVariable
: selectedEnvType === "TEAM_ENV"
? IconUsers
: IconUser
: tooltipEnv?.sourceEnv === "CollectionVariable"
? IconLibrary
: IconUser
}</span>`
const appendEditAction = (tooltip: HTMLElement) => {
@ -236,7 +250,9 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
}
})
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
tooltip.appendChild(editIcon)
if (tooltipEnv?.sourceEnv !== "CollectionVariable") {
tooltip.appendChild(editIcon)
}
}
return {
@ -275,7 +291,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const envContainer = document.createElement("div")
tooltipContainer.appendChild(envContainer)
envContainer.className =
"flex flex-col items-start space-y-1 flex-1 w-full mt-2"
"flex flex-col items-start space-y-1 flex-1 w-full mt-2 !z-[1002]"
const initialValueBlock = document.createElement("div")
initialValueBlock.className = "flex items-center space-x-2"
@ -323,6 +339,8 @@ function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
if (envSource === "RequestVariable")
className = HOPP_REQUEST_VARIABLE_HIGHLIGHT
else if (envSource === "CollectionVariable")
className = HOPP_COLLECTION_ENVIRONMENT_HIGHLIGHT
else if (envSource === "Global") className = HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT
else if (envSource !== undefined) className = HOPP_ENVIRONMENT_HIGHLIGHT
@ -374,56 +392,18 @@ export class HoppEnvironmentPlugin {
private editorView: Ref<EditorView | undefined>
) {
const aggregateEnvs = getAggregateEnvsWithCurrentValue()
const currentTab = restTabs.currentActiveTab.value
const currentTabRequest =
currentTab.document.type === "example-response"
? currentTab.document.response.originalRequest
: currentTab.document.request
this.envs = [...aggregateEnvs]
if (!currentTabRequest) return
watch(
currentTabRequest,
(request) => {
const requestVariables = request?.requestVariables
? request.requestVariables
: []
this.envs = [
...requestVariables.map(({ key, value }) => ({
key,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
})),
...aggregateEnvs,
]
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
},
{ immediate: true, deep: true }
)
const requestVariables = currentTabRequest?.requestVariables ?? []
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
subscribeToStream(aggregateEnvsWithCurrentValue$, (envs) => {
this.envs = [
...requestVariables.map(({ key, value }) => ({
key,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
})),
...envs,
]
this.envs = [...envs]
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([

View file

@ -404,10 +404,10 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
v: 9,
name: options.name || "Untitled Request",
url: finalUrl,
headers: request.headers,
headers: runHeaders,
query,
variables,
auth: request.auth as HoppGQLAuth,
auth,
}
if (operationType === "subscription") {

View file

@ -1,6 +1,6 @@
export { hoppRESTImporter } from "./hopp"
export { hoppOpenAPIImporter } from "./openapi"
export { hoppPostmanImporter } from "./postman"
export { hoppInsomniaImporter } from "./insomnia"
export { hoppInsomniaImporter } from "./insomnia/insomniaColl"
export { toTeamsImporter } from "./myCollections"
export { harImporter } from "./har"

View file

@ -1,5 +1,6 @@
import {
HoppCollection,
HoppCollectionVariable,
HoppRESTAuth,
HoppRESTHeader,
HoppRESTParam,
@ -14,39 +15,33 @@ import {
import * as A from "fp-ts/Array"
import * as TE from "fp-ts/TaskEither"
import * as TO from "fp-ts/TaskOption"
import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/function"
import { ImportRequest, convert } from "insomnia-importers"
import { Header, Parameter } from "insomnia-importers/dist/src/entities"
import { convert } from "insomnia-importers"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { IMPORTER_INVALID_FILE_FORMAT } from ".."
import { replaceInsomniaTemplating } from "./insomniaEnv"
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
import {
InsomniaDoc,
InsomniaDocV5,
InsomniaFolderResource,
InsomniaFolderV5,
InsomniaRequestResource,
InsomniaResource,
InsoReqAuth,
} from "./types"
// TODO: Insomnia allows custom prefixes for Bearer token auth, Hoppscotch doesn't. We just ignore the prefix for now
type UnwrapPromise<T extends Promise<any>> =
T extends Promise<infer Y> ? Y : never
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 = Omit<ImportRequest, "headers" | "parameters"> & {
_type: "request"
} & {
pathParameters?: InsomniaPathParameter[]
} & {
headers: (Header & { description: string })[]
parameters: (Parameter & { description: string })[]
}
const parseInsomniaDoc = (content: string) =>
TO.tryCatch(() => convert(content))
/**
* Used to check if the document is an Insomnia v5 document
* Insomnia v5 documents have a type field that starts with "collection.insomnia.rest/
* @param data InsomniaDoc
* @returns true if the document is an Insomnia v5 document
*/
const isV5InsomniaDoc = (data: InsomniaDoc) =>
data.type &&
typeof data.type === "string" &&
(data.type as string).startsWith("collection.insomnia.rest/5")
const replacePathVarTemplating = (expression: string) =>
expression.replaceAll(/:([^/]+)/g, "<<$1>>")
@ -84,24 +79,22 @@ const getRequestsIn = (
)
)
/**
* The provided type by insomnia-importers, this type corrects it
*/
type InsoReqAuth =
| { type: "basic"; disabled?: boolean; username?: string; password?: string }
| {
type: "oauth2"
disabled?: boolean
accessTokenUrl?: string
authorizationUrl?: string
clientId?: string
scope?: string
}
| {
type: "bearer"
disabled?: boolean
token?: string
}
const getCollectionVariables = (
environment: Record<string, string> | undefined,
folderRes?: InsomniaFolderResource
): HoppCollectionVariable[] => {
const env =
folderRes && folderRes.environment ? folderRes.environment : environment
if (!env) return []
return Object.entries(env).map(([key, value]) => ({
key: replaceVarTemplating(key),
currentValue: "", // set it as empty value since it is handled by currentValue service and we don't want it to sync with BE
initialValue: replaceVarTemplating(value),
secret: false,
}))
}
const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
if (!req.authentication) return { authType: "none", authActive: true }
@ -128,6 +121,9 @@ const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
token: "",
isPKCE: false,
tokenEndpoint: replaceVarTemplating(auth.accessTokenUrl ?? ""),
authRequestParams: [],
refreshRequestParams: [],
tokenRequestParams: [],
},
addTo: "HEADERS",
}
@ -246,6 +242,7 @@ const getHoppFolder = (
requests: getRequestsIn(folderRes, resources).map(getHoppRequest),
auth: { authType: "inherit", authActive: true },
headers: [],
variables: getCollectionVariables(undefined, folderRes), // undefined is used to indicate no environment variables for v4 and below
})
const getHoppCollections = (docs: InsomniaDoc[]) => {
@ -256,10 +253,112 @@ const getHoppCollections = (docs: InsomniaDoc[]) => {
})
}
export const hoppInsomniaImporter = (fileContents: string[]) =>
const getFolders = (collections: InsomniaDocV5["collection"]) => {
if (!collections) return []
return collections.filter(
(x): x is InsomniaFolderV5 => "children" in x && !("url" in x)
)
}
const getRequests = (
collections: InsomniaDocV5["collection"]
): InsomniaRequestResource[] => {
if (!collections) return []
return collections.filter((x): x is InsomniaRequestResource => "url" in x)
}
const getParsedHoppFolder = (
name: string,
collection: InsomniaFolderV5
): HoppCollection => {
return makeCollection({
name: name ?? collection.name ?? "Untitled Collection",
folders: getFolders(collection.children ?? []).map((f) =>
getParsedHoppFolder(f.name, f)
),
requests: getRequests(collection.children ?? []).map(getParsedHoppRequest),
auth: { authType: "inherit", authActive: true },
headers: [],
variables: getCollectionVariables(collection.environment),
})
}
const getParsedHoppRequest = (req: InsomniaRequestResource) => {
return makeRESTRequest({
name: req.name ?? "Untitled Request",
method: req.method?.toUpperCase() ?? "GET",
endpoint: replaceVarTemplating(req.url ?? "", true),
auth: getHoppReqAuth(req),
body: getHoppReqBody(req),
headers: getHoppReqHeaders(req),
params: getHoppReqParams(req),
preRequestScript: "",
testScript: "",
requestVariables: getHoppReqVariables(req),
//insomnia doesn't have saved response
responses: {},
})
}
const getParsedHoppCollections = (docs: InsomniaDocV5[]): HoppCollection[] =>
docs.flatMap((doc) => {
if (doc && Array.isArray(doc.collection)) {
return makeCollection({
name: doc.name ?? "Untitled Collection",
folders: getFolders(doc.collection).map((f) =>
getParsedHoppFolder(f.name, f)
),
requests: getRequests(doc.collection).map((x) =>
getParsedHoppRequest(x)
),
auth: { authType: "inherit", authActive: true },
headers: [],
variables: getCollectionVariables(doc.environments?.data),
})
}
return []
})
const parseInsomniaDoc = (content: string) =>
pipe(
fileContents,
A.traverse(TO.ApplicativeSeq)(parseInsomniaDoc),
TO.map(getHoppCollections),
TO.tryCatch(() => convert(content)),
TO.map((doc) => getHoppCollections([doc])),
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
)
const parseV5InsomniaDoc = (content: string) =>
pipe(
safeParseJSONOrYAML(content),
TO.fromOption,
TO.map((parsed) => parsed as InsomniaDocV5),
TO.map((doc) => getParsedHoppCollections([doc])),
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
)
/**
* insomina-importers v3.6.0 does supoort insomina v5
* (NOTE: Currently, insomnia-importers only supports v4 and below)
* https://github.com/Kong/insomnia/issues/8504
*/
export const hoppInsomniaImporter = (fileContents: string[]) => {
return pipe(
fileContents,
A.traverse(TE.ApplicativeSeq)((content) =>
pipe(
safeParseJSONOrYAML(content),
O.fold(
() => TE.left(IMPORTER_INVALID_FILE_FORMAT),
(parsed) =>
isV5InsomniaDoc(parsed as InsomniaDoc)
? parseV5InsomniaDoc(content)
: parseInsomniaDoc(content)
)
)
),
TE.map(A.flatten)
)
}

View file

@ -1,7 +1,7 @@
import * as TE from "fp-ts/TaskEither"
import * as O from "fp-ts/Option"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { IMPORTER_INVALID_FILE_FORMAT } from ".."
import { z } from "zod"
import {

View file

@ -0,0 +1,150 @@
import { ImportRequest, convert } from "insomnia-importers"
import { Header, Parameter } from "insomnia-importers/dist/src/entities"
type UnwrapPromise<T extends Promise<any>> =
T extends Promise<infer Y> ? Y : never
export type InsomniaDoc = UnwrapPromise<ReturnType<typeof convert>>
export type InsomniaResource = ImportRequest
// insomnia-importers v3.6.0 doesn't provide a type for path parameters and they have deprecated the library
export type InsomniaPathParameter = {
name: string
value: string
}
export type InsomniaFolderResource = ImportRequest & { _type: "request_group" }
export type InsomniaRequestResource = Omit<
ImportRequest,
"headers" | "parameters"
> & {
_type: "request"
} & {
pathParameters?: InsomniaPathParameter[]
} & {
headers: (Header & { description: string })[]
parameters: (Parameter & { description: string })[]
}
/**
* The provided type by insomnia-importers, this type corrects it
*/
export type InsoReqAuth =
| { type: "basic"; disabled?: boolean; username?: string; password?: string }
| {
type: "oauth2"
disabled?: boolean
accessTokenUrl?: string
authorizationUrl?: string
clientId?: string
scope?: string
}
| {
type: "bearer"
disabled?: boolean
token?: string
}
/**
* Insomnia v5 document types
* These types are used to represent the structure of Insomnia v5 documents.
*/
export type InsomniaDocV5 = {
type: `collection.insomnia.rest/${string}`
name: string
meta: {
id: string
created: number
modified: number
description?: string
}
collection: (
| InsomniaFolderV5
| InsomniaRequestResource
| InsomniaScriptOnlyV5
)[]
cookieJar?: InsomniaCookieJarV5
environments?: InsomniaEnvironmentV5
}
export type InsomniaMetaV5 = {
id: string
created: number
modified: number
description?: string
sortKey?: number
}
export type InsomniaScriptOnlyV5 = {
name: string
meta: InsomniaMetaV5
scripts: {
afterResponse?: string
preRequest?: string
}
}
export type InsomniaFolderV5 = {
name: string
meta: InsomniaMetaV5
children?: (InsomniaFolderV5 | InsomniaRequestResource)[]
environment?: Record<string, string>
scripts?: {
afterResponse?: string
preRequest?: string
}
}
export type InsomniaKeyValueV5 = {
id?: string
name: string
value: string
description?: string
disabled?: boolean
type?: string
multiline?: boolean
}
export type InsomniaCookieJarV5 = {
name: string
meta: {
id: string
created: number
modified: number
}
cookies: {
key: string
value: string
domain: string
path: string
secure?: boolean
httpOnly?: boolean
hostOnly?: boolean
creation: string
lastAccessed: string
sameSite?: "lax" | "strict" | "none"
id: string
}[]
}
export type InsomniaEnvironmentV5 = {
name: string
meta: {
id: string
created: number
modified: number
isPrivate?: boolean
}
data: Record<string, string>
subEnvironments?: {
name: string
meta: {
id: string
created: number
modified: number
isPrivate?: boolean
sortKey?: number
}
data: Record<string, string>
}[]
}

View file

@ -13,6 +13,7 @@ import {
ValidContentTypes,
HoppRESTRequestResponses,
makeHoppRESTResponseOriginalRequest,
HoppCollectionVariable,
} from "@hoppscotch/data"
import * as A from "fp-ts/Array"
import { flow, pipe } from "fp-ts/function"
@ -28,6 +29,7 @@ import {
RequestAuthDefinition,
Variable,
VariableDefinition,
VariableList,
} from "postman-collection"
import { stringArrayJoin } from "~/helpers/functional/array"
import { PMRawLanguage } from "~/types/pm-coll-exts"
@ -79,6 +81,32 @@ const parseDescription = (descField?: string | DescriptionDefinition) => {
return descField.content
}
const getHoppCollVariables = (
ig: ItemGroup<Item>
): HoppCollectionVariable[] => {
if (!("variables" in ig && ig.variables)) {
return []
}
return pipe(
(ig.variables as VariableList).all(),
A.filter(
(variable) =>
variable.key !== undefined &&
variable.key !== null &&
variable.key.length > 0
),
A.map((variable) => {
return <HoppCollectionVariable>{
key: replacePMVarTemplating(variable.key ?? ""),
initialValue: replacePMVarTemplating(variable.value ?? ""),
currentValue: "",
secret: variable.type === "secret",
}
})
)
}
const getHoppReqHeaders = (
headers: Item["request"]["headers"] | null
): HoppRESTHeader[] => {
@ -288,6 +316,9 @@ const getHoppReqAuth = (
tokenEndpoint: accessTokenURL,
clientSecret: "",
isPKCE: false,
authRequestParams: [],
tokenRequestParams: [],
refreshRequestParams: [],
},
addTo: "HEADERS",
}
@ -457,6 +488,7 @@ const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection =>
requests: pipe(ig.items.all(), A.filter(isPMItem), A.map(getHoppRequest)),
auth: getHoppReqAuth(ig.auth),
headers: [],
variables: getHoppCollVariables(ig),
})
export const getHoppCollections = (collections: PMCollection[]) => {

View file

@ -259,6 +259,12 @@ export type HoppSavedExampleDocument = {
* (atleast as far as we can say)
*/
isDirty: boolean
/**
* The inherited properties from the parent collection
* (if any)
*/
inheritedProperties?: HoppInheritedProperty
}
/**

View file

@ -1,6 +1,7 @@
import * as E from "fp-ts/Either"
import { BehaviorSubject, Subscription } from "rxjs"
import {
HoppCollectionVariable,
HoppRESTAuth,
HoppRESTHeader,
translateToNewRequest,
@ -26,6 +27,9 @@ import {
TeamCollectionOrderUpdatedDocument,
} from "~/helpers/backend/graphql"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { getService } from "~/modules/dioc"
export const TEAMS_BACKEND_PAGE_SIZE = 10
@ -1034,12 +1038,48 @@ export default class NewTeamCollectionAdapter {
}
}
private getCurrentValue = (
env: HoppCollectionVariable,
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(
collectionID,
varIndex
)?.value
}
return currentEnvironmentValueService.getEnvironmentVariable(
collectionID,
varIndex
)?.currentValue
}
/**
* This function populates the values of the variables with the current values or secrets.
* @param variables Variables to populate
* @returns Populated variables with current values or secrets
*/
private populateValues(
variables: HoppCollectionVariable[],
parentID: string
) {
return variables.map((v, index) => ({
...v,
currentValue: this.getCurrentValue(v, index, parentID) ?? v.currentValue,
}))
}
/**
* Used to obtain the inherited auth and headers for a given folder path, used for both REST and GraphQL team collections
* @param folderPath the path of the folder to cascade the auth from
* @returns the inherited auth and headers for the given folder path
*/
public cascadeParentCollectionForHeaderAuth(folderPath: string) {
public cascadeParentCollectionForProperties(folderPath: string) {
let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "",
parentName: "",
@ -1050,14 +1090,16 @@ export default class NewTeamCollectionAdapter {
}
const headers: HoppInheritedProperty["headers"] = []
if (!folderPath) return { auth, headers }
const variables: HoppInheritedProperty["variables"] = []
if (!folderPath) return { auth, headers, variables }
const path = folderPath.split("/")
// Check if the path is empty or invalid
if (!path || path.length === 0) {
console.error("Invalid path:", folderPath)
return { auth, headers }
return { auth, headers, variables }
}
// Loop through the path and get the last parent folder with authType other than 'inherit'
@ -1067,17 +1109,19 @@ export default class NewTeamCollectionAdapter {
// Check if parentFolder is undefined or null
if (!parentFolder) {
console.error("Parent folder not found for path:", path)
return { auth, headers }
return { auth, headers, variables }
}
const data: {
auth: HoppRESTAuth
headers: HoppRESTHeader[]
variables: HoppCollectionVariable[]
} = parentFolder.data
? JSON.parse(parentFolder.data)
: {
auth: null,
headers: null,
variables: null,
}
if (!data.auth) {
@ -1091,8 +1135,11 @@ export default class NewTeamCollectionAdapter {
if (!data.headers) data.headers = []
if (!data.variables) data.variables = []
const parentFolderAuth = data.auth
const parentFolderHeaders = data.headers
const parentFolderVariables = data.variables
if (
parentFolderAuth?.authType === "inherit" &&
@ -1137,8 +1184,22 @@ export default class NewTeamCollectionAdapter {
}
})
}
// Update variables, overwriting duplicates by key
if (parentFolderVariables) {
const currentPath = [...path.slice(0, i + 1)].join("/")
variables.push({
parentID: parentFolder.id ?? currentPath,
parentName: parentFolder.title,
inheritedVariables: this.populateValues(
parentFolderVariables,
parentFolder.id ?? currentPath
),
})
}
}
return { auth, headers }
return { auth, headers, variables }
}
}

View file

@ -1,4 +1,5 @@
import {
HoppCollectionVariable,
HoppRESTAuth,
HoppRESTHeader,
HoppRESTRequest,
@ -337,7 +338,7 @@ export class TeamSearchService extends Service {
this.teamsSearchResultsLoading.value = false
}
cascadeParentCollectionForHeaderAuthForSearchResults = (
cascadeParentCollectionForPropertiesForSearchResults = (
collectionID: string
): HoppInheritedProperty => {
const defaultInheritedAuth: HoppInheritedProperty["auth"] = {
@ -351,15 +352,23 @@ export class TeamSearchService extends Service {
const defaultInheritedHeaders: HoppInheritedProperty["headers"] = []
const defaultInheritedVariables: HoppInheritedProperty["variables"] = []
const collection = Object.values(this.searchResultsCollections).find(
(col) => col.id === collectionID
)
if (!collection)
return { auth: defaultInheritedAuth, headers: defaultInheritedHeaders }
return {
auth: defaultInheritedAuth,
headers: defaultInheritedHeaders,
variables: defaultInheritedVariables,
}
const inheritedAuthData = this.findInheritableParentAuth(collectionID)
const inheritedHeadersData = this.findInheritableParentHeaders(collectionID)
const inheritedVariablesData =
this.findInheritableParentVariables(collectionID)
return {
auth: E.isRight(inheritedAuthData)
@ -368,6 +377,9 @@ export class TeamSearchService extends Service {
headers: E.isRight(inheritedHeadersData)
? Object.values(inheritedHeadersData.right)
: defaultInheritedHeaders,
variables: E.isRight(inheritedVariablesData)
? Object.values(inheritedVariablesData.right)
: defaultInheritedVariables,
}
}
@ -394,6 +406,7 @@ export class TeamSearchService extends Service {
const parentInheritedData = JSON.parse(collection.data) as {
auth: HoppRESTAuth
headers: HoppRESTHeader[]
variables: HoppCollectionVariable[]
}
const inheritedAuth = parentInheritedData.auth
@ -437,6 +450,7 @@ export class TeamSearchService extends Service {
const parentInheritedData = JSON.parse(collection.data) as {
auth: HoppRESTAuth
headers: HoppRESTHeader[]
variables: HoppCollectionVariable[]
}
const inheritedHeaders = parentInheritedData.headers
@ -464,6 +478,45 @@ export class TeamSearchService extends Service {
return E.right(existingHeaders)
}
findInheritableParentVariables = (
collectionID: string,
existingVariables: HoppInheritedProperty["variables"] = []
): E.Either<string, HoppInheritedProperty["variables"]> => {
const collection = Object.values(this.searchResultsCollections).find(
(col) => col.id === collectionID
)
const vars = [...Object.values(existingVariables)]
if (!collection) {
return E.left("PARENT_NOT_FOUND" as const)
}
if (collection.data) {
const parentData = JSON.parse(collection.data) as {
auth: HoppRESTAuth
headers: HoppRESTHeader[]
variables: HoppCollectionVariable[]
}
const variables = parentData.variables
if (variables) {
vars.push({
parentID: collection.id,
parentName: collection.title,
inheritedVariables: variables,
})
}
}
if (collection.parentID) {
return this.findInheritableParentVariables(collection.parentID, vars)
}
return E.right(vars)
}
expandCollection = async (collectionID: string) => {
if (this.expandingCollections.value.includes(collectionID)) return

View file

@ -3,6 +3,7 @@ import {
HoppGQLAuth,
HoppRESTHeader,
HoppRESTAuth,
HoppCollectionVariable,
} from "@hoppscotch/data"
export type HoppInheritedProperty = {
@ -16,4 +17,9 @@ export type HoppInheritedProperty = {
parentName: string
inheritedHeader: HoppRESTHeader | GQLHeader
}[]
variables: {
parentID: string
parentName: string
inheritedVariables: HoppCollectionVariable[]
}[]
}

View file

@ -0,0 +1,72 @@
import { AggregateEnvironment } from "~/newstore/environments"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { getService } from "~/modules/dioc"
import { HoppCollectionVariable } from "@hoppscotch/data"
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,
varIndex
)?.value
}
return currentEnvironmentValueService.getEnvironmentVariable(
collectionID,
varIndex
)?.currentValue
}
/**
* Function to transform inherited collection variables into an array of `AggregateEnvironment` objects.
* @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.
*/
export const transformInheritedCollectionVariablesToAggregateEnv = (
variables: HoppInheritedProperty["variables"],
showSecret: boolean = true
): AggregateEnvironment[] => {
return variables.flatMap(({ parentID, inheritedVariables }) =>
inheritedVariables.map(
({ currentValue, initialValue, key, secret }, index) => ({
key,
currentValue:
getCurrentValue(secret, index, parentID, showSecret) ?? currentValue,
initialValue,
sourceEnv: "CollectionVariable",
secret,
sourceEnvID: parentID,
})
)
)
}
/**
* Utility function to populate current values in inherited collection variables.
* @param variables - The inherited collection variables to populate.
* @param parentID - The ID of the parent collection from which to inherit values.
* @returns - An array of `HoppCollectionVariable` objects with populated current values.
*/
export const populateValuesInInheritedCollectionVars = (
variables: HoppCollectionVariable[],
parentID?: string
): HoppCollectionVariable[] =>
parentID
? variables.map((variable, index) => ({
...variable,
currentValue:
getCurrentValue(variable.secret, index, parentID) ??
variable.currentValue,
}))
: []

View file

@ -1,6 +1,7 @@
import {
generateUniqueRefId,
HoppCollection,
HoppCollectionVariable,
HoppGQLAuth,
HoppGQLRequest,
HoppRESTAuth,
@ -17,6 +18,8 @@ import { getService } from "~/modules/dioc"
import { getI18n } from "~/modules/i18n"
import { RESTTabService } from "~/services/tab/rest"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { CurrentValueService } from "~/services/current-environment-value.service"
const defaultRESTCollectionState = {
state: [
@ -29,6 +32,7 @@ const defaultRESTCollectionState = {
authActive: false,
},
headers: [],
variables: [],
}),
],
}
@ -44,6 +48,7 @@ const defaultGraphqlCollectionState = {
authActive: false,
},
headers: [],
variables: [],
}),
],
}
@ -69,15 +74,56 @@ export function navigateToFolderWithIndexPath(
return target !== undefined ? target : null
}
const getCurrentValue = (
env: HoppCollectionVariable,
varIndex: number,
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,
varIndex
)?.value
}
return currentEnvironmentValueService.getEnvironmentVariable(
collectionID,
varIndex
)?.currentValue
}
/**
* This function populates the values of the variables with the current values or secrets.
* @param variables Variables to populate
* @returns Populated variables with current values or secrets
*/
function populateValues(
variables: HoppCollectionVariable[],
parentID: string,
showSecret: boolean
) {
return variables.map((v, index) => ({
...v,
currentValue:
getCurrentValue(v, index, parentID, showSecret) ?? v.currentValue,
}))
}
/**
* Used to obtain the inherited auth and headers for a given folder path, used for both REST and GraphQL personal collections
* @param folderPath the path of the folder to cascade the auth from
* @param type the type of collection
* @param showSecret whether to show secret values in the collection variables
* @returns the inherited auth and headers for the given folder path
*/
export function cascadeParentCollectionForHeaderAuth(
export function cascadeParentCollectionForProperties(
folderPath: string | undefined,
type: "rest" | "graphql"
type: "rest" | "graphql",
showSecret: boolean = false
) {
const collectionStore =
type === "rest" ? restCollectionStore : graphqlCollectionStore
@ -92,14 +138,16 @@ export function cascadeParentCollectionForHeaderAuth(
}
const headers: HoppInheritedProperty["headers"] = []
if (!folderPath) return { auth, headers }
const variables: HoppInheritedProperty["variables"] = []
if (!folderPath) return { auth, headers, variables }
const path = folderPath.split("/").map((i) => parseInt(i))
// Check if the path is empty or invalid
if (!path || path.length === 0) {
console.error("Invalid path:", folderPath)
return { auth, headers }
return { auth, headers, variables }
}
// Loop through the path and get the last parent folder with authType other than 'inherit'
@ -112,7 +160,7 @@ export function cascadeParentCollectionForHeaderAuth(
// Check if parentFolder is undefined or null
if (!parentFolder) {
console.error("Parent folder not found for path:", path)
return { auth, headers }
return { auth, headers, variables }
}
const parentFolderAuth = parentFolder.auth as HoppRESTAuth | HoppGQLAuth
@ -120,6 +168,9 @@ export function cascadeParentCollectionForHeaderAuth(
| HoppRESTHeaders
| GQLHeader[]
const parentFolderVariables =
parentFolder.variables as HoppCollectionVariable[]
// check if the parent folder has authType 'inherit' and if it is the root folder
if (
parentFolderAuth?.authType === "inherit" &&
@ -164,9 +215,23 @@ export function cascadeParentCollectionForHeaderAuth(
}
})
}
if (parentFolderVariables) {
const currentPath = [...path.slice(0, i + 1)].join("/")
variables.push({
parentID: parentFolder._ref_id ?? parentFolder.id ?? currentPath,
parentName: parentFolder.name,
inheritedVariables: populateValues(
parentFolderVariables,
parentFolder.id ?? currentPath,
showSecret
),
})
}
}
return { auth, headers }
return { auth, headers, variables }
}
function reorderItems(array: unknown[], from: number, to: number) {
@ -254,6 +319,7 @@ const restCollectionDispatchers = defineDispatchers({
authActive: true,
},
headers: [],
variables: [],
})
const newState = state
@ -879,6 +945,7 @@ const gqlCollectionDispatchers = defineDispatchers({
authActive: true,
},
headers: [],
variables: [],
})
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
@ -1222,8 +1289,13 @@ function computeCollectionInheritedProps(
ref_id: string,
type: "my-collections" | "team-collections" = "my-collections",
parentAuth: HoppRESTAuth | null = null,
parentHeaders: HoppRESTHeaders | null = null
): { auth: HoppRESTAuth; headers: HoppRESTHeaders } | null {
parentHeaders: HoppRESTHeaders | null = null,
parentVariables: HoppCollectionVariable[] | null = null
): {
auth: HoppRESTAuth
headers: HoppRESTHeaders
variables: HoppCollectionVariable[]
} | null {
// Determine the inherited authentication and headers
const inheritedAuth =
collection.auth?.authType === "inherit" && collection.auth.authActive
@ -1235,6 +1307,11 @@ function computeCollectionInheritedProps(
...collection.headers,
]
const inheritedVariables = [
...(parentVariables ?? []),
...collection.variables,
]
// Check if the current collection matches the target reference ID
const isTargetCollection =
type === "my-collections"
@ -1245,6 +1322,7 @@ function computeCollectionInheritedProps(
return {
auth: inheritedAuth,
headers: inheritedHeaders,
variables: inheritedVariables,
}
}
@ -1255,7 +1333,8 @@ function computeCollectionInheritedProps(
ref_id,
type,
inheritedAuth,
inheritedHeaders
inheritedHeaders,
inheritedVariables
)
if (result) return result // Return as soon as a match is found
}
@ -1267,7 +1346,11 @@ export function getRESTCollectionInheritedProps(
collectionID: string,
collections: HoppCollection[] = restCollectionStore.value.state,
type: "my-collections" | "team-collections" = "my-collections"
): { auth: HoppRESTAuth; headers: HoppRESTHeaders } | null {
): {
auth: HoppRESTAuth
headers: HoppRESTHeaders
variables: HoppCollectionVariable[]
} | null {
for (const collection of collections) {
const result = computeCollectionInheritedProps(
collection,

View file

@ -14,6 +14,7 @@ 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" }
@ -49,9 +50,6 @@ const defaultEnvironmentsState = {
} as SelectedEnvironmentIndex,
}
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
type EnvironmentStore = typeof defaultEnvironmentsState
const dispatchers = defineDispatchers({
@ -423,6 +421,7 @@ export type AggregateEnvironment = {
currentValue: string
secret: boolean
sourceEnv: string
sourceEnvID?: string
}
/**
@ -431,13 +430,27 @@ export type AggregateEnvironment = {
* NOTE: The source environment attribute will be "Global" for Global Env as source.
* The priority of the variables is as follows:
* 1. Pre-defined variables
* 2. Selected Environment Variables
* 3. Global Environment Variables
* 2. Request Variables (from the current request)
* 3. Selected Environment Variables
* 4. Global Environment Variables
*/
export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
[currentEnvironment$, globalEnv$]
).pipe(
map(([selectedEnv, globalEnv]) => {
const restTabs = getService(RESTTabService)
const currentTab = restTabs.currentActiveTab.value
const currentTabRequest =
currentTab.document.type === "example-response"
? currentTab.document.response.originalRequest
: currentTab.document.request
const requestVariables = currentTabRequest?.requestVariables
? currentTabRequest.requestVariables
: []
const effectiveAggregateEnvs: AggregateEnvironment[] = []
// Ensure pre-defined variables are prioritised over other environment variables with the same name
@ -453,6 +466,18 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
const aggregateEnvKeys = effectiveAggregateEnvs.map(({ key }) => key)
requestVariables.forEach(({ key, value, active }) => {
if (!aggregateEnvKeys.includes(key) && active) {
effectiveAggregateEnvs.push({
key,
currentValue: value,
initialValue: value,
secret: false,
sourceEnv: "RequestVariable",
})
}
})
selectedEnv?.variables.forEach((variable) => {
const { key, secret } = variable
const currentValue =
@ -495,8 +520,47 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
)
export function getAggregateEnvs() {
const restTabs = getService(RESTTabService)
const currentEnv = getCurrentEnvironment()
const currentTab = restTabs.currentActiveTab.value
const currentTabRequest =
currentTab.document.type === "example-response"
? currentTab.document.response.originalRequest
: currentTab.document.request
const requestVariables = currentTabRequest?.requestVariables
? currentTabRequest.requestVariables
: []
return [
...HOPP_SUPPORTED_PREDEFINED_VARIABLES.map(({ key, getValue }) => {
return <AggregateEnvironment>{
key,
currentValue: getValue(),
initialValue: getValue(),
secret: false,
sourceEnv: currentEnv.name,
}
}),
...requestVariables
.map(({ key, value, active }) => {
if (active) {
return <AggregateEnvironment>{
key,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
}
}
return
})
.filter((v): v is AggregateEnvironment => v !== undefined),
...currentEnv.variables.map((x) => {
let currentValue = ""
if (!x.secret) {
@ -528,9 +592,49 @@ 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 }) => {
return <AggregateEnvironment>{
key,
currentValue: getValue(),
initialValue: getValue(),
secret: false,
sourceEnv: currentEnv.name,
}
}),
...requestVariables
.map(({ key, value, active }) => {
if (active) {
return <AggregateEnvironment>{
key,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
}
}
return
})
.filter((v): v is AggregateEnvironment => v !== undefined),
...currentEnv.variables.map((x, index) => {
let currentValue = x.currentValue
if (x.secret) {
@ -581,6 +685,22 @@ 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 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
@ -594,6 +714,18 @@ export const aggregateEnvsWithCurrentValue$: Observable<
})
})
requestVariables.map(({ key, value, active }) => {
if (active) {
results.push({
key,
currentValue: value,
initialValue: value,
secret: false,
sourceEnv: "RequestVariable",
})
}
})
selectedEnv?.variables.map((x, index) => {
let currentValue = x.currentValue
if (x.secret) {

View file

@ -1,5 +1,7 @@
import { Service } from "dioc"
import { Container, Service } from "dioc"
import { cloneDeep } from "lodash-es"
import { nextTick } from "vue"
import { watch } from "vue"
import { reactive, computed } from "vue"
/**
@ -21,6 +23,12 @@ export type Variable = {
export class CurrentValueService extends Service {
public static readonly ID = "CURRENT_VALUE_SERVICE"
constructor(c: Container) {
super(c)
// Initialize the secret environments map
this.watchCurrentEnvironments()
}
/**
* Map of current value of environments.
* The key is the ID of the environment.
@ -159,4 +167,26 @@ export class CurrentValueService extends Service {
})
return environments
})
/**
* Watches the current environments for changes.
* If a secret variable is removed or has an empty key, it will be deleted.
*/
protected watchCurrentEnvironments() {
watch(
() => this.environments,
() => {
nextTick(() => {
this.environments.forEach((vars, id) => {
const filteredVars = vars.filter((v) => v.key !== "")
if (filteredVars.length === 0) {
this.environments.delete(id)
}
})
})
},
{ deep: true }
)
}
}

View file

@ -46,7 +46,14 @@ describe("RequestInspectorService", () => {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
headers: [
{
key: "Cookie",
value: "some-cookie",
active: true,
description: "",
},
],
})
const result = requestInspector.getInspections(req)
@ -83,7 +90,14 @@ describe("RequestInspectorService", () => {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
headers: [
{
key: "Cookie",
value: "some-cookie",
active: true,
description: "",
},
],
})
const result = requestInspector.getInspections(req)

View file

@ -24,6 +24,7 @@ import { useStreamStatic } from "~/composables/stream"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { RESTTabService } from "~/services/tab/rest"
import { CurrentValueService } from "~/services/current-environment-value.service"
import { transformInheritedCollectionVariablesToAggregateEnv } from "~/helpers/utils/inheritedCollectionVarTransformer"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
@ -76,16 +77,17 @@ export class EnvironmentInspectorService extends Service implements Inspector {
const currentTab = this.restTabs.currentActiveTab.value
const currentTabRequest =
currentTab.document.type === "request"
? currentTab.document.request
: currentTab.document.type === "example-response"
? currentTab.document.response.originalRequest
: null
const collectionVariables =
currentTab.document.type === "request" ||
currentTab.document.type === "example-response"
? transformInheritedCollectionVariablesToAggregateEnv(
currentTab.document.inheritedProperties?.variables ?? []
)
: []
const environmentVariables = [
...(currentTabRequest?.requestVariables ?? []),
...this.aggregateEnvsWithValue.value,
...collectionVariables,
]
const envKeys = environmentVariables.map((e) => e.key)
@ -192,32 +194,35 @@ export class EnvironmentInspectorService extends Service implements Inspector {
const currentSelectedEnvironment = getCurrentEnvironment()
const currentTab = this.restTabs.currentActiveTab.value
const currentTabRequest =
currentTab.document.type === "request"
? currentTab.document.request
: currentTab.document.type === "example-response"
? currentTab.document.response.originalRequest
: null
const collectionVariables =
currentTab.document.type === "request" ||
currentTab.document.type === "example-response"
? transformInheritedCollectionVariablesToAggregateEnv(
currentTab.document.inheritedProperties?.variables ?? [],
false
)
: []
const environmentVariables =
this.filterNonEmptyEnvironmentVariables([
// Transform the request variables to environment variables
...(currentTabRequest?.requestVariables ?? []).map((env) => ({
key: env.key,
currentValue: env.value,
initialValue: env.value,
secret: false,
sourceEnv: "RequestVariable",
})),
...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(
env.sourceEnv !== "Global"
? currentSelectedEnvironment.id
: "Global",
tooltipSourceEnvID,
env.key
)

View file

@ -25,7 +25,7 @@ const DEFAULT_SETTINGS = getDefaultSettings()
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
{
v: 9,
v: 10,
name: "Echo",
requests: [
{
@ -51,13 +51,14 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
],
auth: { authType: "none", authActive: true },
headers: [],
variables: [],
folders: [],
},
]
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
{
v: 9,
v: 10,
name: "Echo",
requests: [
{
@ -75,6 +76,7 @@ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
],
auth: { authType: "none", authActive: true },
headers: [],
variables: [],
folders: [],
},
]

View file

@ -9,6 +9,7 @@ import {
HoppRESTRequestResponse,
HoppCollection,
GlobalEnvironment,
CollectionVariable,
} from "@hoppscotch/data"
import { entityReference } from "verzod"
import { z } from "zod"
@ -313,6 +314,15 @@ const HoppInheritedPropertySchema = z
inheritedHeader: z.union([HoppRESTHeaders, GQLHeader]),
})
),
variables: z
.array(
z.object({
parentID: z.string(),
parentName: z.string(),
inheritedVariables: z.array(CollectionVariable),
})
)
.catch([]),
})
.strict()
@ -579,6 +589,7 @@ export const REST_TAB_STATE_SCHEMA = z
response: entityReference(HoppRESTRequestResponse),
saveContext: z.optional(HoppRESTSaveContextSchema),
isDirty: z.boolean(),
inheritedProperties: z.optional(HoppInheritedPropertySchema),
}),
]),
})

View file

@ -1,5 +1,6 @@
import { Service } from "dioc"
import { reactive, computed } from "vue"
import { Container, Service } from "dioc"
import { nextTick } from "vue"
import { reactive, computed, watch } from "vue"
/**
* Defines a secret environment variable.
@ -19,6 +20,12 @@ export type SecretVariable = {
export class SecretEnvironmentService extends Service {
public static readonly ID = "SECRET_ENVIRONMENT_SERVICE"
constructor(c: Container) {
super(c)
// Initialize the secret environments map
this.watchSecretEnvironments()
}
/**
* Map of secret environments.
* The key is the ID of the secret environment.
@ -137,4 +144,26 @@ export class SecretEnvironmentService extends Service {
})
return secretEnvironments
})
/**
* Watches the secret environments for changes.
* If a secret variable is removed or has an empty key, it will be deleted.
*/
protected watchSecretEnvironments() {
watch(
() => this.secretEnvironments,
() => {
nextTick(() => {
this.secretEnvironments.forEach((secretVars, id) => {
const filteredVars = secretVars.filter((v) => v.key !== "")
if (filteredVars.length === 0) {
this.secretEnvironments.delete(id)
}
})
})
},
{ deep: true }
)
}
}

View file

@ -10,7 +10,7 @@ import { Ref, computed, effectScope, markRaw, ref, watch } from "vue"
import { getI18n } from "~/modules/i18n"
import MiniSearch from "minisearch"
import {
cascadeParentCollectionForHeaderAuth,
cascadeParentCollectionForProperties,
graphqlCollectionStore,
restCollectionStore,
} from "~/newstore/collections"
@ -318,11 +318,6 @@ export class CollectionsSpotlightSearcherService
if (!req) return
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath.join("/"),
"rest"
)
this.restTab.createNewTab(
{
type: "request",
@ -333,10 +328,10 @@ export class CollectionsSpotlightSearcherService
folderPath: folderPath.join("/"),
requestIndex: reqIndex,
},
inheritedProperties: {
auth,
headers,
},
inheritedProperties: cascadeParentCollectionForProperties(
folderPath.join("/"),
"rest"
),
},
true
)
@ -350,10 +345,6 @@ export class CollectionsSpotlightSearcherService
if (!req) return
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath.join("/"),
"graphql"
)
this.gqlTab.createNewTab({
saveContext: {
originLocation: "user-collection",
@ -363,10 +354,10 @@ export class CollectionsSpotlightSearcherService
cursorPosition: 0,
request: req,
isDirty: false,
inheritedProperties: {
auth,
headers,
},
inheritedProperties: cascadeParentCollectionForProperties(
folderPath.join("/"),
"graphql"
),
})
}
}

View file

@ -217,12 +217,12 @@ export class TeamsSpotlightSearcherService
if (!selectedRequest) return
const collectionID = result.id
const collectionID = selectedRequest.collectionID
if (!collectionID) return
inheritedProperties =
this.teamsSearch.cascadeParentCollectionForHeaderAuthForSearchResults(
this.teamsSearch.cascadeParentCollectionForPropertiesForSearchResults(
collectionID
)

View file

@ -1,5 +1,6 @@
import {
HoppCollection,
HoppCollectionVariable,
HoppRESTHeaders,
HoppRESTRequest,
} from "@hoppscotch/data"
@ -15,6 +16,7 @@ import {
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { HoppTestData, HoppTestResult } from "~/helpers/types/HoppTestResult"
import { HoppTab } from "../tab"
import { populateValuesInInheritedCollectionVars } from "~/helpers/utils/inheritedCollectionVarTransformer"
export type TestRunnerOptions = {
stopRef: Ref<boolean>
@ -59,6 +61,7 @@ export class TestRunnerService extends Service {
headers: collection.headers,
folders: [],
requests: [],
variables: [],
}
this.runTestCollection(tab, collection, options)
@ -87,7 +90,9 @@ export class TestRunnerService extends Service {
options: TestRunnerOptions,
parentPath: number[] = [],
parentHeaders?: HoppRESTHeaders,
parentAuth?: HoppRESTRequest["auth"]
parentAuth?: HoppRESTRequest["auth"],
parentVariables: HoppCollection["variables"] = [],
parentID?: string
) {
try {
// Compute inherited auth and headers for this collection
@ -101,6 +106,17 @@ export class TestRunnerService extends Service {
...collection.headers,
]
const inheritedVariables = [
...(populateValuesInInheritedCollectionVars(
parentVariables,
parentID || collection._ref_id || collection.id
) || []),
...(populateValuesInInheritedCollectionVars(
collection.variables,
collection._ref_id || collection.id
) || []),
]
// Process folders progressively
for (let i = 0; i < collection.folders.length; i++) {
if (options.stopRef?.value) {
@ -129,7 +145,9 @@ export class TestRunnerService extends Service {
options,
currentPath,
inheritedHeaders,
inheritedAuth
inheritedAuth,
inheritedVariables,
collection._ref_id || collection.id
)
}
@ -165,7 +183,8 @@ export class TestRunnerService extends Service {
finalRequest,
collection,
options,
currentPath
currentPath,
inheritedVariables
)
if (options.delay && options.delay > 0) {
@ -255,7 +274,8 @@ export class TestRunnerService extends Service {
request: TestRunnerRequest,
collection: HoppCollection,
options: TestRunnerOptions,
path: number[]
path: number[],
inheritedVariables: HoppCollectionVariable[] = []
) {
if (options.stopRef?.value) {
throw new Error("Test execution stopped")
@ -270,7 +290,8 @@ export class TestRunnerService extends Service {
const results = await runTestRunnerRequest(
request,
options.keepVariableValues
options.keepVariableValues,
inheritedVariables
)
if (options.stopRef?.value) {

View file

@ -9,6 +9,9 @@ import V6_VERSION from "./v/6"
import V7_VERSION from "./v/7"
import V8_VERSION from "./v/8"
import V9_VERSION from "./v/9"
import V10_VERSION from "./v/10"
export { CollectionVariable } from "./v/10"
import { z } from "zod"
import { translateToNewRequest } from "../rest"
@ -20,7 +23,7 @@ const versionedObject = z.object({
})
export const HoppCollection = createVersionedEntity({
latestVersion: 9,
latestVersion: 10,
versionMap: {
1: V1_VERSION,
2: V2_VERSION,
@ -31,6 +34,7 @@ export const HoppCollection = createVersionedEntity({
7: V7_VERSION,
8: V8_VERSION,
9: V9_VERSION,
10: V10_VERSION,
},
getVersion(data) {
const versionCheck = versionedObject.safeParse(data)
@ -46,7 +50,11 @@ export const HoppCollection = createVersionedEntity({
export type HoppCollection = InferredEntity<typeof HoppCollection>
export const CollectionSchemaVersion = 9
export type HoppCollectionVariable = InferredEntity<
typeof HoppCollection
>["variables"][number]
export const CollectionSchemaVersion = 10
/**
* Generates a Collection object. This ignores the version number object
@ -74,6 +82,7 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
const auth = x.auth ?? { authType: "inherit", authActive: true }
const headers = x.headers ?? []
const variables = x.variables ?? []
const obj = makeCollection({
name,
@ -81,6 +90,7 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
requests,
auth,
headers,
variables,
})
if (x.id) obj.id = x.id
@ -102,6 +112,7 @@ export function translateToNewGQLCollection(x: any): HoppCollection {
const auth = x.auth ?? { authType: "inherit", authActive: true }
const headers = x.headers ?? []
const variables = x.variables ?? []
const obj = makeCollection({
name,
@ -109,6 +120,7 @@ export function translateToNewGQLCollection(x: any): HoppCollection {
requests,
auth,
headers,
variables,
})
if (x.id) obj.id = x.id

View file

@ -0,0 +1,54 @@
import { defineVersion, entityRefUptoVersion } from "verzod"
import { z } from "zod"
import { HoppCollection } from ".."
import { v9_baseCollectionSchema } from "./9"
export const CollectionVariable = z.object({
key: z.string(),
initialValue: z.string(),
currentValue: z.string(),
secret: z.boolean(),
})
export type CollectionVariable = z.infer<typeof CollectionVariable>
export const v10_baseCollectionSchema = v9_baseCollectionSchema.extend({
v: z.literal(10),
variables: z.array(CollectionVariable),
})
type Input = z.input<typeof v10_baseCollectionSchema> & {
folders: Input[]
}
type Output = z.output<typeof v10_baseCollectionSchema> & {
folders: Output[]
}
export const V10_SCHEMA = v10_baseCollectionSchema.extend({
folders: z.lazy(() => z.array(entityRefUptoVersion(HoppCollection, 10))),
}) as z.ZodType<Output, z.ZodTypeDef, Input>
export default defineVersion({
initial: false,
schema: V10_SCHEMA,
up(old: z.infer<typeof V10_SCHEMA>) {
const result: z.infer<typeof V10_SCHEMA> = {
...old,
v: 10 as const,
variables: [],
folders: old.folders.map((folder) => {
const result = HoppCollection.safeParseUpToVersion(folder, 10)
if (result.type !== "ok") {
throw new Error("Failed to migrate child collections")
}
return result.value
}),
}
return result
},
})

View file

@ -2,7 +2,6 @@ import { z } from "zod"
import { defineVersion } from "verzod"
import { V1_SCHEMA } from "./1"
// add initialValue and currentValue to the schema and delete value and add it to initialValue and currentValue
export const V2_SCHEMA = V1_SCHEMA.extend({
v: z.literal(2),
variables: z.array(

View file

@ -1,11 +1,14 @@
mutation CreateGQLChildUserCollection(
$title: String!
$parentUserCollectionID: ID!
$data: String
) {
createGQLChildUserCollection(
title: $title
parentUserCollectionID: $parentUserCollectionID
data: $data
) {
id
data
}
}

View file

@ -1,5 +1,6 @@
mutation CreateGQLRootUserCollection($title: String!) {
createGQLRootUserCollection(title: $title) {
mutation CreateGQLRootUserCollection($title: String!, $data: String) {
createGQLRootUserCollection(title: $title, data: $data) {
id
data
}
}

View file

@ -1,11 +1,14 @@
mutation CreateRESTChildUserCollection(
$title: String!
$parentUserCollectionID: ID!
$data: String
) {
createRESTChildUserCollection(
title: $title
parentUserCollectionID: $parentUserCollectionID
data: $data
) {
id
data
}
}

View file

@ -1,5 +1,6 @@
mutation CreateRESTRootUserCollection($title: String!) {
createRESTRootUserCollection(title: $title) {
mutation CreateRESTRootUserCollection($title: String!, $data: String) {
createRESTRootUserCollection(title: $title, data: $data) {
id
data
}
}

View file

@ -0,0 +1,15 @@
mutation UpdateUserCollection(
$userCollectionID: ID!
$newTitle: String
$data: String
) {
updateUserCollection(
userCollectionID: $userCollectionID
newTitle: $newTitle
data: $data
) {
id
title
data
}
}

View file

@ -53,6 +53,9 @@ import {
UpdateRestUserRequestDocument,
UpdateRestUserRequestMutation,
UpdateRestUserRequestMutationVariables,
UpdateUserCollectionDocument,
UpdateUserCollectionMutation,
UpdateUserCollectionMutationVariables,
UpdateUserCollectionOrderDocument,
UpdateUserCollectionOrderMutation,
UpdateUserCollectionOrderMutationVariables,
@ -68,22 +71,24 @@ import {
UserRequestUpdatedDocument,
} from "../../api/generated/graphql"
export const createRESTRootUserCollection = (title: string) =>
export const createRESTRootUserCollection = (title: string, data?: string) =>
runMutation<
CreateRestRootUserCollectionMutation,
CreateRestRootUserCollectionMutationVariables,
""
>(CreateRestRootUserCollectionDocument, {
title,
data,
})()
export const createGQLRootUserCollection = (title: string) =>
export const createGQLRootUserCollection = (title: string, data?: string) =>
runMutation<
CreateGqlRootUserCollectionMutation,
CreateGqlRootUserCollectionMutationVariables,
""
>(CreateGqlRootUserCollectionDocument, {
title,
data,
})()
export const createRESTUserRequest = (
@ -118,7 +123,8 @@ export const createGQLUserRequest = (
export const createRESTChildUserCollection = (
title: string,
parentUserCollectionID: string
parentUserCollectionID: string,
data?: string
) =>
runMutation<
CreateRestChildUserCollectionMutation,
@ -127,11 +133,13 @@ export const createRESTChildUserCollection = (
>(CreateRestChildUserCollectionDocument, {
title,
parentUserCollectionID,
data,
})()
export const createGQLChildUserCollection = (
title: string,
parentUserCollectionID: string
parentUserCollectionID: string,
data?: string
) =>
runMutation<
CreateGqlChildUserCollectionMutation,
@ -140,6 +148,7 @@ export const createGQLChildUserCollection = (
>(CreateGqlChildUserCollectionDocument, {
title,
parentUserCollectionID,
data,
})()
export const deleteUserCollection = (userCollectionID: string) =>
@ -161,6 +170,17 @@ export const renameUserCollection = (
""
>(RenameUserCollectionDocument, { userCollectionID, newTitle })()
export const updateUserCollection = (
userCollectionID: string,
newTitle?: string,
data?: string
) =>
runMutation<
UpdateUserCollectionMutation,
UpdateUserCollectionMutationVariables,
""
>(UpdateUserCollectionDocument, { userCollectionID, newTitle, data })()
export const moveUserCollection = (
sourceCollectionID: string,
destinationCollectionID?: string

View file

@ -130,11 +130,12 @@ function exportedCollectionToHoppCollection(
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
return {
id: restCollection.id,
v: 9,
v: 10,
name: restCollection.name,
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -182,6 +183,7 @@ function exportedCollectionToHoppCollection(
}),
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
}
} else {
const gqlCollection = collection as ExportedUserCollectionGQL
@ -192,11 +194,12 @@ function exportedCollectionToHoppCollection(
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
return {
id: gqlCollection.id,
v: 9,
v: 10,
name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -224,6 +227,7 @@ function exportedCollectionToHoppCollection(
}),
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
}
}
}
@ -366,6 +370,7 @@ function setupUserCollectionCreatedSubscription() {
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
runDispatchWithOutSyncing(() => {
@ -374,17 +379,19 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 9,
v: 10,
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
})
: addRESTCollection({
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 9,
v: 10,
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
})
const localIndex = collectionStore.value.state.length - 1
@ -587,12 +594,13 @@ function setupUserCollectionDuplicatedSubscription() {
)
// Incoming data transformed to the respective internal representations
const { auth, headers } =
const { auth, headers, variables } =
data && data != "null"
? JSON.parse(data)
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
const folders = transformDuplicatedCollections(childCollectionsJSONStr)
@ -607,9 +615,10 @@ function setupUserCollectionDuplicatedSubscription() {
name,
folders,
requests,
v: 9,
v: 10,
auth,
headers: addDescriptionField(headers),
variables: variables ?? [],
}
// only folders will have parent collection id
@ -1023,10 +1032,14 @@ function transformDuplicatedCollections(
requests: userRequests,
title: name,
}) => {
const { auth, headers } =
const { auth, headers, variables } =
data && data !== "null"
? JSON.parse(data)
: { auth: { authType: "inherit", authActive: true }, headers: [] }
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
const folders = transformDuplicatedCollections(childCollectionsJSONStr)
@ -1037,9 +1050,10 @@ function transformDuplicatedCollections(
name,
folders,
requests,
v: 9,
v: 10,
auth,
headers: addDescriptionField(headers),
variables: variables ?? [],
}
}
)

View file

@ -9,7 +9,11 @@ import {
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import {
generateUniqueRefId,
HoppCollection,
HoppRESTRequest,
} from "@hoppscotch/data"
import { getSyncInitFunction } from "../../lib/sync"
@ -25,6 +29,7 @@ import {
moveUserCollection,
moveUserRequest,
renameUserCollection,
updateUserCollection,
updateUserCollectionOrder,
} from "./collections.api"
@ -47,27 +52,84 @@ const recursivelySyncCollections = async (
// if parentUserCollectionID does not exist, create the collection as a root collection
if (!parentUserCollectionID) {
const res = await createRESTRootUserCollection(collection.name)
const data = {
auth: collection.auth ?? {
authType: "inherit",
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
const res = await createRESTRootUserCollection(
collection.name,
JSON.stringify(data)
)
if (E.isRight(res)) {
parentCollectionID = res.right.createRESTRootUserCollection.id
const returnedData = res.right.createRESTRootUserCollection.data
? JSON.parse(res.right.createRESTRootUserCollection.data)
: {
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
collection.id = parentCollectionID
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
collection.auth = returnedData.auth
collection.headers = returnedData.headers
collection.variables = returnedData.variables
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
} else {
parentCollectionID = undefined
}
} else {
// if parentUserCollectionID exists, create the collection as a child collection
const data = {
auth: collection.auth ?? {
authType: "inherit",
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
const res = await createRESTChildUserCollection(
collection.name,
parentUserCollectionID
parentUserCollectionID,
JSON.stringify(data)
)
if (E.isRight(res)) {
const childCollectionId = res.right.createRESTChildUserCollection.id
const returnedData = res.right.createRESTChildUserCollection.data
? JSON.parse(res.right.createRESTChildUserCollection.data)
: {
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
_ref_id: generateUniqueRefId("coll"),
variables: [],
}
collection.id = childCollectionId
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
collection.auth = returnedData.auth
collection.headers = returnedData.headers
parentCollectionID = childCollectionId
collection.variables = returnedData.variables
removeDuplicateRESTCollectionOrFolder(
childCollectionId,
@ -155,8 +217,15 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
[collectionIndex]
)?.id
if (collectionID && collection.name) {
renameUserCollection(collectionID, collection.name)
const data = {
auth: collection.auth,
headers: collection.headers,
variables: collection.variables,
_ref_id: collection._ref_id,
}
if (collectionID) {
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
}
},
async addFolder({ name, path }) {
@ -195,9 +264,15 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
)?.id
const folderName = folder.name
const data = {
auth: folder.auth,
headers: folder.headers,
variables: folder.variables,
_ref_id: folder._ref_id,
}
if (folderID && folderName) {
renameUserCollection(folderID, folderName)
if (folderID) {
updateUserCollection(folderID, folderName, JSON.stringify(data))
}
},
async removeFolder({ folderID }) {

View file

@ -8,7 +8,11 @@ import {
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import {
generateUniqueRefId,
HoppCollection,
HoppRESTRequest,
} from "@hoppscotch/data"
import { getSyncInitFunction } from "../../lib/sync"
@ -22,6 +26,7 @@ import {
deleteUserRequest,
editGQLUserRequest,
renameUserCollection,
updateUserCollection,
} from "./collections.api"
import * as E from "fp-ts/Either"
@ -44,12 +49,41 @@ const recursivelySyncCollections = async (
// if parentUserCollectionID does not exist, create the collection as a root collection
if (!parentUserCollectionID) {
const res = await createGQLRootUserCollection(collection.name)
const data = {
auth: collection.auth ?? {
authType: "inherit",
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
}
const res = await createGQLRootUserCollection(
collection.name,
JSON.stringify(data)
)
if (E.isRight(res)) {
parentCollectionID = res.right.createGQLRootUserCollection.id
const returnedData = res.right.createGQLRootUserCollection.data
? JSON.parse(res.right.createGQLRootUserCollection.data)
: {
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
collection.id = parentCollectionID
collection.auth = returnedData.auth
collection.headers = returnedData.headers
collection.variables = returnedData.variables
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
removeDuplicateGraphqlCollectionOrFolder(
parentCollectionID,
collectionPath
@ -59,15 +93,44 @@ const recursivelySyncCollections = async (
}
} else {
// if parentUserCollectionID exists, create the collection as a child collection
const data = {
auth: collection.auth ?? {
authType: "inherit",
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
}
const res = await createGQLChildUserCollection(
collection.name,
parentUserCollectionID
parentUserCollectionID,
JSON.stringify(data)
)
if (E.isRight(res)) {
const childCollectionId = res.right.createGQLChildUserCollection.id
const returnedData = res.right.createGQLChildUserCollection.data
? JSON.parse(res.right.createGQLChildUserCollection.data)
: {
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
collection.id = childCollectionId
collection.auth = returnedData.auth
collection.headers = returnedData.headers
parentCollectionID = childCollectionId
collection.variables = returnedData.variables
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
removeDuplicateGraphqlCollectionOrFolder(
childCollectionId,
@ -158,8 +221,15 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
[collectionIndex]
)?.id
if (collectionID && collection.name) {
renameUserCollection(collectionID, collection.name)
const data = {
auth: collection.auth,
headers: collection.headers,
variables: collection.variables,
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
}
if (collectionID) {
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
}
},
async addFolder({ name, path }) {
@ -197,8 +267,15 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
path.split("/").map((index) => parseInt(index))
)?.id
if (folderBackendId && folder.name) {
renameUserCollection(folderBackendId, folder.name)
const data = {
auth: folder.auth,
headers: folder.headers,
variables: folder.variables,
_ref_id: folder._ref_id ?? generateUniqueRefId("coll"),
}
if (folderBackendId) {
updateUserCollection(folderBackendId, folder.name, JSON.stringify(data))
}
},
async removeFolder({ folderID }) {

View file

@ -8,7 +8,11 @@ import {
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import {
generateUniqueRefId,
HoppCollection,
HoppRESTRequest,
} from "@hoppscotch/data"
import { getSyncInitFunction } from "@lib/sync"
@ -52,6 +56,8 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
const res = await createGQLRootUserCollection(
collection.name,
@ -69,11 +75,15 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: [],
variables: [],
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
}
collection.id = parentCollectionID
collection.auth = returnedData.auth
collection.headers = returnedData.headers
collection.variables = returnedData.variables
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
removeDuplicateGraphqlCollectionOrFolder(
parentCollectionID,
@ -91,6 +101,8 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
const res = await createGQLChildUserCollection(
@ -110,12 +122,16 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: [],
variables: [],
_ref_id: collection._ref_id ?? generateUniqueRefId("coll"),
}
collection.id = childCollectionId
collection.auth = returnedData.auth
collection.headers = returnedData.headers
parentCollectionID = childCollectionId
collection.variables = returnedData.variables
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
removeDuplicateGraphqlCollectionOrFolder(
childCollectionId,
@ -209,6 +225,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
const data = {
auth: collection.auth,
headers: collection.headers,
variables: collection.variables,
_ref_id: collection._ref_id,
}
if (collectionID) {
@ -253,6 +271,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
const data = {
auth: folder.auth,
headers: folder.headers,
variables: folder.variables,
_ref_id: folder._ref_id,
}
if (folderBackendId) {

View file

@ -135,12 +135,13 @@ function exportedCollectionToHoppCollection(
auth: { authType: "inherit", authActive: false },
headers: [],
_ref_id: generateUniqueRefId("coll"),
variables: [],
}
return {
id: restCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 9,
v: 10,
name: restCollection.name,
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -188,6 +189,7 @@ function exportedCollectionToHoppCollection(
}),
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
}
} else {
const gqlCollection = collection as ExportedUserCollectionGQL
@ -199,12 +201,13 @@ function exportedCollectionToHoppCollection(
auth: { authType: "inherit", authActive: true },
headers: [],
_ref_id: generateUniqueRefId("coll"),
variables: [],
}
return {
id: gqlCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 9,
v: 10,
name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -232,6 +235,7 @@ function exportedCollectionToHoppCollection(
}),
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
}
}
}
@ -375,6 +379,7 @@ function setupUserCollectionCreatedSubscription() {
auth: { authType: "inherit", authActive: true },
headers: [],
_ref_id: generateUniqueRefId("coll"),
variables: [],
}
runDispatchWithOutSyncing(() => {
@ -383,19 +388,21 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 9,
v: 10,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
})
: addRESTCollection({
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 9,
v: 10,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
})
const localIndex = collectionStore.value.state.length - 1
@ -598,12 +605,13 @@ function setupUserCollectionDuplicatedSubscription() {
)
// Incoming data transformed to the respective internal representations
const { auth, headers } =
const { auth, headers, variables } =
data && data != "null"
? JSON.parse(data)
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
// Duplicated collection will have a unique ref id
const _ref_id = generateUniqueRefId("coll")
@ -620,10 +628,11 @@ function setupUserCollectionDuplicatedSubscription() {
name,
folders,
requests,
v: 9,
v: 10,
_ref_id,
auth,
headers: addDescriptionField(headers),
variables: variables ?? [],
}
// only folders will have parent collection id
@ -1037,10 +1046,14 @@ function transformDuplicatedCollections(
requests: userRequests,
title: name,
}) => {
const { auth, headers } =
const { auth, headers, variables } =
data && data !== "null"
? JSON.parse(data)
: { auth: { authType: "inherit", authActive: true }, headers: [] }
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
const _ref_id = generateUniqueRefId("coll")
@ -1054,9 +1067,10 @@ function transformDuplicatedCollections(
folders,
requests,
_ref_id,
v: 9,
v: 10,
auth,
headers: addDescriptionField(headers),
variables: variables ?? [],
}
}
)

View file

@ -57,6 +57,7 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
const res = await createRESTRootUserCollection(
@ -74,6 +75,7 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
@ -81,6 +83,7 @@ const recursivelySyncCollections = async (
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
collection.auth = returnedData.auth
collection.headers = returnedData.headers
collection.variables = returnedData.variables
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
} else {
parentCollectionID = undefined
@ -93,6 +96,7 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
@ -113,6 +117,7 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
@ -121,6 +126,7 @@ const recursivelySyncCollections = async (
collection.auth = returnedData.auth
collection.headers = returnedData.headers
parentCollectionID = childCollectionId
collection.variables = returnedData.variables
removeDuplicateRESTCollectionOrFolder(
childCollectionId,
@ -211,6 +217,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
const data = {
auth: collection.auth,
headers: collection.headers,
variables: collection.variables,
_ref_id: collection._ref_id,
}
if (collectionID) {
@ -256,6 +264,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
const data = {
auth: folder.auth,
headers: folder.headers,
variables: folder.variables,
_ref_id: folder._ref_id,
}
if (folderID) {
updateUserCollection(folderID, folderName, JSON.stringify(data))

View file

@ -8,7 +8,11 @@ import {
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import {
generateUniqueRefId,
HoppCollection,
HoppRESTRequest,
} from "@hoppscotch/data"
import { getSyncInitFunction } from "@lib/sync"
@ -52,6 +56,8 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
const res = await createGQLRootUserCollection(
collection.name,
@ -69,11 +75,15 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
collection.id = parentCollectionID
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
collection.auth = returnedData.auth
collection.headers = returnedData.headers
collection.variables = returnedData.variables
removeDuplicateGraphqlCollectionOrFolder(
parentCollectionID,
@ -91,6 +101,8 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
const res = await createGQLChildUserCollection(
@ -110,12 +122,16 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
collection.id = childCollectionId
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
collection.auth = returnedData.auth
collection.headers = returnedData.headers
parentCollectionID = childCollectionId
collection.variables = returnedData.variables
removeDuplicateGraphqlCollectionOrFolder(
childCollectionId,
@ -209,6 +225,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
const data = {
auth: collection.auth,
headers: collection.headers,
variables: collection.variables,
_ref_id: collection._ref_id,
}
if (collectionID) {
@ -253,6 +271,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
const data = {
auth: folder.auth,
headers: folder.headers,
variables: folder.variables,
_ref_id: folder._ref_id,
}
if (folderBackendId) {

View file

@ -135,12 +135,13 @@ function exportedCollectionToHoppCollection(
auth: { authType: "inherit", authActive: true },
headers: [],
_ref_id: generateUniqueRefId("coll"),
variables: [],
}
return {
id: restCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 9,
v: 10,
name: restCollection.name,
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -188,6 +189,7 @@ function exportedCollectionToHoppCollection(
}),
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
}
} else {
const gqlCollection = collection as ExportedUserCollectionGQL
@ -199,12 +201,13 @@ function exportedCollectionToHoppCollection(
auth: { authType: "inherit", authActive: true },
headers: [],
_ref_id: generateUniqueRefId("coll"),
variables: [],
}
return {
id: gqlCollection.id,
_ref_id: data._ref_id ?? generateUniqueRefId("coll"),
v: 9,
v: 10,
name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
@ -232,6 +235,7 @@ function exportedCollectionToHoppCollection(
}),
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
}
}
}
@ -375,6 +379,7 @@ function setupUserCollectionCreatedSubscription() {
auth: { authType: "inherit", authActive: true },
headers: [],
_ref_id: generateUniqueRefId("coll"),
variables: [],
}
runDispatchWithOutSyncing(() => {
@ -383,19 +388,21 @@ function setupUserCollectionCreatedSubscription() {
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 9,
v: 10,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
})
: addRESTCollection({
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 9,
v: 10,
_ref_id: data._ref_id,
auth: data.auth,
headers: addDescriptionField(data.headers),
variables: data.variables ?? [],
})
const localIndex = collectionStore.value.state.length - 1
@ -598,12 +605,13 @@ function setupUserCollectionDuplicatedSubscription() {
)
// Incoming data transformed to the respective internal representations
const { auth, headers } =
const { auth, headers, variables } =
data && data != "null"
? JSON.parse(data)
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
// Duplicated collection will have a unique ref id
const _ref_id = generateUniqueRefId("coll")
@ -620,10 +628,11 @@ function setupUserCollectionDuplicatedSubscription() {
name,
folders,
requests,
v: 9,
v: 10,
_ref_id,
auth,
headers: addDescriptionField(headers),
variables: variables ?? [],
}
// only folders will have parent collection id
@ -1037,10 +1046,14 @@ function transformDuplicatedCollections(
requests: userRequests,
title: name,
}) => {
const { auth, headers } =
const { auth, headers, variables } =
data && data !== "null"
? JSON.parse(data)
: { auth: { authType: "inherit", authActive: true }, headers: [] }
: {
auth: { authType: "inherit", authActive: true },
headers: [],
variables: [],
}
const _ref_id = generateUniqueRefId("coll")
@ -1054,9 +1067,10 @@ function transformDuplicatedCollections(
folders,
requests,
_ref_id,
v: 9,
v: 10,
auth,
headers: addDescriptionField(headers),
variables: variables ?? [],
}
}
)

View file

@ -57,6 +57,7 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
const res = await createRESTRootUserCollection(
@ -74,6 +75,7 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
@ -81,7 +83,11 @@ const recursivelySyncCollections = async (
collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll")
collection.auth = returnedData.auth
collection.headers = returnedData.headers
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
collection.variables = returnedData.variables
removeDuplicateRESTCollectionOrFolder(
parentCollectionID,
`${collectionPath}`
)
} else {
parentCollectionID = undefined
}
@ -93,6 +99,7 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: collection.headers ?? [],
variables: collection.variables ?? [],
_ref_id: collection._ref_id,
}
@ -113,6 +120,7 @@ const recursivelySyncCollections = async (
authActive: true,
},
headers: [],
variables: [],
_ref_id: generateUniqueRefId("coll"),
}
@ -121,6 +129,7 @@ const recursivelySyncCollections = async (
collection.auth = returnedData.auth
collection.headers = returnedData.headers
parentCollectionID = childCollectionId
collection.variables = returnedData.variables
removeDuplicateRESTCollectionOrFolder(
childCollectionId,
@ -211,6 +220,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
const data = {
auth: collection.auth,
headers: collection.headers,
variables: collection.variables,
_ref_id: collection._ref_id,
}
if (collectionID) {
@ -256,6 +267,8 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
const data = {
auth: folder.auth,
headers: folder.headers,
variables: folder.variables,
_ref_id: folder._ref_id,
}
if (folderID) {
updateUserCollection(folderID, folderName, JSON.stringify(data))