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, "v": 6,
"id": "cm9wmuzj46s3imbs891pdamv4", "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": [ "folders": [
{ {
"v": 6, "v": 6,
@ -688,5 +688,13 @@
"description": "" "description": ""
} }
], ],
"variables": [
{
"key": "collection-variable",
"currentValue": "collection-variable-value",
"initialValue": "collection-variable-value",
"secret": false
}
],
"_ref_id": "coll_m9wn4jl9_aa8a3bc2-a96f-4cac-86f3-2df4bb355cc8" "_ref_id": "coll_m9wn4jl9_aa8a3bc2-a96f-4cac-86f3-2df4bb355cc8"
} }

View file

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

View file

@ -11,29 +11,29 @@ import {
WorkspaceEnvironment, WorkspaceEnvironment,
} from "../../../utils/workspace-access"; } 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", 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", title: "CollectionA",
parentID: null, parentID: null,
folders: [ folders: [
{ {
id: "clx1ldkzs005v10f86b9wx4yc", id: "clx1ldkzs005v10f86b9wx4yc",
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[]}', data: '{"auth":{"authType":"inherit","authActive":true},"headers":[],"variables":[]}',
title: "FolderA", title: "FolderA",
parentID: "clx1ldkzs005t10f8rp5u60q7", parentID: "clx1ldkzs005t10f8rp5u60q7",
folders: [ folders: [
{ {
id: "clx1ldkzt005x10f8i0u5lzgj", 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", title: "FolderB",
parentID: "clx1ldkzs005v10f86b9wx4yc", parentID: "clx1ldkzs005v10f86b9wx4yc",
folders: [ folders: [
{ {
id: "clx1ldkzu005z10f880zx17bg", id: "clx1ldkzu005z10f880zx17bg",
data: '{"auth":{"authType":"inherit","authActive":true},"headers":[]}', data: '{"auth":{"authType":"inherit","authActive":true},"headers":[],"variables":[]}',
title: "FolderC", title: "FolderC",
parentID: "clx1ldkzt005x10f8i0u5lzgj", parentID: "clx1ldkzt005x10f8i0u5lzgj",
folders: [], 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, v: CollectionSchemaVersion,
@ -142,6 +142,7 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
authActive: true, authActive: true,
}, },
headers: [], headers: [],
variables: [],
}, },
], ],
requests: [ requests: [
@ -181,6 +182,14 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
], ],
requests: [ requests: [
@ -211,6 +220,7 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
authActive: true, authActive: true,
}, },
headers: [], headers: [],
variables: [],
}, },
], ],
requests: [ requests: [
@ -250,27 +260,35 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC
description: "", 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", 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: 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, parentID: null,
folders: [ folders: [
{ {
id: "clx1fjgah000110f8a5bs68gd", 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", title: "folder-1",
parentID: "clx1f86hv000010f8szcfya0t", parentID: "clx1f86hv000010f8szcfya0t",
folders: [ folders: [
{ {
id: "clx1fjwmm000410f8l1gkkr1a", 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", title: "folder-11",
parentID: "clx1fjgah000110f8a5bs68gd", parentID: "clx1fjgah000110f8a5bs68gd",
folders: [], folders: [],
@ -287,7 +305,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
}, },
{ {
id: "clx1fjyxm000510f8pv90dt43", 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", title: "folder-12",
parentID: "clx1fjgah000110f8a5bs68gd", parentID: "clx1fjgah000110f8a5bs68gd",
folders: [], folders: [],
@ -304,7 +322,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
}, },
{ {
id: "clx1fk1cv000610f88kc3aupy", 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", title: "folder-13",
parentID: "clx1fjgah000110f8a5bs68gd", parentID: "clx1fjgah000110f8a5bs68gd",
folders: [], folders: [],
@ -333,13 +351,13 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
}, },
{ {
id: "clx1fjk9o000210f8j0573pls", 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", title: "folder-2",
parentID: "clx1f86hv000010f8szcfya0t", parentID: "clx1f86hv000010f8szcfya0t",
folders: [ folders: [
{ {
id: "clx1fk516000710f87sfpw6bo", 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", title: "folder-21",
parentID: "clx1fjk9o000210f8j0573pls", parentID: "clx1fjk9o000210f8j0573pls",
folders: [], folders: [],
@ -356,7 +374,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
}, },
{ {
id: "clx1fk72t000810f8gfwkpi5y", 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", title: "folder-22",
parentID: "clx1fjk9o000210f8j0573pls", parentID: "clx1fjk9o000210f8j0573pls",
folders: [], folders: [],
@ -373,7 +391,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
}, },
{ {
id: "clx1fk95g000910f8bunhaoo8", 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", title: "folder-23",
parentID: "clx1fjk9o000210f8j0573pls", parentID: "clx1fjk9o000210f8j0573pls",
folders: [], folders: [],
@ -402,13 +420,13 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
}, },
{ {
id: "clx1fjmlq000310f86o4d3w2o", 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", title: "folder-3",
parentID: "clx1f86hv000010f8szcfya0t", parentID: "clx1f86hv000010f8szcfya0t",
folders: [ folders: [
{ {
id: "clx1iwq0p003e10f8u8zg0p85", 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", title: "folder-31",
parentID: "clx1fjmlq000310f86o4d3w2o", parentID: "clx1fjmlq000310f86o4d3w2o",
folders: [], folders: [],
@ -425,7 +443,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
}, },
{ {
id: "clx1izut7003m10f894ip59zg", 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", title: "folder-32",
parentID: "clx1fjmlq000310f86o4d3w2o", parentID: "clx1fjmlq000310f86o4d3w2o",
folders: [], folders: [],
@ -442,7 +460,7 @@ export const WORKSPACE_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Worksp
}, },
{ {
id: "clx1j2ka9003q10f8cdbzpgpg", 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", title: "folder-33",
parentID: "clx1fjmlq000310f86o4d3w2o", parentID: "clx1fjmlq000310f86o4d3w2o",
folders: [], 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[] = export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppCollection[] =
[ [
{ {
v: 9, v: 10,
id: "clx1f86hv000010f8szcfya0t", 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: [ folders: [
{ {
v: 9, v: 10,
id: "clx1fjgah000110f8a5bs68gd", id: "clx1fjgah000110f8a5bs68gd",
name: "folder-1", name: "folder-1",
folders: [ folders: [
{ {
v: 9, v: 10,
id: "clx1fjwmm000410f8l1gkkr1a", id: "clx1fjwmm000410f8l1gkkr1a",
name: "folder-11", name: "folder-11",
folders: [], folders: [],
@ -535,9 +553,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: 9, v: 10,
id: "clx1fjyxm000510f8pv90dt43", id: "clx1fjyxm000510f8pv90dt43",
name: "folder-12", name: "folder-12",
folders: [], folders: [],
@ -593,9 +619,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: 9, v: 10,
id: "clx1fk1cv000610f88kc3aupy", id: "clx1fk1cv000610f88kc3aupy",
name: "folder-13", name: "folder-13",
folders: [], folders: [],
@ -669,6 +703,14 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
], ],
requests: [ requests: [
@ -705,14 +747,22 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: 9, v: 10,
id: "clx1fjk9o000210f8j0573pls", id: "clx1fjk9o000210f8j0573pls",
name: "folder-2", name: "folder-2",
folders: [ folders: [
{ {
v: 9, v: 10,
id: "clx1fk516000710f87sfpw6bo", id: "clx1fk516000710f87sfpw6bo",
name: "folder-21", name: "folder-21",
folders: [], folders: [],
@ -750,9 +800,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: 9, v: 10,
id: "clx1fk72t000810f8gfwkpi5y", id: "clx1fk72t000810f8gfwkpi5y",
name: "folder-22", name: "folder-22",
folders: [], folders: [],
@ -808,9 +866,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: 9, v: 10,
id: "clx1fk95g000910f8bunhaoo8", id: "clx1fk95g000910f8bunhaoo8",
name: "folder-23", name: "folder-23",
folders: [], folders: [],
@ -871,6 +937,14 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
], ],
requests: [ requests: [
@ -913,14 +987,23 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: 9, v: 10,
id: "clx1fjmlq000310f86o4d3w2o", id: "clx1fjmlq000310f86o4d3w2o",
name: "folder-3", name: "folder-3",
folders: [ folders: [
{ {
v: 9, v: 10,
id: "clx1iwq0p003e10f8u8zg0p85", id: "clx1iwq0p003e10f8u8zg0p85",
name: "folder-31", name: "folder-31",
folders: [], folders: [],
@ -958,9 +1041,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: 9, v: 10,
id: "clx1izut7003m10f894ip59zg", id: "clx1izut7003m10f894ip59zg",
name: "folder-32", name: "folder-32",
folders: [], folders: [],
@ -1016,9 +1107,17 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: 9, v: 10,
id: "clx1j2ka9003q10f8cdbzpgpg", id: "clx1j2ka9003q10f8cdbzpgpg",
name: "folder-33", name: "folder-33",
folders: [], folders: [],
@ -1079,6 +1178,14 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
], ],
requests: [ requests: [
@ -1134,6 +1241,14 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
], ],
requests: [ requests: [
@ -1179,16 +1294,24 @@ export const TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Hopp
description: "", 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 // 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", id: "clx1kxvao005m10f8luqivrf1",
data: null, data: null,
title: "Collection with no authorization/headers set", title: "Collection with no authorization/headers/variables set",
parentID: null, parentID: null,
folders: [ folders: [
{ {
@ -1210,7 +1333,7 @@ export const WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK:
}, },
{ {
id: "clx1kym98005o10f8qg17t9o2", 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", title: "folder-2",
parentID: "clx1kxvao005m10f8luqivrf1", parentID: "clx1kxvao005m10f8luqivrf1",
folders: [], folders: [],
@ -1235,7 +1358,7 @@ export const WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK:
}, },
{ {
id: "clx1l2eaz005s10f8loetbbeb", 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", title: "folder-4",
parentID: "clx1kxvao005m10f8luqivrf1", parentID: "clx1kxvao005m10f8luqivrf1",
folders: [], 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, v: CollectionSchemaVersion,
id: "clx1kxvao005m10f8luqivrf1", id: "clx1kxvao005m10f8luqivrf1",
name: "Collection with no authorization/headers set", name: "Collection with no authorization/headers/variables set",
folders: [ folders: [
{ {
v: CollectionSchemaVersion, v: CollectionSchemaVersion,
@ -1284,6 +1407,7 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
authActive: true, authActive: true,
}, },
headers: [], headers: [],
variables: [],
}, },
{ {
v: CollectionSchemaVersion, v: CollectionSchemaVersion,
@ -1323,6 +1447,14 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
{ {
v: CollectionSchemaVersion, v: CollectionSchemaVersion,
@ -1335,6 +1467,7 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
authActive: true, authActive: true,
}, },
headers: [], headers: [],
variables: [],
}, },
{ {
v: CollectionSchemaVersion, v: CollectionSchemaVersion,
@ -1354,6 +1487,14 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
description: "", description: "",
}, },
], ],
variables: [
{
key: "collection-variable",
currentValue: "collection-variable-value",
initialValue: "collection-variable-value",
secret: false,
},
],
}, },
], ],
requests: [], requests: [],
@ -1362,6 +1503,7 @@ export const TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK
authActive: true, authActive: true,
}, },
headers: [], headers: [],
variables: [],
}, },
]; ];

View file

@ -5,18 +5,18 @@ import {
transformWorkspaceEnvironment, transformWorkspaceEnvironment,
} from "../../utils/workspace-access"; } from "../../utils/workspace-access";
import { import {
TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK, TRANSFORMED_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK,
TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK, TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK,
TRANSFORMED_ENVIRONMENT_V0_FORMAT_MOCK, TRANSFORMED_ENVIRONMENT_V0_FORMAT_MOCK,
TRANSFORMED_ENVIRONMENT_V1_FORMAT_MOCK, TRANSFORMED_ENVIRONMENT_V1_FORMAT_MOCK,
TRANSFORMED_ENVIRONMENT_V2_FORMAT_MOCK, TRANSFORMED_ENVIRONMENT_V2_FORMAT_MOCK,
TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK, TRANSFORMED_MULTIPLE_CHILD_COLLECTIONS_WITH_AUTH_HEADERS_MOCK,
WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK, WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK,
WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK, WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_VARIABLES_MOCK,
WORKSPACE_ENVIRONMENT_V0_FORMAT_MOCK, WORKSPACE_ENVIRONMENT_V0_FORMAT_MOCK,
WORKSPACE_ENVIRONMENT_V1_FORMAT_MOCK, WORKSPACE_ENVIRONMENT_V1_FORMAT_MOCK,
WORKSPACE_ENVIRONMENT_V2_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"; } from "./fixtures/workspace-access.mock";
describe("workspace-access", () => { 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", () => { test("Successfully transforms collection data with deeply nested collections and authorization/headers set at each level to the `HoppCollection` format", () => {
expect( expect(
transformWorkspaceCollections( 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", () => { test("Successfully transforms collection data with multiple child collections and authorization/headers set at each level to the `HoppCollection` format", () => {
expect( expect(
transformWorkspaceCollections( 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); ).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", () => { test("Adds the default value for `auth` & `header` fields while transforming collections without authorization/headers set at certain levels", () => {
expect( expect(
transformWorkspaceCollections( transformWorkspaceCollections(
WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_AT_CERTAIN_LEVELS_MOCK WORKSPACE_COLLECTIONS_WITHOUT_AUTH_HEADERS_VARIABLES_AT_CERTAIN_LEVELS_MOCK
) )
).toEqual( ).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 { z } from "zod";
import { TestReport } from "../interfaces/response"; import { TestReport } from "../interfaces/response";
@ -37,5 +42,6 @@ export type ProcessRequestParams = {
envs: HoppEnvs; envs: HoppEnvs;
path: string; path: string;
delay: number; delay: number;
legacySandbox: boolean; legacySandbox?: boolean;
collectionVariables?: HoppCollectionVariable[];
}; };

View file

@ -115,12 +115,18 @@ const processCollection = async (
for (const request of collection.requests) { for (const request of collection.requests) {
const _request = preProcessRequest(request as HoppRESTRequest, collection); const _request = preProcessRequest(request as HoppRESTRequest, collection);
const requestPath = `${path}/${_request.name}`; const requestPath = `${path}/${_request.name}`;
const collectionVariables = collection.variables.filter(
(variable) => variable.key && variable.key.trim() !== ""
);
const processRequestParams: ProcessRequestParams = { const processRequestParams: ProcessRequestParams = {
path: requestPath, path: requestPath,
request: _request, request: _request,
envs, envs,
delay, delay,
legacySandbox, legacySandbox,
collectionVariables,
}; };
// Request processing initiated message. // Request processing initiated message.
@ -161,6 +167,20 @@ const processCollection = async (
updatedFolder.headers.push(...filteredHeaders); 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( await processCollection(
updatedFolder, updatedFolder,
`${path}/${updatedFolder.name}`, `${path}/${updatedFolder.name}`,

View file

@ -1,5 +1,6 @@
import { import {
EnvironmentVariable, EnvironmentVariable,
HoppCollectionVariable,
HoppRESTHeader, HoppRESTHeader,
HoppRESTParam, HoppRESTParam,
HoppRESTRequestVariables, HoppRESTRequestVariables,
@ -269,11 +270,13 @@ export const getResourceContents = async (
* *
* @param {HoppRESTRequestVariables} requestVariables - Incoming request variables. * @param {HoppRESTRequestVariables} requestVariables - Incoming request variables.
* @param {EnvironmentVariable[]} environmentVariables - Incoming environment 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. * @returns {EnvironmentVariable[]} The resolved list of variables that conforms to the shape of environment variables.
*/ */
export const getResolvedVariables = ( export const getResolvedVariables = (
requestVariables: HoppRESTRequestVariables, requestVariables: HoppRESTRequestVariables,
environmentVariables: EnvironmentVariable[] environmentVariables: EnvironmentVariable[],
collectionVariables: HoppCollectionVariable[] = []
): EnvironmentVariable[] => { ): EnvironmentVariable[] => {
// Transforming request variables to the shape of environment variables // Transforming request variables to the shape of environment variables
const activeRequestVariables = requestVariables const activeRequestVariables = requestVariables
@ -287,11 +290,21 @@ export const getResolvedVariables = (
const requestVariableKeys = activeRequestVariables.map(({ key }) => key); const requestVariableKeys = activeRequestVariables.map(({ key }) => key);
// Request variables have higher priority, hence filtering out environment variables with the same keys // Request variables have higher priority, hence filtering out collection variables with the same keys
const filteredEnvironmentVariables = environmentVariables.filter( const filteredCollectionVariables = collectionVariables.filter(
({ key }) => !requestVariableKeys.includes(key) ({ 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 // Setting currentValue to initialValue for environment variables
// because the exported file might not have the currentValue field // because the exported file might not have the currentValue field
const processedEnvironmentVariables = filteredEnvironmentVariables.map( 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, parseTemplateString,
parseTemplateStringE, parseTemplateStringE,
generateJWTToken, generateJWTToken,
HoppCollectionVariable,
} from "@hoppscotch/data"; } from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node"; import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array"; import * as A from "fp-ts/Array";
@ -46,7 +47,8 @@ import { calculateHawkHeader } from "@hoppscotch/data";
export const preRequestScriptRunner = ( export const preRequestScriptRunner = (
request: HoppRESTRequest, request: HoppRESTRequest,
envs: HoppEnvs, envs: HoppEnvs,
legacySandbox: boolean legacySandbox: boolean,
collectionVariables?: HoppCollectionVariable[]
): TE.TaskEither< ): TE.TaskEither<
HoppCLIError, HoppCLIError,
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs } { effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
@ -67,7 +69,7 @@ export const preRequestScriptRunner = (
), ),
TE.chainW((env) => TE.chainW((env) =>
TE.tryCatch( TE.tryCatch(
() => getEffectiveRESTRequest(request, env), () => getEffectiveRESTRequest(request, env, collectionVariables),
(reason) => error({ code: "PRE_REQUEST_SCRIPT_ERROR", data: reason }) (reason) => error({ code: "PRE_REQUEST_SCRIPT_ERROR", data: reason })
) )
), ),
@ -93,7 +95,8 @@ export const preRequestScriptRunner = (
*/ */
export async function getEffectiveRESTRequest( export async function getEffectiveRESTRequest(
request: HoppRESTRequest, request: HoppRESTRequest,
environment: Environment environment: Environment,
collectionVariables?: HoppCollectionVariable[]
): Promise< ): Promise<
E.Either< E.Either<
HoppCLIError, HoppCLIError,
@ -104,7 +107,8 @@ export async function getEffectiveRESTRequest(
const resolvedVariables = getResolvedVariables( const resolvedVariables = getResolvedVariables(
request.requestVariables, request.requestVariables,
envVariables envVariables,
collectionVariables
); );
// Parsing final headers with applied ENVs. // Parsing final headers with applied ENVs.

View file

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

View file

@ -3,6 +3,7 @@ import {
Environment, Environment,
EnvironmentSchemaVersion, EnvironmentSchemaVersion,
HoppCollection, HoppCollection,
HoppCollectionVariable,
HoppRESTAuth, HoppRESTAuth,
HoppRESTHeaders, HoppRESTHeaders,
HoppRESTRequest, HoppRESTRequest,
@ -173,12 +174,17 @@ export const transformWorkspaceCollections = (
return collections.map((collection) => { return collections.map((collection) => {
const { id, title, data, requests, folders } = collection; const { id, title, data, requests, folders } = collection;
const parsedData: { auth?: HoppRESTAuth; headers?: HoppRESTHeaders } = data const parsedData: {
? JSON.parse(data) auth?: HoppRESTAuth;
: {}; headers?: HoppRESTHeaders;
variables: HoppCollectionVariable[];
} = data ? JSON.parse(data) : {};
const { auth = { authType: "inherit", authActive: true }, headers = [] } = const {
parsedData; auth = { authType: "inherit", authActive: true },
headers = [],
variables = [],
} = parsedData;
const transformedAuth = transformAuth(auth); const transformedAuth = transformAuth(auth);
@ -186,6 +192,10 @@ export const transformWorkspaceCollections = (
header.description ? header : { ...header, description: "" } 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 // 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 // Any relevant migrations have to be accounted here
// `ref_id` field isn't necessary being applicable only to personal workspace and asociates with syncing // `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), requests: transformWorkspaceRequests(requests),
auth: transformedAuth, auth: transformedAuth,
headers: transformedHeaders, headers: transformedHeaders,
variables: filteredCollectionVariables,
}; };
}); });
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ import {
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql" import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment" 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 { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment" import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"

View file

@ -134,7 +134,7 @@
<input <input
v-model="env.key" v-model="env.key"
v-focus 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', { :placeholder="`${t('count.variable', {
count: index + 1, count: index + 1,
})}`" })}`"
@ -150,6 +150,7 @@
:select-text-on-mount=" :select-text-on-mount="
env.key ? env.key === editingVariableName : false env.key ? env.key === editingVariableName : false
" "
:auto-complete-env="true"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@ -173,6 +174,7 @@
:select-text-on-mount=" :select-text-on-mount="
env.key ? env.key === editingVariableName : false env.key ? env.key === editingVariableName : false
" "
:auto-complete-env="true"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"

View file

@ -138,7 +138,7 @@
<input <input
v-model="env.key" v-model="env.key"
v-focus 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', { :placeholder="`${t('count.variable', {
count: index + 1, count: index + 1,
})}`" })}`"

View file

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

View file

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

View file

@ -62,7 +62,7 @@
v-if="showDescription" v-if="showDescription"
:value="description" :value="description"
:placeholder="t('count.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" type="text"
:class="{ 'opacity-50': !entityActive }" :class="{ 'opacity-50': !entityActive }"
@update:value="emit('update:description', $event.target.value)" @update:value="emit('update:description', $event.target.value)"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -169,7 +169,8 @@ function removeDuplicatesAndKeepLast(arr: HoppInheritedProperty["headers"]) {
export function updateInheritedPropertiesForAffectedRequests( export function updateInheritedPropertiesForAffectedRequests(
path: string, path: string,
inheritedProperties: HoppInheritedProperty, inheritedProperties: HoppInheritedProperty,
type: "rest" | "graphql" type: "rest" | "graphql",
collectionId?: string
) { ) {
const tabService = const tabService =
type === "rest" ? getService(RESTTabService) : getService(GQLTabService) type === "rest" ? getService(RESTTabService) : getService(GQLTabService)
@ -226,6 +227,22 @@ export function updateInheritedPropertiesForAffectedRequests(
tab.value.document.inheritedProperties.headers = mergedHeaders 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 IconUsers from "~icons/lucide/users?raw"
import IconGlobe from "~icons/lucide/globe?raw" import IconGlobe from "~icons/lucide/globe?raw"
import IconVariable from "~icons/lucide/variable?raw" import IconVariable from "~icons/lucide/variable?raw"
import IconLibrary from "~icons/lucide/library?raw"
import { isComment } from "./helpers" import { isComment } from "./helpers"
import { CurrentValueService } from "~/services/current-environment-value.service" 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" "cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
const HOPP_REQUEST_VARIABLE_HIGHLIGHT = "request-variable-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_ENVIRONMENT_HIGHLIGHT = "environment-variable-highlight"
const HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT = "global-variable-highlight" const HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT = "global-variable-highlight"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "environment-not-found-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 isSecret = tooltipEnv?.secret === true
const hasSource = Boolean(tooltipEnv?.sourceEnv) 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( const hasSecretStored = secretEnvironmentService.hasSecretValue(
tooltipEnv?.sourceEnv !== "Global" tooltipSourceEnvID,
? currentSelectedEnvironment.id
: "Global",
tooltipEnv?.key ?? "" tooltipEnv?.key ?? ""
) )
@ -198,7 +210,9 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
? IconVariable ? IconVariable
: selectedEnvType === "TEAM_ENV" : selectedEnvType === "TEAM_ENV"
? IconUsers ? IconUsers
: IconUser : tooltipEnv?.sourceEnv === "CollectionVariable"
? IconLibrary
: IconUser
}</span>` }</span>`
const appendEditAction = (tooltip: HTMLElement) => { 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>` 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 { return {
@ -275,7 +291,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const envContainer = document.createElement("div") const envContainer = document.createElement("div")
tooltipContainer.appendChild(envContainer) tooltipContainer.appendChild(envContainer)
envContainer.className = 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") const initialValueBlock = document.createElement("div")
initialValueBlock.className = "flex items-center space-x-2" initialValueBlock.className = "flex items-center space-x-2"
@ -323,6 +339,8 @@ function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
if (envSource === "RequestVariable") if (envSource === "RequestVariable")
className = HOPP_REQUEST_VARIABLE_HIGHLIGHT 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 === "Global") className = HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT
else if (envSource !== undefined) className = HOPP_ENVIRONMENT_HIGHLIGHT else if (envSource !== undefined) className = HOPP_ENVIRONMENT_HIGHLIGHT
@ -374,56 +392,18 @@ export class HoppEnvironmentPlugin {
private editorView: Ref<EditorView | undefined> private editorView: Ref<EditorView | undefined>
) { ) {
const aggregateEnvs = getAggregateEnvsWithCurrentValue() const aggregateEnvs = getAggregateEnvsWithCurrentValue()
const currentTab = restTabs.currentActiveTab.value
const currentTabRequest = this.envs = [...aggregateEnvs]
currentTab.document.type === "example-response"
? currentTab.document.response.originalRequest
: currentTab.document.request
if (!currentTabRequest) return this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
watch( cursorTooltipField(this.envs),
currentTabRequest, environmentHighlightStyle(this.envs),
(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 ?? []
subscribeToStream(aggregateEnvsWithCurrentValue$, (envs) => { subscribeToStream(aggregateEnvsWithCurrentValue$, (envs) => {
this.envs = [ this.envs = [...envs]
...requestVariables.map(({ key, value }) => ({
key,
currentValue: value,
initialValue: value,
sourceEnv: "RequestVariable",
secret: false,
})),
...envs,
]
this.editorView.value?.dispatch({ this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([ effects: this.compartment.reconfigure([

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import { import {
HoppCollection, HoppCollection,
HoppCollectionVariable,
HoppRESTAuth, HoppRESTAuth,
HoppRESTHeader, HoppRESTHeader,
HoppRESTParam, HoppRESTParam,
@ -14,39 +15,33 @@ import {
import * as A from "fp-ts/Array" import * as A from "fp-ts/Array"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import * as TO from "fp-ts/TaskOption" import * as TO from "fp-ts/TaskOption"
import * as O from "fp-ts/Option"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import { ImportRequest, convert } from "insomnia-importers" import { convert } from "insomnia-importers"
import { Header, Parameter } from "insomnia-importers/dist/src/entities"
import { IMPORTER_INVALID_FILE_FORMAT } from "." import { IMPORTER_INVALID_FILE_FORMAT } from ".."
import { replaceInsomniaTemplating } from "./insomniaEnv" 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 /**
* Used to check if the document is an Insomnia v5 document
type UnwrapPromise<T extends Promise<any>> = * Insomnia v5 documents have a type field that starts with "collection.insomnia.rest/
T extends Promise<infer Y> ? Y : never * @param data InsomniaDoc
* @returns true if the document is an Insomnia v5 document
type InsomniaDoc = UnwrapPromise<ReturnType<typeof convert>> */
type InsomniaResource = ImportRequest const isV5InsomniaDoc = (data: InsomniaDoc) =>
data.type &&
// insomnia-importers v3.6.0 doesn't provide a type for path parameters and they have deprecated the library typeof data.type === "string" &&
type InsomniaPathParameter = { (data.type as string).startsWith("collection.insomnia.rest/5")
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))
const replacePathVarTemplating = (expression: string) => const replacePathVarTemplating = (expression: string) =>
expression.replaceAll(/:([^/]+)/g, "<<$1>>") expression.replaceAll(/:([^/]+)/g, "<<$1>>")
@ -84,24 +79,22 @@ const getRequestsIn = (
) )
) )
/** const getCollectionVariables = (
* The provided type by insomnia-importers, this type corrects it environment: Record<string, string> | undefined,
*/ folderRes?: InsomniaFolderResource
type InsoReqAuth = ): HoppCollectionVariable[] => {
| { type: "basic"; disabled?: boolean; username?: string; password?: string } const env =
| { folderRes && folderRes.environment ? folderRes.environment : environment
type: "oauth2"
disabled?: boolean if (!env) return []
accessTokenUrl?: string
authorizationUrl?: string return Object.entries(env).map(([key, value]) => ({
clientId?: string key: replaceVarTemplating(key),
scope?: string 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,
type: "bearer" }))
disabled?: boolean }
token?: string
}
const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => { const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
if (!req.authentication) return { authType: "none", authActive: true } if (!req.authentication) return { authType: "none", authActive: true }
@ -128,6 +121,9 @@ const getHoppReqAuth = (req: InsomniaRequestResource): HoppRESTAuth => {
token: "", token: "",
isPKCE: false, isPKCE: false,
tokenEndpoint: replaceVarTemplating(auth.accessTokenUrl ?? ""), tokenEndpoint: replaceVarTemplating(auth.accessTokenUrl ?? ""),
authRequestParams: [],
refreshRequestParams: [],
tokenRequestParams: [],
}, },
addTo: "HEADERS", addTo: "HEADERS",
} }
@ -246,6 +242,7 @@ const getHoppFolder = (
requests: getRequestsIn(folderRes, resources).map(getHoppRequest), requests: getRequestsIn(folderRes, resources).map(getHoppRequest),
auth: { authType: "inherit", authActive: true }, auth: { authType: "inherit", authActive: true },
headers: [], headers: [],
variables: getCollectionVariables(undefined, folderRes), // undefined is used to indicate no environment variables for v4 and below
}) })
const getHoppCollections = (docs: InsomniaDoc[]) => { 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( pipe(
fileContents, TO.tryCatch(() => convert(content)),
A.traverse(TO.ApplicativeSeq)(parseInsomniaDoc), TO.map((doc) => getHoppCollections([doc])),
TO.map(getHoppCollections),
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT) 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 TE from "fp-ts/TaskEither"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import { IMPORTER_INVALID_FILE_FORMAT } from "." import { IMPORTER_INVALID_FILE_FORMAT } from ".."
import { z } from "zod" import { z } from "zod"
import { 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, ValidContentTypes,
HoppRESTRequestResponses, HoppRESTRequestResponses,
makeHoppRESTResponseOriginalRequest, makeHoppRESTResponseOriginalRequest,
HoppCollectionVariable,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import * as A from "fp-ts/Array" import * as A from "fp-ts/Array"
import { flow, pipe } from "fp-ts/function" import { flow, pipe } from "fp-ts/function"
@ -28,6 +29,7 @@ import {
RequestAuthDefinition, RequestAuthDefinition,
Variable, Variable,
VariableDefinition, VariableDefinition,
VariableList,
} from "postman-collection" } from "postman-collection"
import { stringArrayJoin } from "~/helpers/functional/array" import { stringArrayJoin } from "~/helpers/functional/array"
import { PMRawLanguage } from "~/types/pm-coll-exts" import { PMRawLanguage } from "~/types/pm-coll-exts"
@ -79,6 +81,32 @@ const parseDescription = (descField?: string | DescriptionDefinition) => {
return descField.content 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 = ( const getHoppReqHeaders = (
headers: Item["request"]["headers"] | null headers: Item["request"]["headers"] | null
): HoppRESTHeader[] => { ): HoppRESTHeader[] => {
@ -288,6 +316,9 @@ const getHoppReqAuth = (
tokenEndpoint: accessTokenURL, tokenEndpoint: accessTokenURL,
clientSecret: "", clientSecret: "",
isPKCE: false, isPKCE: false,
authRequestParams: [],
tokenRequestParams: [],
refreshRequestParams: [],
}, },
addTo: "HEADERS", addTo: "HEADERS",
} }
@ -457,6 +488,7 @@ const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection =>
requests: pipe(ig.items.all(), A.filter(isPMItem), A.map(getHoppRequest)), requests: pipe(ig.items.all(), A.filter(isPMItem), A.map(getHoppRequest)),
auth: getHoppReqAuth(ig.auth), auth: getHoppReqAuth(ig.auth),
headers: [], headers: [],
variables: getHoppCollVariables(ig),
}) })
export const getHoppCollections = (collections: PMCollection[]) => { export const getHoppCollections = (collections: PMCollection[]) => {

View file

@ -259,6 +259,12 @@ export type HoppSavedExampleDocument = {
* (atleast as far as we can say) * (atleast as far as we can say)
*/ */
isDirty: boolean 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 * as E from "fp-ts/Either"
import { BehaviorSubject, Subscription } from "rxjs" import { BehaviorSubject, Subscription } from "rxjs"
import { import {
HoppCollectionVariable,
HoppRESTAuth, HoppRESTAuth,
HoppRESTHeader, HoppRESTHeader,
translateToNewRequest, translateToNewRequest,
@ -26,6 +27,9 @@ import {
TeamCollectionOrderUpdatedDocument, TeamCollectionOrderUpdatedDocument,
} from "~/helpers/backend/graphql" } from "~/helpers/backend/graphql"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties" 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 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 * 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 * @param folderPath the path of the folder to cascade the auth from
* @returns the inherited auth and headers for the given folder path * @returns the inherited auth and headers for the given folder path
*/ */
public cascadeParentCollectionForHeaderAuth(folderPath: string) { public cascadeParentCollectionForProperties(folderPath: string) {
let auth: HoppInheritedProperty["auth"] = { let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "", parentID: folderPath ?? "",
parentName: "", parentName: "",
@ -1050,14 +1090,16 @@ export default class NewTeamCollectionAdapter {
} }
const headers: HoppInheritedProperty["headers"] = [] const headers: HoppInheritedProperty["headers"] = []
if (!folderPath) return { auth, headers } const variables: HoppInheritedProperty["variables"] = []
if (!folderPath) return { auth, headers, variables }
const path = folderPath.split("/") const path = folderPath.split("/")
// Check if the path is empty or invalid // Check if the path is empty or invalid
if (!path || path.length === 0) { if (!path || path.length === 0) {
console.error("Invalid path:", folderPath) 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' // 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 // Check if parentFolder is undefined or null
if (!parentFolder) { if (!parentFolder) {
console.error("Parent folder not found for path:", path) console.error("Parent folder not found for path:", path)
return { auth, headers } return { auth, headers, variables }
} }
const data: { const data: {
auth: HoppRESTAuth auth: HoppRESTAuth
headers: HoppRESTHeader[] headers: HoppRESTHeader[]
variables: HoppCollectionVariable[]
} = parentFolder.data } = parentFolder.data
? JSON.parse(parentFolder.data) ? JSON.parse(parentFolder.data)
: { : {
auth: null, auth: null,
headers: null, headers: null,
variables: null,
} }
if (!data.auth) { if (!data.auth) {
@ -1091,8 +1135,11 @@ export default class NewTeamCollectionAdapter {
if (!data.headers) data.headers = [] if (!data.headers) data.headers = []
if (!data.variables) data.variables = []
const parentFolderAuth = data.auth const parentFolderAuth = data.auth
const parentFolderHeaders = data.headers const parentFolderHeaders = data.headers
const parentFolderVariables = data.variables
if ( if (
parentFolderAuth?.authType === "inherit" && 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 { import {
HoppCollectionVariable,
HoppRESTAuth, HoppRESTAuth,
HoppRESTHeader, HoppRESTHeader,
HoppRESTRequest, HoppRESTRequest,
@ -337,7 +338,7 @@ export class TeamSearchService extends Service {
this.teamsSearchResultsLoading.value = false this.teamsSearchResultsLoading.value = false
} }
cascadeParentCollectionForHeaderAuthForSearchResults = ( cascadeParentCollectionForPropertiesForSearchResults = (
collectionID: string collectionID: string
): HoppInheritedProperty => { ): HoppInheritedProperty => {
const defaultInheritedAuth: HoppInheritedProperty["auth"] = { const defaultInheritedAuth: HoppInheritedProperty["auth"] = {
@ -351,15 +352,23 @@ export class TeamSearchService extends Service {
const defaultInheritedHeaders: HoppInheritedProperty["headers"] = [] const defaultInheritedHeaders: HoppInheritedProperty["headers"] = []
const defaultInheritedVariables: HoppInheritedProperty["variables"] = []
const collection = Object.values(this.searchResultsCollections).find( const collection = Object.values(this.searchResultsCollections).find(
(col) => col.id === collectionID (col) => col.id === collectionID
) )
if (!collection) if (!collection)
return { auth: defaultInheritedAuth, headers: defaultInheritedHeaders } return {
auth: defaultInheritedAuth,
headers: defaultInheritedHeaders,
variables: defaultInheritedVariables,
}
const inheritedAuthData = this.findInheritableParentAuth(collectionID) const inheritedAuthData = this.findInheritableParentAuth(collectionID)
const inheritedHeadersData = this.findInheritableParentHeaders(collectionID) const inheritedHeadersData = this.findInheritableParentHeaders(collectionID)
const inheritedVariablesData =
this.findInheritableParentVariables(collectionID)
return { return {
auth: E.isRight(inheritedAuthData) auth: E.isRight(inheritedAuthData)
@ -368,6 +377,9 @@ export class TeamSearchService extends Service {
headers: E.isRight(inheritedHeadersData) headers: E.isRight(inheritedHeadersData)
? Object.values(inheritedHeadersData.right) ? Object.values(inheritedHeadersData.right)
: defaultInheritedHeaders, : 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 { const parentInheritedData = JSON.parse(collection.data) as {
auth: HoppRESTAuth auth: HoppRESTAuth
headers: HoppRESTHeader[] headers: HoppRESTHeader[]
variables: HoppCollectionVariable[]
} }
const inheritedAuth = parentInheritedData.auth const inheritedAuth = parentInheritedData.auth
@ -437,6 +450,7 @@ export class TeamSearchService extends Service {
const parentInheritedData = JSON.parse(collection.data) as { const parentInheritedData = JSON.parse(collection.data) as {
auth: HoppRESTAuth auth: HoppRESTAuth
headers: HoppRESTHeader[] headers: HoppRESTHeader[]
variables: HoppCollectionVariable[]
} }
const inheritedHeaders = parentInheritedData.headers const inheritedHeaders = parentInheritedData.headers
@ -464,6 +478,45 @@ export class TeamSearchService extends Service {
return E.right(existingHeaders) 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) => { expandCollection = async (collectionID: string) => {
if (this.expandingCollections.value.includes(collectionID)) return if (this.expandingCollections.value.includes(collectionID)) return

View file

@ -3,6 +3,7 @@ import {
HoppGQLAuth, HoppGQLAuth,
HoppRESTHeader, HoppRESTHeader,
HoppRESTAuth, HoppRESTAuth,
HoppCollectionVariable,
} from "@hoppscotch/data" } from "@hoppscotch/data"
export type HoppInheritedProperty = { export type HoppInheritedProperty = {
@ -16,4 +17,9 @@ export type HoppInheritedProperty = {
parentName: string parentName: string
inheritedHeader: HoppRESTHeader | GQLHeader 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 { import {
generateUniqueRefId, generateUniqueRefId,
HoppCollection, HoppCollection,
HoppCollectionVariable,
HoppGQLAuth, HoppGQLAuth,
HoppGQLRequest, HoppGQLRequest,
HoppRESTAuth, HoppRESTAuth,
@ -17,6 +18,8 @@ import { getService } from "~/modules/dioc"
import { getI18n } from "~/modules/i18n" import { getI18n } from "~/modules/i18n"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore" import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { CurrentValueService } from "~/services/current-environment-value.service"
const defaultRESTCollectionState = { const defaultRESTCollectionState = {
state: [ state: [
@ -29,6 +32,7 @@ const defaultRESTCollectionState = {
authActive: false, authActive: false,
}, },
headers: [], headers: [],
variables: [],
}), }),
], ],
} }
@ -44,6 +48,7 @@ const defaultGraphqlCollectionState = {
authActive: false, authActive: false,
}, },
headers: [], headers: [],
variables: [],
}), }),
], ],
} }
@ -69,15 +74,56 @@ export function navigateToFolderWithIndexPath(
return target !== undefined ? target : null 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 * 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 folderPath the path of the folder to cascade the auth from
* @param type the type of collection * @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 * @returns the inherited auth and headers for the given folder path
*/ */
export function cascadeParentCollectionForHeaderAuth( export function cascadeParentCollectionForProperties(
folderPath: string | undefined, folderPath: string | undefined,
type: "rest" | "graphql" type: "rest" | "graphql",
showSecret: boolean = false
) { ) {
const collectionStore = const collectionStore =
type === "rest" ? restCollectionStore : graphqlCollectionStore type === "rest" ? restCollectionStore : graphqlCollectionStore
@ -92,14 +138,16 @@ export function cascadeParentCollectionForHeaderAuth(
} }
const headers: HoppInheritedProperty["headers"] = [] 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)) const path = folderPath.split("/").map((i) => parseInt(i))
// Check if the path is empty or invalid // Check if the path is empty or invalid
if (!path || path.length === 0) { if (!path || path.length === 0) {
console.error("Invalid path:", folderPath) 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' // 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 // Check if parentFolder is undefined or null
if (!parentFolder) { if (!parentFolder) {
console.error("Parent folder not found for path:", path) console.error("Parent folder not found for path:", path)
return { auth, headers } return { auth, headers, variables }
} }
const parentFolderAuth = parentFolder.auth as HoppRESTAuth | HoppGQLAuth const parentFolderAuth = parentFolder.auth as HoppRESTAuth | HoppGQLAuth
@ -120,6 +168,9 @@ export function cascadeParentCollectionForHeaderAuth(
| HoppRESTHeaders | HoppRESTHeaders
| GQLHeader[] | GQLHeader[]
const parentFolderVariables =
parentFolder.variables as HoppCollectionVariable[]
// check if the parent folder has authType 'inherit' and if it is the root folder // check if the parent folder has authType 'inherit' and if it is the root folder
if ( if (
parentFolderAuth?.authType === "inherit" && 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) { function reorderItems(array: unknown[], from: number, to: number) {
@ -254,6 +319,7 @@ const restCollectionDispatchers = defineDispatchers({
authActive: true, authActive: true,
}, },
headers: [], headers: [],
variables: [],
}) })
const newState = state const newState = state
@ -879,6 +945,7 @@ const gqlCollectionDispatchers = defineDispatchers({
authActive: true, authActive: true,
}, },
headers: [], headers: [],
variables: [],
}) })
const newState = state const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x)) const indexPaths = path.split("/").map((x) => parseInt(x))
@ -1222,8 +1289,13 @@ function computeCollectionInheritedProps(
ref_id: string, ref_id: string,
type: "my-collections" | "team-collections" = "my-collections", type: "my-collections" | "team-collections" = "my-collections",
parentAuth: HoppRESTAuth | null = null, parentAuth: HoppRESTAuth | null = null,
parentHeaders: HoppRESTHeaders | null = null parentHeaders: HoppRESTHeaders | null = null,
): { auth: HoppRESTAuth; headers: HoppRESTHeaders } | null { parentVariables: HoppCollectionVariable[] | null = null
): {
auth: HoppRESTAuth
headers: HoppRESTHeaders
variables: HoppCollectionVariable[]
} | null {
// Determine the inherited authentication and headers // Determine the inherited authentication and headers
const inheritedAuth = const inheritedAuth =
collection.auth?.authType === "inherit" && collection.auth.authActive collection.auth?.authType === "inherit" && collection.auth.authActive
@ -1235,6 +1307,11 @@ function computeCollectionInheritedProps(
...collection.headers, ...collection.headers,
] ]
const inheritedVariables = [
...(parentVariables ?? []),
...collection.variables,
]
// Check if the current collection matches the target reference ID // Check if the current collection matches the target reference ID
const isTargetCollection = const isTargetCollection =
type === "my-collections" type === "my-collections"
@ -1245,6 +1322,7 @@ function computeCollectionInheritedProps(
return { return {
auth: inheritedAuth, auth: inheritedAuth,
headers: inheritedHeaders, headers: inheritedHeaders,
variables: inheritedVariables,
} }
} }
@ -1255,7 +1333,8 @@ function computeCollectionInheritedProps(
ref_id, ref_id,
type, type,
inheritedAuth, inheritedAuth,
inheritedHeaders inheritedHeaders,
inheritedVariables
) )
if (result) return result // Return as soon as a match is found if (result) return result // Return as soon as a match is found
} }
@ -1267,7 +1346,11 @@ export function getRESTCollectionInheritedProps(
collectionID: string, collectionID: string,
collections: HoppCollection[] = restCollectionStore.value.state, collections: HoppCollection[] = restCollectionStore.value.state,
type: "my-collections" | "team-collections" = "my-collections" type: "my-collections" | "team-collections" = "my-collections"
): { auth: HoppRESTAuth; headers: HoppRESTHeaders } | null { ): {
auth: HoppRESTAuth
headers: HoppRESTHeaders
variables: HoppCollectionVariable[]
} | null {
for (const collection of collections) { for (const collection of collections) {
const result = computeCollectionInheritedProps( const result = computeCollectionInheritedProps(
collection, collection,

View file

@ -14,6 +14,7 @@ import DispatchingStore, {
} from "~/newstore/DispatchingStore" } from "~/newstore/DispatchingStore"
import { CurrentValueService } from "~/services/current-environment-value.service" import { CurrentValueService } from "~/services/current-environment-value.service"
import { SecretEnvironmentService } from "~/services/secret-environment.service" import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { RESTTabService } from "~/services/tab/rest"
export type SelectedEnvironmentIndex = export type SelectedEnvironmentIndex =
| { type: "NO_ENV_SELECTED" } | { type: "NO_ENV_SELECTED" }
@ -49,9 +50,6 @@ const defaultEnvironmentsState = {
} as SelectedEnvironmentIndex, } as SelectedEnvironmentIndex,
} }
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
type EnvironmentStore = typeof defaultEnvironmentsState type EnvironmentStore = typeof defaultEnvironmentsState
const dispatchers = defineDispatchers({ const dispatchers = defineDispatchers({
@ -423,6 +421,7 @@ export type AggregateEnvironment = {
currentValue: string currentValue: string
secret: boolean secret: boolean
sourceEnv: string sourceEnv: string
sourceEnvID?: string
} }
/** /**
@ -431,13 +430,27 @@ export type AggregateEnvironment = {
* NOTE: The source environment attribute will be "Global" for Global Env as source. * NOTE: The source environment attribute will be "Global" for Global Env as source.
* The priority of the variables is as follows: * The priority of the variables is as follows:
* 1. Pre-defined variables * 1. Pre-defined variables
* 2. Selected Environment Variables * 2. Request Variables (from the current request)
* 3. Global Environment Variables * 3. Selected Environment Variables
* 4. Global Environment Variables
*/ */
export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest( export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
[currentEnvironment$, globalEnv$] [currentEnvironment$, globalEnv$]
).pipe( ).pipe(
map(([selectedEnv, globalEnv]) => { 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[] = [] const effectiveAggregateEnvs: AggregateEnvironment[] = []
// Ensure pre-defined variables are prioritised over other environment variables with the same name // 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) 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) => { selectedEnv?.variables.forEach((variable) => {
const { key, secret } = variable const { key, secret } = variable
const currentValue = const currentValue =
@ -495,8 +520,47 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
) )
export function getAggregateEnvs() { export function getAggregateEnvs() {
const restTabs = getService(RESTTabService)
const currentEnv = getCurrentEnvironment() 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 [ 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) => { ...currentEnv.variables.map((x) => {
let currentValue = "" let currentValue = ""
if (!x.secret) { if (!x.secret) {
@ -528,9 +592,49 @@ export function getAggregateEnvs() {
} }
export function getAggregateEnvsWithCurrentValue() { export function getAggregateEnvsWithCurrentValue() {
const restTabs = getService(RESTTabService)
const secretEnvironmentService = getService(SecretEnvironmentService)
const currentEnvironmentValueService = getService(CurrentValueService)
const currentEnv = getCurrentEnvironment() 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 [ 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) => { ...currentEnv.variables.map((x, index) => {
let currentValue = x.currentValue let currentValue = x.currentValue
if (x.secret) { if (x.secret) {
@ -581,6 +685,22 @@ export const aggregateEnvsWithCurrentValue$: Observable<
AggregateEnvironment[] AggregateEnvironment[]
> = combineLatest([currentEnvironment$, globalEnv$]).pipe( > = combineLatest([currentEnvironment$, globalEnv$]).pipe(
map(([selectedEnv, globalEnv]) => { 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[] = [] const results: AggregateEnvironment[] = []
// Ensure pre-defined variables are prioritised over other environment variables with the same name // 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) => { selectedEnv?.variables.map((x, index) => {
let currentValue = x.currentValue let currentValue = x.currentValue
if (x.secret) { if (x.secret) {

View file

@ -1,5 +1,7 @@
import { Service } from "dioc" import { Container, Service } from "dioc"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { nextTick } from "vue"
import { watch } from "vue"
import { reactive, computed } from "vue" import { reactive, computed } from "vue"
/** /**
@ -21,6 +23,12 @@ export type Variable = {
export class CurrentValueService extends Service { export class CurrentValueService extends Service {
public static readonly ID = "CURRENT_VALUE_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. * Map of current value of environments.
* The key is the ID of the environment. * The key is the ID of the environment.
@ -159,4 +167,26 @@ export class CurrentValueService extends Service {
}) })
return environments 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({ const req = ref({
...getDefaultRESTRequest(), ...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data", 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) const result = requestInspector.getInspections(req)
@ -83,7 +90,14 @@ describe("RequestInspectorService", () => {
const req = ref({ const req = ref({
...getDefaultRESTRequest(), ...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data", 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) const result = requestInspector.getInspections(req)

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import { Service } from "dioc" import { Container, Service } from "dioc"
import { reactive, computed } from "vue" import { nextTick } from "vue"
import { reactive, computed, watch } from "vue"
/** /**
* Defines a secret environment variable. * Defines a secret environment variable.
@ -19,6 +20,12 @@ export type SecretVariable = {
export class SecretEnvironmentService extends Service { export class SecretEnvironmentService extends Service {
public static readonly ID = "SECRET_ENVIRONMENT_SERVICE" public static readonly ID = "SECRET_ENVIRONMENT_SERVICE"
constructor(c: Container) {
super(c)
// Initialize the secret environments map
this.watchSecretEnvironments()
}
/** /**
* Map of secret environments. * Map of secret environments.
* The key is the ID of the secret environment. * The key is the ID of the secret environment.
@ -137,4 +144,26 @@ export class SecretEnvironmentService extends Service {
}) })
return secretEnvironments 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 { getI18n } from "~/modules/i18n"
import MiniSearch from "minisearch" import MiniSearch from "minisearch"
import { import {
cascadeParentCollectionForHeaderAuth, cascadeParentCollectionForProperties,
graphqlCollectionStore, graphqlCollectionStore,
restCollectionStore, restCollectionStore,
} from "~/newstore/collections" } from "~/newstore/collections"
@ -318,11 +318,6 @@ export class CollectionsSpotlightSearcherService
if (!req) return if (!req) return
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath.join("/"),
"rest"
)
this.restTab.createNewTab( this.restTab.createNewTab(
{ {
type: "request", type: "request",
@ -333,10 +328,10 @@ export class CollectionsSpotlightSearcherService
folderPath: folderPath.join("/"), folderPath: folderPath.join("/"),
requestIndex: reqIndex, requestIndex: reqIndex,
}, },
inheritedProperties: { inheritedProperties: cascadeParentCollectionForProperties(
auth, folderPath.join("/"),
headers, "rest"
}, ),
}, },
true true
) )
@ -350,10 +345,6 @@ export class CollectionsSpotlightSearcherService
if (!req) return if (!req) return
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath.join("/"),
"graphql"
)
this.gqlTab.createNewTab({ this.gqlTab.createNewTab({
saveContext: { saveContext: {
originLocation: "user-collection", originLocation: "user-collection",
@ -363,10 +354,10 @@ export class CollectionsSpotlightSearcherService
cursorPosition: 0, cursorPosition: 0,
request: req, request: req,
isDirty: false, isDirty: false,
inheritedProperties: { inheritedProperties: cascadeParentCollectionForProperties(
auth, folderPath.join("/"),
headers, "graphql"
}, ),
}) })
} }
} }

View file

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

View file

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

View file

@ -9,6 +9,9 @@ import V6_VERSION from "./v/6"
import V7_VERSION from "./v/7" import V7_VERSION from "./v/7"
import V8_VERSION from "./v/8" import V8_VERSION from "./v/8"
import V9_VERSION from "./v/9" import V9_VERSION from "./v/9"
import V10_VERSION from "./v/10"
export { CollectionVariable } from "./v/10"
import { z } from "zod" import { z } from "zod"
import { translateToNewRequest } from "../rest" import { translateToNewRequest } from "../rest"
@ -20,7 +23,7 @@ const versionedObject = z.object({
}) })
export const HoppCollection = createVersionedEntity({ export const HoppCollection = createVersionedEntity({
latestVersion: 9, latestVersion: 10,
versionMap: { versionMap: {
1: V1_VERSION, 1: V1_VERSION,
2: V2_VERSION, 2: V2_VERSION,
@ -31,6 +34,7 @@ export const HoppCollection = createVersionedEntity({
7: V7_VERSION, 7: V7_VERSION,
8: V8_VERSION, 8: V8_VERSION,
9: V9_VERSION, 9: V9_VERSION,
10: V10_VERSION,
}, },
getVersion(data) { getVersion(data) {
const versionCheck = versionedObject.safeParse(data) const versionCheck = versionedObject.safeParse(data)
@ -46,7 +50,11 @@ export const HoppCollection = createVersionedEntity({
export type HoppCollection = InferredEntity<typeof HoppCollection> 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 * 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 auth = x.auth ?? { authType: "inherit", authActive: true }
const headers = x.headers ?? [] const headers = x.headers ?? []
const variables = x.variables ?? []
const obj = makeCollection({ const obj = makeCollection({
name, name,
@ -81,6 +90,7 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
requests, requests,
auth, auth,
headers, headers,
variables,
}) })
if (x.id) obj.id = x.id 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 auth = x.auth ?? { authType: "inherit", authActive: true }
const headers = x.headers ?? [] const headers = x.headers ?? []
const variables = x.variables ?? []
const obj = makeCollection({ const obj = makeCollection({
name, name,
@ -109,6 +120,7 @@ export function translateToNewGQLCollection(x: any): HoppCollection {
requests, requests,
auth, auth,
headers, headers,
variables,
}) })
if (x.id) obj.id = x.id 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 { defineVersion } from "verzod"
import { V1_SCHEMA } from "./1" 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({ export const V2_SCHEMA = V1_SCHEMA.extend({
v: z.literal(2), v: z.literal(2),
variables: z.array( variables: z.array(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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