mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
6 Commits
feature/to
...
feat/parse
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
758ef9fc96 | ||
|
|
ced9d38abc | ||
|
|
98f3a524dc | ||
|
|
a06a339d0c | ||
|
|
e34ac3de7c | ||
|
|
074c6be5f4 |
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -5,13 +5,14 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -51,10 +52,6 @@ jobs:
|
||||
cli-test:
|
||||
name: CLI Tests
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
448
package-lock.json
generated
448
package-lock.json
generated
@@ -8586,6 +8586,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/boolean": {
|
||||
@@ -9416,229 +9417,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
|
||||
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio-select": "^2.1.0",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"encoding-sniffer": "^0.2.0",
|
||||
"htmlparser2": "^9.1.0",
|
||||
"parse5": "^7.1.2",
|
||||
"parse5-htmlparser2-tree-adapter": "^7.0.0",
|
||||
"parse5-parser-stream": "^7.1.2",
|
||||
"undici": "^6.19.5",
|
||||
"whatwg-mimetype": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select/node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select/node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select/node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/cheerio-select/node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select/node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/cheerio/node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/htmlparser2": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/parse5": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
|
||||
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^4.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -10805,6 +10583,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -11946,19 +11725,6 @@
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding-sniffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
|
||||
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.3",
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
@@ -17611,6 +17377,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
@@ -18015,106 +17782,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
|
||||
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^4.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-parser-stream": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-parser-stream/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-parser-stream/node_modules/parse5": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
|
||||
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^4.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -21503,6 +21170,7 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
@@ -23561,15 +23229,6 @@
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
@@ -24098,27 +23757,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
@@ -24260,28 +23898,6 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js/node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||
@@ -24414,6 +24030,7 @@
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"axios": "1.7.5",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "5.65.2",
|
||||
"codemirror-graphql": "2.1.1",
|
||||
@@ -24427,7 +24044,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"httpsnippet": "^3.0.6",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
@@ -24530,7 +24147,7 @@
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.5",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "^3.0.0",
|
||||
@@ -24551,16 +24168,6 @@
|
||||
"bru": "bin/bru.js"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@@ -24603,7 +24210,7 @@
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"about-window": "^1.15.2",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.5",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
@@ -24640,17 +24247,6 @@
|
||||
"dmg-license": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@@ -24745,11 +24341,10 @@
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.5",
|
||||
"btoa": "^1.2.1",
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"json-query": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -24759,8 +24354,7 @@
|
||||
"node-vault": "^0.10.2",
|
||||
"path": "^0.12.7",
|
||||
"quickjs-emscripten": "^0.29.2",
|
||||
"uuid": "^9.0.0",
|
||||
"xml2js": "^0.6.2"
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
@@ -24774,16 +24368,6 @@
|
||||
"@usebruno/vm2": "^3.9.13"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-js/node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
@@ -24861,7 +24445,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.5",
|
||||
"body-parser": "1.20.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
@@ -24875,16 +24459,6 @@
|
||||
"multer": "^1.4.5-lts.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-tests/node_modules/fast-xml-parser": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz",
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"axios": "1.7.5",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "5.65.2",
|
||||
"codemirror-graphql": "2.1.1",
|
||||
@@ -33,7 +34,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"httpsnippet": "^3.0.6",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
|
||||
@@ -8,8 +8,6 @@ const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
|
||||
line-break: anywhere;
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
/* Removes the glow outline around the folded json */
|
||||
@@ -28,10 +26,6 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
top: unset;
|
||||
left: unset;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: 1px solid #d3d6db;
|
||||
|
||||
@@ -74,16 +74,13 @@ if (!SERVER_RENDERED) {
|
||||
'bru.setNextRequest(requestName)',
|
||||
'req.disableParsingResponseJson()',
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.runRequest(requestPathName)',
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.runner.stopExecution()'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -197,20 +194,8 @@ export default class CodeEditor extends React.Component {
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll',
|
||||
'Ctrl-/': () => {
|
||||
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
|
||||
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
},
|
||||
'Cmd-/': () => {
|
||||
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
|
||||
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
}
|
||||
'Ctrl-/': 'toggleComment',
|
||||
'Cmd-/': 'toggleComment'
|
||||
},
|
||||
foldOptions: {
|
||||
widget: (from, to) => {
|
||||
|
||||
@@ -104,15 +104,18 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
<InfoTip infotipId="request-var">
|
||||
<InfoTip
|
||||
text={`
|
||||
<div>
|
||||
<ul>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>global</span> - use global proxy config</li>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>enabled</span> - use collection proxy config</li>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>disable</span> - disable proxy</li>
|
||||
<li><span style="width: 50px;display:inline-block;">global</span> - use global proxy config</li>
|
||||
<li><span style="width: 50px;display:inline-block;">enabled</span> - use collection proxy config</li>
|
||||
<li><span style="width: 50px;display:inline-block;">disable</span> - disable proxy</li>
|
||||
</ul>
|
||||
</div>
|
||||
</InfoTip>
|
||||
`}
|
||||
infotipId="request-var"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
|
||||
@@ -89,7 +89,7 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip content="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { IconX } from '@tabler/icons';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
import slash from 'utils/common/slash';
|
||||
|
||||
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
|
||||
const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
value = value || [];
|
||||
const dispatch = useDispatch();
|
||||
const filenames = (isSingleFilePicker ? [value] : value || [])
|
||||
const filenames = value
|
||||
.filter((v) => v != null && v != '')
|
||||
.map((v) => {
|
||||
const separator = isWindowsOS() ? '\\' : '/';
|
||||
@@ -19,7 +20,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
const title = filenames.map((v) => `- ${v}`).join('\n');
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""]))
|
||||
dispatch(browseFiles())
|
||||
.then((filePaths) => {
|
||||
// If file is in the collection's directory, then we use relative path
|
||||
// Otherwise, we use the absolute path
|
||||
@@ -33,7 +34,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
return filePath;
|
||||
});
|
||||
|
||||
onChange(isSingleFilePicker ? filePaths[0] : filePaths);
|
||||
onChange(filePaths);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -41,14 +42,14 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
onChange(isSingleFilePicker ? '' : []);
|
||||
onChange([]);
|
||||
};
|
||||
|
||||
const renderButtonText = (filenames) => {
|
||||
if (filenames.length == 1) {
|
||||
return filenames[0];
|
||||
}
|
||||
return filenames.length + ' file(s) selected';
|
||||
return filenames.length + ' files selected';
|
||||
};
|
||||
|
||||
return filenames.length > 0 ? (
|
||||
@@ -65,9 +66,9 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
|
||||
{isSingleFilePicker ? 'Select File' : 'Select Files'}
|
||||
Select Files
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePickerEditor;
|
||||
export default FilePickerEditor;
|
||||
|
||||
@@ -88,7 +88,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
|
||||
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Tooltip as ReactInfoTip } from 'react-tooltip';
|
||||
|
||||
const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
|
||||
const InfoTip = ({ text, infotipId }) => {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
@@ -17,7 +17,7 @@ const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
|
||||
</svg>
|
||||
<ReactInfoTip anchorId={infotipId} {...props} />
|
||||
<ReactInfoTip anchorId={infotipId} html={text} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-param {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,164 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { get, cloneDeep, isArray } from 'lodash';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFile as _addFile, updateFile, deleteFile } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FilePickerEditor from 'components/FilePickerEditor/index';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
|
||||
const FileBody = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file');
|
||||
|
||||
const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');
|
||||
|
||||
const addFile = () => {
|
||||
dispatch(
|
||||
_addFile({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
case 'filePath': {
|
||||
param.filePath = e.target.filePath;
|
||||
param.contentType = "";
|
||||
break;
|
||||
}
|
||||
case 'contentType': {
|
||||
param.contentType = e.target.contentType;
|
||||
break;
|
||||
}
|
||||
case 'selected': {
|
||||
param.selected = e.target.selected;
|
||||
setEnableFileUid(param.uid)
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateFile({
|
||||
param: param,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveParams = (param) => {
|
||||
dispatch(
|
||||
deleteFile({
|
||||
paramUid: param.uid,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">File</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">Content-Type</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">Selected</div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{params && params.length
|
||||
? params.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid}>
|
||||
<td>
|
||||
<FilePickerEditor
|
||||
isSingleFilePicker={true}
|
||||
value={param.filePath}
|
||||
onChange={(path) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
filePath: path
|
||||
}
|
||||
},
|
||||
param,
|
||||
'filePath'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
className="flex items-center justify-center"
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
placeholder="Auto"
|
||||
value={param.contentType}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
contentType: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'contentType'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">
|
||||
<input
|
||||
key={param.uid}
|
||||
type="radio"
|
||||
name="selected"
|
||||
checked={enabledFileUid === param.uid || param.selected}
|
||||
tabIndex="-1"
|
||||
className="mr-1 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'selected')}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center">
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button className="btn-add-param text-link pr-2 pt-3 select-none" onClick={addFile}>
|
||||
+ Add File
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default FileBody;
|
||||
@@ -154,7 +154,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5 flex-1 relative">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className="flex w-full mt-5 flex-1">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledWrapper className="w-full relative">
|
||||
<button
|
||||
className="btn-add-param text-link px-4 py-4 select-none absolute top-0 right-0 z-10"
|
||||
onClick={onPrettify}
|
||||
@@ -68,7 +68,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -176,7 +176,8 @@ const QueryParams = ({ item, collection }) => {
|
||||
</button>
|
||||
<div className="mb-2 title text-xs flex items-stretch">
|
||||
<span>Path</span>
|
||||
<InfoTip infotipId="path-param-InfoTip">
|
||||
<InfoTip
|
||||
text={`
|
||||
<div>
|
||||
Path variables are automatically added whenever the
|
||||
<code className="font-mono mx-2">:name</code>
|
||||
@@ -185,7 +186,9 @@ const QueryParams = ({ item, collection }) => {
|
||||
https://example.com/v1/users/<span>:id</span>
|
||||
</code>
|
||||
</div>
|
||||
</InfoTip>
|
||||
`}
|
||||
infotipId="path-param-InfoTip"
|
||||
/>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
@@ -128,15 +128,6 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
SPARQL
|
||||
</div>
|
||||
<div className="label-item font-medium">Other</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('file');
|
||||
}}
|
||||
>
|
||||
File / Binary
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FileBody from '../FileBody/index';
|
||||
|
||||
const RequestBody = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -63,10 +62,6 @@ const RequestBody = ({ item, collection }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (bodyMode === 'file') {
|
||||
return <FileBody item={item} collection={collection}/>
|
||||
}
|
||||
|
||||
if (bodyMode === 'formUrlEncoded') {
|
||||
return <FormUrlEncodedParams item={item} collection={collection} />;
|
||||
}
|
||||
@@ -77,4 +72,4 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
|
||||
};
|
||||
export default RequestBody;
|
||||
export default RequestBody;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -5,6 +5,7 @@ import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,17 +28,19 @@ const Tests = ({ item, collection }) => {
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={tests || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={tests || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
|
||||
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
|
||||
</div>
|
||||
), accessor: 'value', width: '46%' },
|
||||
{ name: '', accessor: '', width: '14%' }
|
||||
|
||||
@@ -35,7 +35,7 @@ const CollectionToolBar = ({ collection }) => {
|
||||
const viewCollectionSettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: collection.uid,
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-settings'
|
||||
})
|
||||
|
||||
@@ -2,15 +2,15 @@ import React from 'react';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
|
||||
const getTabInfo = (type, tabName) => {
|
||||
switch (type) {
|
||||
case 'collection-settings': {
|
||||
return (
|
||||
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
|
||||
<>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Collection</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'collection-overview': {
|
||||
@@ -31,7 +31,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
|
||||
}
|
||||
case 'folder-settings': {
|
||||
return (
|
||||
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
|
||||
<div className="flex items-center flex-nowrap overflow-hidden">
|
||||
<IconFolder size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
|
||||
<span className="ml-1 leading-6 truncate">{tabName || 'Folder'}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useRef, Fragment } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
@@ -73,13 +73,13 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
|
||||
className="flex items-center justify-between tab-container px-1"
|
||||
onMouseUp={handleMouseUp} // Add middle-click behavior here
|
||||
>
|
||||
{tab.type === 'folder-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} tabName={folder?.name} />
|
||||
) : (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
@@ -144,9 +144,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`flex items-baseline tab-label pl-2 ${tab.preview ? "italic" : ""}`}
|
||||
className="flex items-baseline tab-label pl-2"
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}
|
||||
onMouseUp={(e) => {
|
||||
if (!item.draft) return handleMouseUp(e);
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<InfoTip
|
||||
content="This folder will be created under the selected location"
|
||||
text="This folder will be created under the selected location"
|
||||
infotipId="collection-folder-name-infotip"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -5,8 +5,8 @@ import classnames from 'classnames';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { moveItem, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -23,16 +23,13 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import CollectionItemIcon from './CollectionItemIcon';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import CollectionItemIcon from './CollectionItemIcon/index';
|
||||
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isSidebarDragging = useSelector((state) => state.app.isDragging);
|
||||
const dispatch = useDispatch();
|
||||
const collectionItemRef = useRef(null);
|
||||
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
||||
@@ -46,31 +43,28 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: `collection-item-${collection.uid}`,
|
||||
type: `COLLECTION_ITEM_${collection.uid}`,
|
||||
item: item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
}),
|
||||
options: {
|
||||
dropEffect: "move"
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: `collection-item-${collection.uid}`,
|
||||
accept: `COLLECTION_ITEM_${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
if (draggedItem.uid !== item.uid) {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
}
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
return draggedItem.uid !== item.uid;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
drag(drop(collectionItemRef));
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
@@ -89,6 +83,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
'item-hovered': isOver
|
||||
});
|
||||
|
||||
const scrollToTheActiveTab = () => {
|
||||
const activeTab = document.querySelector('.request-tab.active');
|
||||
if (activeTab) {
|
||||
activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
@@ -98,13 +99,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
};
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (event.detail != 1) return;
|
||||
//scroll to the active tab
|
||||
setTimeout(scrollToTheActiveTab, 50);
|
||||
|
||||
const isRequest = isItemARequest(item);
|
||||
|
||||
if (isRequest) {
|
||||
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
@@ -114,21 +112,20 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item),
|
||||
type: 'request',
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
type: 'folder-settings',
|
||||
type: 'folder-settings'
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
@@ -137,12 +134,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderCollapse = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const handleFolderCollapse = () => {
|
||||
dispatch(
|
||||
collectionFolderClicked({
|
||||
itemUid: item.uid,
|
||||
@@ -162,6 +156,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleClick = (event) => {
|
||||
setRenameItemModalOpen(true);
|
||||
};
|
||||
|
||||
let indents = range(item.depth);
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const isFolder = isItemAFolder(item);
|
||||
@@ -182,10 +180,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDoubleClick = (event) => {
|
||||
dispatch(makeTabPermanent({ uid: item.uid }))
|
||||
};
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
@@ -226,13 +220,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowInFolder = () => {
|
||||
dispatch(showInFolder(item.pathname)).catch((error) => {
|
||||
console.error('Error opening the folder', error);
|
||||
toast.error('Error opening the folder');
|
||||
});
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
|
||||
@@ -259,7 +246,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
)}
|
||||
<div className={itemRowClassName} ref={collectionItemRef}>
|
||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
? indents.map((i) => {
|
||||
@@ -286,9 +273,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
style={{
|
||||
paddingLeft: 8
|
||||
}}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<div style={{ width: 16, minWidth: 16 }}>
|
||||
{isFolder ? (
|
||||
@@ -304,6 +288,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
|
||||
<div
|
||||
className="ml-1 flex w-full h-full items-center overflow-hidden"
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<CollectionItemIcon item={item} />
|
||||
<span className="item-name" title={item.name}>
|
||||
@@ -384,15 +371,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
Generate Code
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleShowInFolder();
|
||||
}}
|
||||
>
|
||||
Show in Folder
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item delete-item"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -12,17 +12,6 @@ const Wrapper = styled.div`
|
||||
transform: rotateZ(90deg);
|
||||
}
|
||||
|
||||
&.item-hovered {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
.collection-actions {
|
||||
.dropdown {
|
||||
div[aria-expanded='false'] {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collection-actions {
|
||||
.dropdown {
|
||||
div[aria-expanded='true'] {
|
||||
|
||||
@@ -2,13 +2,13 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { uuid } from 'utils/common';
|
||||
import filter from 'lodash/filter';
|
||||
import { useDrop, useDrag } from 'react-dnd';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { collapseCollection } from 'providers/ReduxStore/slices/collections';
|
||||
import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
import CollectionItem from './CollectionItem';
|
||||
@@ -20,8 +20,7 @@ import { isItemAFolder, isItemARequest } from 'utils/collections';
|
||||
import RenameCollection from './RenameCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CloneCollection from './CloneCollection';
|
||||
import { areItemsLoading, findItemInCollection } from 'utils/collections';
|
||||
import { scrollToTheActiveTab } from 'utils/tabs';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
@@ -30,10 +29,8 @@ const Collection = ({ collection, searchText }) => {
|
||||
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
|
||||
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const dispatch = useDispatch();
|
||||
const isLoading = areItemsLoading(collection);
|
||||
const collectionRef = useRef(null);
|
||||
|
||||
const menuDropdownTippyRef = useRef();
|
||||
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
|
||||
@@ -55,16 +52,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ensureCollectionIsMounted = () => {
|
||||
if (collection.mountStatus === 'unmounted') {
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
const collectionIsCollapsed = hasSearchText ? false : collection.collapsed;
|
||||
|
||||
@@ -73,37 +60,30 @@ const Collection = ({ collection, searchText }) => {
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (event.detail != 1) return;
|
||||
// Check if the click came from the chevron icon
|
||||
const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon');
|
||||
setTimeout(scrollToTheActiveTab, 50);
|
||||
|
||||
ensureCollectionIsMounted();
|
||||
|
||||
if (collection.mountStatus === 'unmounted') {
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
}
|
||||
dispatch(collapseCollection(collection.uid));
|
||||
|
||||
|
||||
// Only open collection settings if not clicking the chevron
|
||||
if(!isChevronClick) {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: collection.uid,
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-settings',
|
||||
type: 'collection-settings'
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleClick = (event) => {
|
||||
dispatch(makeTabPermanent({ uid: collection.uid }))
|
||||
};
|
||||
|
||||
const handleCollectionCollapse = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
ensureCollectionIsMounted();
|
||||
dispatch(collapseCollection(collection.uid));
|
||||
}
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const _menuDropdown = menuDropdownTippyRef.current;
|
||||
if (_menuDropdown) {
|
||||
@@ -118,58 +98,33 @@ const Collection = ({ collection, searchText }) => {
|
||||
const viewCollectionSettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: collection.uid,
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-settings'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const isCollectionItem = (itemType) => {
|
||||
return itemType.startsWith('collection-item');
|
||||
};
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: "collection",
|
||||
item: collection,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
options: {
|
||||
dropEffect: "move"
|
||||
}
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: ["collection", `collection-item-${collection.uid}`],
|
||||
drop: (draggedItem, monitor) => {
|
||||
const itemType = monitor.getItemType();
|
||||
if (isCollectionItem(itemType)) {
|
||||
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid))
|
||||
} else {
|
||||
dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection}));
|
||||
}
|
||||
accept: `COLLECTION_ITEM_${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
return draggedItem.uid !== collection.uid;
|
||||
// todo need to make sure that draggedItem belongs to the collection
|
||||
return true;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
drag(drop(collectionRef));
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const collectionRowClassName = classnames('flex py-1 collection-name items-center', {
|
||||
'item-hovered': isOver
|
||||
});
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
@@ -199,13 +154,10 @@ const Collection = ({ collection, searchText }) => {
|
||||
{showCloneCollectionModalOpen && (
|
||||
<CloneCollection collection={collection} onClose={() => setShowCloneCollectionModalOpen(false)} />
|
||||
)}
|
||||
<div className={collectionRowClassName}
|
||||
ref={collectionRef}
|
||||
>
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<div
|
||||
className="flex flex-grow items-center overflow-hidden"
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onContextMenu={handleRightClick}
|
||||
>
|
||||
<IconChevronRight
|
||||
@@ -213,7 +165,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
strokeWidth={2}
|
||||
className={`chevron-icon ${iconClassName}`}
|
||||
style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}
|
||||
onClick={handleCollectionCollapse}
|
||||
/>
|
||||
<div className="ml-1" id="sidebar-collection-name">
|
||||
{collection.name}
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
IconSortDescendingLetters,
|
||||
IconX
|
||||
} from '@tabler/icons';
|
||||
import Collection from './Collection';
|
||||
import Collection from '../Collections/Collection';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
// todo: move this to a separate folder
|
||||
@@ -117,7 +119,9 @@ const Collections = () => {
|
||||
{collections && collections.length
|
||||
? collections.map((c) => {
|
||||
return (
|
||||
<Collection searchText={searchText} collection={c} key={c.uid} />
|
||||
<DndProvider backend={HTML5Backend} key={c.uid}>
|
||||
<Collection searchText={searchText} collection={c} key={c.uid} />
|
||||
</DndProvider>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
|
||||
@@ -120,7 +120,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<InfoTip
|
||||
content="This folder will be created under the selected location"
|
||||
text="This folder will be created under the selected location"
|
||||
infotipId="collection-folder-name-infotip"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -34,7 +34,7 @@ const ToolHint = ({
|
||||
<StyledWrapper theme={appliedTheme}>
|
||||
<ReactToolHint
|
||||
anchorId={toolhintId}
|
||||
content={text}
|
||||
html={text}
|
||||
className="toolhint"
|
||||
offset={offset}
|
||||
place={place}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './pages/index';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
@@ -10,9 +8,7 @@ if (rootElement) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<App />
|
||||
</DndProvider>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,12 @@ import collectionsReducer from './slices/collections';
|
||||
import tabsReducer from './slices/tabs';
|
||||
import notificationsReducer from './slices/notifications';
|
||||
import globalEnvironmentsReducer from './slices/global-environments';
|
||||
import { draftDetectMiddleware } from './middlewares/draft/middleware';
|
||||
|
||||
const isDevEnv = () => {
|
||||
return import.meta.env.MODE === 'development';
|
||||
};
|
||||
|
||||
let middleware = [tasksMiddleware.middleware, draftDetectMiddleware];
|
||||
let middleware = [tasksMiddleware.middleware];
|
||||
if (isDevEnv()) {
|
||||
middleware = [...middleware, debugMiddleware.middleware];
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { handleMakeTabParmanent } from "./utils";
|
||||
|
||||
const actionsToIntercept = [
|
||||
'collections/requestUrlChanged',
|
||||
'collections/updateAuth',
|
||||
'collections/addQueryParam',
|
||||
'collections/moveQueryParam',
|
||||
'collections/updateQueryParam',
|
||||
'collections/deleteQueryParam',
|
||||
'collections/updatePathParam',
|
||||
'collections/addRequestHeader',
|
||||
'collections/updateRequestHeader',
|
||||
'collections/deleteRequestHeader',
|
||||
'collections/moveRequestHeader',
|
||||
'collections/addFormUrlEncodedParam',
|
||||
'collections/updateFormUrlEncodedParam',
|
||||
'collections/deleteFormUrlEncodedParam',
|
||||
'collections/moveFormUrlEncodedParam',
|
||||
'collections/addMultipartFormParam',
|
||||
'collections/updateMultipartFormParam',
|
||||
'collections/deleteMultipartFormParam',
|
||||
'collections/moveMultipartFormParam',
|
||||
'collections/updateRequestAuthMode',
|
||||
'collections/updateRequestBodyMode',
|
||||
'collections/updateRequestBody',
|
||||
'collections/updateRequestGraphqlQuery',
|
||||
'collections/updateRequestGraphqlVariables',
|
||||
'collections/updateRequestScript',
|
||||
'collections/updateResponseScript',
|
||||
'collections/updateRequestTests',
|
||||
'collections/updateRequestMethod',
|
||||
'collections/addAssertion',
|
||||
'collections/updateAssertion',
|
||||
'collections/deleteAssertion',
|
||||
'collections/moveAssertion',
|
||||
'collections/addVar',
|
||||
'collections/updateVar',
|
||||
'collections/deleteVar',
|
||||
'collections/moveVar',
|
||||
'collections/addFolderHeader',
|
||||
'collections/updateFolderHeader',
|
||||
'collections/deleteFolderHeader',
|
||||
'collections/addFolderVar',
|
||||
'collections/updateFolderVar',
|
||||
'collections/deleteFolderVar',
|
||||
'collections/updateRequestDocs',
|
||||
'collections/runRequestEvent', // TODO: This doesn't necessarily related to a draft state, need to rethink.
|
||||
];
|
||||
|
||||
export const draftDetectMiddleware = ({ dispatch, getState }) => (next) => (action) => {
|
||||
if (actionsToIntercept.includes(action.type)) {
|
||||
const state = getState();
|
||||
handleMakeTabParmanent(state, action, dispatch);
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { makeTabPermanent } from "providers/ReduxStore/slices/tabs";
|
||||
import { findCollectionByUid, findItemInCollection } from "utils/collections/index";
|
||||
import find from 'lodash/find';
|
||||
|
||||
function handleMakeTabParmanent(state, action, dispatch) {
|
||||
const tabs = state.tabs.tabs;
|
||||
const activeTabUid = state.tabs.activeTabUid;
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const itemUid = action.payload.itemUid || action.payload.folderUid
|
||||
const collection = findCollectionByUid(state.collections.collections, action.payload.collectionUid);
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item && focusedTab.preview == true) {
|
||||
dispatch(makeTabPermanent({ uid: itemUid }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
handleMakeTabParmanent
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
transformRequestToSaveToFilesystem
|
||||
} from 'utils/collections';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
import { PATH_SEPARATOR, getDirectoryName, isWindowsPath } from 'utils/common/platform';
|
||||
import { PATH_SEPARATOR, getDirectoryName } from 'utils/common/platform';
|
||||
import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network';
|
||||
import { callIpc } from 'utils/common/ipc';
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
selectEnvironment as _selectEnvironment,
|
||||
sortCollections as _sortCollections,
|
||||
updateCollectionMountStatus,
|
||||
moveCollection,
|
||||
requestCancelled,
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
@@ -494,7 +493,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
);
|
||||
if (!reqWithSameNameExists) {
|
||||
const dirname = getDirectoryName(item.pathname);
|
||||
const fullName = isWindowsPath(item.pathname) ? path.win32.join(dirname, filename) : path.join(dirname, filename);
|
||||
const fullName = path.join(dirname, filename);
|
||||
const { ipcRenderer } = window;
|
||||
const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');
|
||||
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
|
||||
@@ -759,8 +758,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
xml: null,
|
||||
sparql: null,
|
||||
multipartForm: null,
|
||||
formUrlEncoded: null,
|
||||
file: null
|
||||
formUrlEncoded: null
|
||||
},
|
||||
auth: auth ?? {
|
||||
mode: 'none'
|
||||
@@ -1040,17 +1038,14 @@ export const browseDirectory = () => (dispatch, getState) => {
|
||||
};
|
||||
|
||||
export const browseFiles =
|
||||
(filters, properties) =>
|
||||
(_dispatch, _getState) => {
|
||||
(filters = []) =>
|
||||
(dispatch, getState) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:browse-files', filters, properties)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
ipcRenderer.invoke('renderer:browse-files', filters).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@@ -1152,22 +1147,6 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g
|
||||
});
|
||||
};
|
||||
|
||||
export const moveCollectionAndPersist = ({ draggedItem, targetItem }) => (dispatch, getState) => {
|
||||
dispatch(moveCollection({ draggedItem, targetItem }));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
const state = getState();
|
||||
|
||||
const collectionPaths = state.collections.collections.map((collection) => collection.pathname);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:update-collection-paths', collectionPaths)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
@@ -1240,10 +1219,3 @@ export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const showInFolder = (collectionPath) => () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('renderer:show-in-folder', collectionPath).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { uuid } from 'utils/common';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set } from 'lodash';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
addDepth,
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
|
||||
import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform';
|
||||
import toast from 'react-hot-toast';
|
||||
import mime from 'mime-types';
|
||||
import path from 'node:path';
|
||||
|
||||
const initialState = {
|
||||
collections: [],
|
||||
@@ -100,12 +98,6 @@ export const collectionsSlice = createSlice({
|
||||
break;
|
||||
}
|
||||
},
|
||||
moveCollection: (state, action) => {
|
||||
const { draggedItem, targetItem } = action.payload;
|
||||
state.collections = state.collections.filter((i) => i.uid !== draggedItem.uid); // Remove dragged item
|
||||
const targetItemIndex = state.collections.findIndex((i) => i.uid === targetItem.uid); // Find target item
|
||||
state.collections.splice(targetItemIndex, 0, draggedItem); // Insert dragged-item above target-item
|
||||
},
|
||||
updateLastAction: (state, action) => {
|
||||
const { collectionUid, lastAction } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -903,76 +895,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
addFile: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.body.file = item.draft.request.body.file || [];
|
||||
|
||||
item.draft.request.body.file.push({
|
||||
uid: uuid(),
|
||||
filePath: '',
|
||||
contentType: '',
|
||||
selected: false
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFile: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
const param = find(item.draft.request.body.file, (p) => p.uid === action.payload.param.uid);
|
||||
|
||||
if (param) {
|
||||
const contentType = mime.contentType(path.extname(action.payload.param.filePath));
|
||||
param.filePath = action.payload.param.filePath;
|
||||
param.contentType = action.payload.param.contentType || contentType || '';
|
||||
param.selected = action.payload.param.selected;
|
||||
|
||||
item.draft.request.body.file = item.draft.request.body.file.map((p) => {
|
||||
p.selected = p.uid === param.uid;
|
||||
return p;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteFile: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
item.draft.request.body.file = filter(
|
||||
item.draft.request.body.file,
|
||||
(p) => p.uid !== action.payload.paramUid
|
||||
);
|
||||
|
||||
if (item.draft.request.body.file.length > 0) {
|
||||
item.draft.request.body.file[0].selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -1029,10 +951,6 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.body.sparql = action.payload.content;
|
||||
break;
|
||||
}
|
||||
case 'file': {
|
||||
item.draft.request.body.file = action.payload.content;
|
||||
break;
|
||||
}
|
||||
case 'formUrlEncoded': {
|
||||
item.draft.request.body.formUrlEncoded = action.payload.content;
|
||||
break;
|
||||
@@ -1767,12 +1685,6 @@ export const collectionsSlice = createSlice({
|
||||
// we don't want to lose the draft in this case
|
||||
if (areItemsTheSameExceptSeqUpdate(item, file.data)) {
|
||||
item.seq = file.data.seq;
|
||||
if (item?.draft) {
|
||||
item.draft.seq = file.data.seq;
|
||||
}
|
||||
if (item?.draft && areItemsTheSameExceptSeqUpdate(item?.draft, file.data)) {
|
||||
item.draft = null;
|
||||
}
|
||||
} else {
|
||||
item.name = file.data.name;
|
||||
item.type = file.data.type;
|
||||
@@ -2040,9 +1952,6 @@ export const {
|
||||
addMultipartFormParam,
|
||||
updateMultipartFormParam,
|
||||
deleteMultipartFormParam,
|
||||
addFile,
|
||||
updateFile,
|
||||
deleteFile,
|
||||
moveMultipartFormParam,
|
||||
updateRequestAuthMode,
|
||||
updateRequestBodyMode,
|
||||
@@ -2094,8 +2003,7 @@ export const {
|
||||
runFolderEvent,
|
||||
resetCollectionRunner,
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
moveCollection
|
||||
updateFolderDocs
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { findIndex } from 'lodash';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import last from 'lodash/last';
|
||||
@@ -20,59 +19,31 @@ export const tabsSlice = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
addTab: (state, action) => {
|
||||
const { uid, collectionUid, type, requestPaneTab, preview } = action.payload;
|
||||
const nonReplaceableTabTypes = [
|
||||
"variables",
|
||||
"collection-runner",
|
||||
"security-settings",
|
||||
];
|
||||
|
||||
const existingTab = find(state.tabs, (tab) => tab.uid === uid);
|
||||
if (existingTab) {
|
||||
state.activeTabUid = existingTab.uid;
|
||||
const alreadyExists = find(state.tabs, (tab) => tab.uid === action.payload.uid);
|
||||
if (alreadyExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nonReplaceableTabTypes.includes(type)) {
|
||||
const existingTab = tabTypeAlreadyExists(state.tabs, collectionUid, type);
|
||||
if (existingTab) {
|
||||
state.activeTabUid = existingTab.uid;
|
||||
if (
|
||||
['variables', 'collection-settings', 'collection-overview', 'collection-runner', 'security-settings'].includes(action.payload.type)
|
||||
) {
|
||||
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
|
||||
if (tab) {
|
||||
state.activeTabUid = tab.uid;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const lastTab = state.tabs[state.tabs.length - 1];
|
||||
if (state.tabs.length > 0 && lastTab.preview) {
|
||||
state.tabs[state.tabs.length - 1] = {
|
||||
uid,
|
||||
collectionUid,
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: requestPaneTab || 'params',
|
||||
responsePaneTab: 'response',
|
||||
type: type || 'request',
|
||||
preview: preview !== undefined
|
||||
? preview
|
||||
: !nonReplaceableTabTypes.includes(type),
|
||||
...(uid ? { folderUid: uid } : {})
|
||||
}
|
||||
|
||||
state.activeTabUid = uid;
|
||||
return
|
||||
}
|
||||
|
||||
state.tabs.push({
|
||||
uid,
|
||||
collectionUid,
|
||||
uid: action.payload.uid,
|
||||
collectionUid: action.payload.collectionUid,
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: requestPaneTab || 'params',
|
||||
requestPaneTab: action.payload.requestPaneTab || 'params',
|
||||
responsePaneTab: 'response',
|
||||
type: type || 'request',
|
||||
...(uid ? { folderUid: uid } : {}),
|
||||
preview: preview !== undefined
|
||||
? preview
|
||||
: !nonReplaceableTabTypes.includes(type)
|
||||
type: action.payload.type || 'request',
|
||||
...(action.payload.uid ? { folderUid: action.payload.uid } : {})
|
||||
});
|
||||
state.activeTabUid = uid;
|
||||
state.activeTabUid = action.payload.uid;
|
||||
},
|
||||
focusTab: (state, action) => {
|
||||
state.activeTabUid = action.payload.uid;
|
||||
@@ -153,15 +124,6 @@ export const tabsSlice = createSlice({
|
||||
const collectionUid = action.payload.collectionUid;
|
||||
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
|
||||
state.activeTabUid = null;
|
||||
},
|
||||
makeTabPermanent: (state, action) => {
|
||||
const { uid } = action.payload;
|
||||
const tab = find(state.tabs, (t) => t.uid === uid);
|
||||
if (tab) {
|
||||
tab.preview = false;
|
||||
} else{
|
||||
console.error("Tab not found!")
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -174,8 +136,7 @@ export const {
|
||||
updateRequestPaneTab,
|
||||
updateResponsePaneTab,
|
||||
closeTabs,
|
||||
closeAllCollectionTabs,
|
||||
makeTabPermanent
|
||||
closeAllCollectionTabs
|
||||
} = tabsSlice.actions;
|
||||
|
||||
export default tabsSlice.reducer;
|
||||
export default tabsSlice.reducer;
|
||||
|
||||
@@ -14,8 +14,6 @@ const createContentType = (mode) => {
|
||||
return 'application/json';
|
||||
case 'multipartForm':
|
||||
return 'multipart/form-data';
|
||||
case 'file':
|
||||
return 'application/octet-stream';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -62,51 +60,22 @@ const createPostData = (body, type) => {
|
||||
}
|
||||
|
||||
const contentType = createContentType(body.mode);
|
||||
|
||||
switch (body.mode) {
|
||||
case 'formUrlEncoded':
|
||||
return {
|
||||
mimeType: contentType,
|
||||
text: new URLSearchParams(
|
||||
body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.reduce((acc, param) => {
|
||||
acc[param.name] = param.value;
|
||||
return acc;
|
||||
}, {})
|
||||
).toString(),
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value
|
||||
}))
|
||||
};
|
||||
case 'multipartForm':
|
||||
return {
|
||||
mimeType: contentType,
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
...(param.type === 'file' && { fileName: param.value })
|
||||
}))
|
||||
};
|
||||
case 'file':
|
||||
return {
|
||||
mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType,
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.selected)
|
||||
.map((param) => ({
|
||||
value: param.filePath,
|
||||
}))
|
||||
};
|
||||
default:
|
||||
return {
|
||||
mimeType: contentType,
|
||||
text: body[body.mode]
|
||||
};
|
||||
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
|
||||
return {
|
||||
mimeType: contentType,
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
...(param.type === 'file' && { fileName: param.value })
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
mimeType: contentType,
|
||||
text: body[body.mode]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,7 +89,6 @@ export const buildHarRequest = ({ request, headers, type }) => {
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body, type),
|
||||
headersSize: 0,
|
||||
bodySize: 0,
|
||||
binary: true
|
||||
bodySize: 0
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ export const deleteUidsInItems = (items) => {
|
||||
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
|
||||
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
|
||||
each(get(item, 'request.body.file'), (param) => delete param.uid);
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import find from 'lodash/find';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import isString from 'lodash/isString';
|
||||
import map from 'lodash/map';
|
||||
import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
@@ -272,17 +281,6 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
});
|
||||
};
|
||||
|
||||
const copyFileParams = (params = []) => {
|
||||
return map(params, (param) => {
|
||||
return {
|
||||
uid: param.uid,
|
||||
filePath: param.filePath,
|
||||
contentType: param.contentType,
|
||||
selected: param.selected
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const copyItems = (sourceItems, destItems) => {
|
||||
each(sourceItems, (si) => {
|
||||
if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') {
|
||||
@@ -310,8 +308,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
graphql: si.request.body.graphql,
|
||||
sparql: si.request.body.sparql,
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm),
|
||||
file: copyFileParams(si.request.body.file)
|
||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||
},
|
||||
script: si.request.script,
|
||||
vars: si.request.vars,
|
||||
@@ -664,10 +661,6 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
label = 'SPARQL';
|
||||
break;
|
||||
}
|
||||
case 'file': {
|
||||
label = 'File / Binary';
|
||||
break;
|
||||
}
|
||||
case 'formUrlEncoded': {
|
||||
label = 'Form URL Encoded';
|
||||
break;
|
||||
@@ -768,7 +761,6 @@ export const refreshUidsInItem = (item) => {
|
||||
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
|
||||
|
||||
return item;
|
||||
};
|
||||
@@ -779,13 +771,11 @@ export const deleteUidsInItem = (item) => {
|
||||
const headers = get(item, 'request.headers', []);
|
||||
const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);
|
||||
const bodyMultipartForm = get(item, 'request.body.multipartForm', []);
|
||||
const file = get(item, 'request.body.file', []);
|
||||
|
||||
params.forEach((param) => delete param.uid);
|
||||
headers.forEach((header) => delete header.uid);
|
||||
bodyFormUrlEncoded.forEach((param) => delete param.uid);
|
||||
bodyMultipartForm.forEach((param) => delete param.uid);
|
||||
file.forEach((param) => delete param.uid);
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
@@ -159,8 +159,6 @@ export const getCodeMirrorModeBasedOnContentType = (contentType, body) => {
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
return 'application/ld+json';
|
||||
} else if (contentType.includes('image')) {
|
||||
return 'application/image';
|
||||
} else if (contentType.includes('xml')) {
|
||||
return 'application/xml';
|
||||
} else if (contentType.includes('html')) {
|
||||
@@ -171,6 +169,8 @@ export const getCodeMirrorModeBasedOnContentType = (contentType, body) => {
|
||||
return 'application/xml';
|
||||
} else if (contentType.includes('yaml')) {
|
||||
return 'application/yaml';
|
||||
} else if (contentType.includes('image')) {
|
||||
return 'application/image';
|
||||
} else {
|
||||
return 'application/text';
|
||||
}
|
||||
|
||||
@@ -94,8 +94,6 @@ export const getContentType = (headers) => {
|
||||
if (contentType && contentType.length) {
|
||||
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
|
||||
return 'application/ld+json';
|
||||
} else if (typeof contentType[0] === 'string' && /^image\/svg\+xml/i.test(contentType[0])) {
|
||||
return 'image/svg+xml';
|
||||
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
|
||||
return 'application/xml';
|
||||
}
|
||||
|
||||
@@ -24,25 +24,11 @@ export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
|
||||
return relativePath ? relativePath.split(path.sep) : [];
|
||||
};
|
||||
|
||||
|
||||
export const isWindowsPath = (pathname) => {
|
||||
|
||||
if (!isWindowsOS()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for Windows drive letter format (e.g., "C:\")
|
||||
const hasDriveLetter = /^[a-zA-Z]:\\/.test(pathname);
|
||||
|
||||
// Check for UNC path format (e.g., "\\server\share") a.k.a. network path || WSL path
|
||||
const isUNCPath = pathname.startsWith('\\\\');
|
||||
|
||||
return hasDriveLetter || isUNCPath;
|
||||
};
|
||||
|
||||
|
||||
export const getDirectoryName = (pathname) => {
|
||||
return isWindowsPath(pathname) ? path.win32.dirname(pathname) : path.dirname(pathname);
|
||||
// convert to unix style path
|
||||
pathname = slash(pathname);
|
||||
|
||||
return path.dirname(pathname);
|
||||
};
|
||||
|
||||
export const isWindowsOS = () => {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import parseCurlCommand from './parse-curl';
|
||||
import * as querystring from 'query-string';
|
||||
import * as jsesc from 'jsesc';
|
||||
import * as path from 'path';
|
||||
|
||||
function getContentType(headers = {}) {
|
||||
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
|
||||
@@ -100,29 +99,8 @@ function getMultipleDataString(request, parsedQueryString) {
|
||||
function getFilesString(request) {
|
||||
const data = {};
|
||||
|
||||
data.data = {};
|
||||
|
||||
if (request.isDataBinary) {
|
||||
let filePath = '';
|
||||
|
||||
if (request.data.startsWith('@')) {
|
||||
filePath = request.data.slice(1);
|
||||
} else {
|
||||
filePath = request.data;
|
||||
}
|
||||
|
||||
data.data = [
|
||||
{
|
||||
filePath: repr(filePath),
|
||||
contentType: request.headers['Content-Type'],
|
||||
selected: true,
|
||||
}
|
||||
];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
data.files = {};
|
||||
data.data = {};
|
||||
|
||||
for (const multipartKey in request.multipartUploads) {
|
||||
const multipartValue = request.multipartUploads[multipartKey];
|
||||
@@ -162,7 +140,6 @@ const curlToJson = (curlCommand) => {
|
||||
requestJson.url = request.urlWithoutQuery;
|
||||
requestJson.raw_url = request.url;
|
||||
requestJson.method = request.method;
|
||||
requestJson.isDataBinary = request.isDataBinary;
|
||||
|
||||
if (request.cookies) {
|
||||
const cookies = {};
|
||||
@@ -184,10 +161,12 @@ const curlToJson = (curlCommand) => {
|
||||
|
||||
if (request.query) {
|
||||
requestJson.queries = getQueries(request);
|
||||
} else if (request.multipartUploads || request.isDataBinary) {
|
||||
Object.assign(requestJson, getFilesString(request));
|
||||
} else if (typeof request.data === 'string' || typeof request.data === 'number') {
|
||||
}
|
||||
|
||||
if (typeof request.data === 'string' || typeof request.data === 'number') {
|
||||
Object.assign(requestJson, getDataString(request));
|
||||
} else if (request.multipartUploads) {
|
||||
Object.assign(requestJson, getFilesString(request));
|
||||
}
|
||||
|
||||
if (request.insecure) {
|
||||
|
||||
@@ -86,38 +86,4 @@ describe('curlToJson', () => {
|
||||
method: 'get'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a parse a curl with a post body with binary file type', () => {
|
||||
const curlCommand = `curl 'https://www.usebruno.com'
|
||||
-H 'Accept: application/json, text/plain, */*'
|
||||
-H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'
|
||||
-H 'Content-Type: application/json;charset=utf-8'
|
||||
-H 'Origin: https://www.usebruno.com'
|
||||
-H 'Referer: https://www.usebruno.com/'
|
||||
--data-binary '@/path/to/file'
|
||||
`;
|
||||
|
||||
const result = curlToJson(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://www.usebruno.com',
|
||||
raw_url: 'https://www.usebruno.com',
|
||||
method: 'post',
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Origin: 'https://www.usebruno.com',
|
||||
Referer: 'https://www.usebruno.com/'
|
||||
},
|
||||
isDataBinary: true,
|
||||
data: [
|
||||
{
|
||||
filePath: '/path/to/file',
|
||||
contentType: 'application/json;charset=utf-8',
|
||||
selected: true
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,18 +50,14 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
|
||||
sparql: null,
|
||||
multipartForm: null,
|
||||
formUrlEncoded: null,
|
||||
graphql: null,
|
||||
file: null
|
||||
graphql: null
|
||||
};
|
||||
|
||||
if (parsedBody && contentType && typeof contentType === 'string') {
|
||||
if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
|
||||
body.mode = 'graphql';
|
||||
body.graphql = parseGraphQL(parsedBody);
|
||||
} else if (requestType === 'http-request' && request.isDataBinary) {
|
||||
body.mode = 'file';
|
||||
body.file = parsedBody;
|
||||
}else if (contentType.includes('application/json')) {
|
||||
} else if (contentType.includes('application/json')) {
|
||||
body.mode = 'json';
|
||||
body.json = convertToCodeMirrorJson(parsedBody);
|
||||
} else if (contentType.includes('xml')) {
|
||||
|
||||
@@ -35,7 +35,6 @@ export const updateUidsInCollection = (_collection) => {
|
||||
each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));
|
||||
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
updateItemUids(item.items);
|
||||
|
||||
@@ -229,27 +229,26 @@ const transformOpenapiRequestItem = (request) => {
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const resolveRefs = (spec, components = spec?.components, cache = new Map()) => {
|
||||
const resolveRefs = (spec, components = spec?.components, visitedItems = new Set()) => {
|
||||
if (!spec || typeof spec !== 'object') {
|
||||
return spec;
|
||||
}
|
||||
|
||||
if (cache.has(spec)) {
|
||||
return cache.get(spec);
|
||||
}
|
||||
|
||||
if (Array.isArray(spec)) {
|
||||
return spec.map(item => resolveRefs(item, components, cache));
|
||||
return spec.map((item) => resolveRefs(item, components, visitedItems));
|
||||
}
|
||||
|
||||
if ('$ref' in spec) {
|
||||
const refPath = spec.$ref;
|
||||
|
||||
if (cache.has(refPath)) {
|
||||
return cache.get(refPath);
|
||||
if (visitedItems.has(refPath)) {
|
||||
return spec;
|
||||
} else {
|
||||
visitedItems.add(refPath);
|
||||
}
|
||||
|
||||
if (refPath.startsWith('#/components/')) {
|
||||
// Local reference within components
|
||||
const refKeys = refPath.replace('#/components/', '').split('/');
|
||||
let ref = components;
|
||||
|
||||
@@ -257,26 +256,25 @@ const resolveRefs = (spec, components = spec?.components, cache = new Map()) =>
|
||||
if (ref && ref[key]) {
|
||||
ref = ref[key];
|
||||
} else {
|
||||
// Handle invalid references gracefully?
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
||||
cache.set(refPath, {});
|
||||
const resolved = resolveRefs(ref, components, cache);
|
||||
cache.set(refPath, resolved);
|
||||
return resolved;
|
||||
return resolveRefs(ref, components, visitedItems);
|
||||
} else {
|
||||
// Handle external references (not implemented here)
|
||||
// You would need to fetch the external reference and resolve it.
|
||||
// Example: Fetch and resolve an external reference from a URL.
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
const resolved = {};
|
||||
cache.set(spec, resolved);
|
||||
|
||||
for (const [key, value] of Object.entries(spec)) {
|
||||
resolved[key] = resolveRefs(value, components, cache);
|
||||
// Recursively resolve references in nested objects
|
||||
for (const prop in spec) {
|
||||
spec[prop] = resolveRefs(spec[prop], components, new Set(visitedItems));
|
||||
}
|
||||
|
||||
return resolved;
|
||||
return spec;
|
||||
};
|
||||
|
||||
const groupRequestsByTags = (requests) => {
|
||||
|
||||
@@ -422,7 +422,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
||||
brunoRequestItem.request.auth.mode = 'apikey';
|
||||
brunoRequestItem.request.auth.apikey = {
|
||||
key: authValues.key,
|
||||
value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it,
|
||||
value: authValues.value,
|
||||
placement: "header" //By default we are placing the apikey values in headers!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,6 @@ const replacements = {
|
||||
'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(',
|
||||
'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(',
|
||||
'postman\\.clearEnvironmentVariable\\(': 'bru.deleteEnvVar(',
|
||||
'pm\\.execution\\.skipRequest\\(\\)': 'bru.runner.skipRequest()',
|
||||
'pm\\.execution\\.skipRequest': 'bru.runner.skipRequest',
|
||||
'pm\\.execution\\.setNextRequest\\(null\\)': 'bru.runner.stopExecution()',
|
||||
'pm\\.execution\\.setNextRequest\\(\'null\'\\)': 'bru.runner.stopExecution()',
|
||||
};
|
||||
|
||||
const extendedReplacements = Object.keys(replacements).reduce((acc, key) => {
|
||||
@@ -54,7 +50,7 @@ export const postmanTranslation = (script, logCallback) => {
|
||||
}
|
||||
if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) {
|
||||
modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
|
||||
//logCallback?.();
|
||||
logCallback?.();
|
||||
}
|
||||
return modifiedScript;
|
||||
} catch (e) {
|
||||
|
||||
@@ -11,10 +11,3 @@ export const isItemAFolder = (item) => {
|
||||
export const itemIsOpenedInTabs = (item, tabs) => {
|
||||
return find(tabs, (t) => t.uid === item.uid);
|
||||
};
|
||||
|
||||
export const scrollToTheActiveTab = () => {
|
||||
const activeTab = document.querySelector('.request-tab.active');
|
||||
if (activeTab) {
|
||||
activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
@@ -52,7 +52,7 @@
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.5",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "^3.0.0",
|
||||
@@ -66,6 +66,7 @@
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ const { rpad } = require('../utils/common');
|
||||
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
const constants = require('../constants');
|
||||
const { findItemInCollection } = require('../utils/collection');
|
||||
const command = 'run [filename]';
|
||||
const desc = 'Run a request';
|
||||
|
||||
@@ -19,7 +18,6 @@ const printRunSummary = (results) => {
|
||||
let totalRequests = 0;
|
||||
let passedRequests = 0;
|
||||
let failedRequests = 0;
|
||||
let skippedRequests = 0;
|
||||
let totalAssertions = 0;
|
||||
let passedAssertions = 0;
|
||||
let failedAssertions = 0;
|
||||
@@ -51,10 +49,7 @@ const printRunSummary = (results) => {
|
||||
failedAssertions += 1;
|
||||
}
|
||||
}
|
||||
if (!hasAnyTestsOrAssertions && result.skipped) {
|
||||
skippedRequests += 1;
|
||||
}
|
||||
else if (!hasAnyTestsOrAssertions && result.error) {
|
||||
if (!hasAnyTestsOrAssertions && result.error) {
|
||||
failedRequests += 1;
|
||||
} else {
|
||||
passedRequests += 1;
|
||||
@@ -67,9 +62,6 @@ const printRunSummary = (results) => {
|
||||
if (failedRequests > 0) {
|
||||
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
|
||||
}
|
||||
if (skippedRequests > 0) {
|
||||
requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
|
||||
}
|
||||
requestSummary += `, ${totalRequests} total`;
|
||||
|
||||
let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`;
|
||||
@@ -92,7 +84,6 @@ const printRunSummary = (results) => {
|
||||
totalRequests,
|
||||
passedRequests,
|
||||
failedRequests,
|
||||
skippedRequests,
|
||||
totalAssertions,
|
||||
passedAssertions,
|
||||
failedAssertions,
|
||||
@@ -153,7 +144,7 @@ const createCollectionFromPath = (collectionPath) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
return currentDirItems;
|
||||
return currentDirItems
|
||||
};
|
||||
collection.items = traverse(collectionPath);
|
||||
return collection;
|
||||
@@ -643,34 +634,6 @@ const handler = async function (argv) {
|
||||
}
|
||||
|
||||
const runtime = getJsSandboxRuntime(sandbox);
|
||||
|
||||
const runSingleRequestByPathname = async (relativeItemPathname) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let itemPathname = path.join(collectionPath, relativeItemPathname);
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const bruJson = cloneDeep(findItemInCollection(collection, itemPathname));
|
||||
if (bruJson) {
|
||||
const res = await runSingleRequest(
|
||||
itemPathname,
|
||||
bruJson,
|
||||
collectionPath,
|
||||
runtimeVariables,
|
||||
envVars,
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
);
|
||||
resolve(res?.response);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
});
|
||||
}
|
||||
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
while (currentRequestIndex < bruJsons.length) {
|
||||
@@ -688,8 +651,7 @@ const handler = async function (argv) {
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
collection
|
||||
);
|
||||
|
||||
results.push({
|
||||
@@ -739,11 +701,6 @@ const handler = async function (argv) {
|
||||
|
||||
// determine next request
|
||||
const nextRequestName = result?.nextRequestName;
|
||||
|
||||
if (result?.shouldStopRunnerExecution) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextRequestName !== undefined) {
|
||||
nJumps++;
|
||||
if (nJumps > 10000) {
|
||||
|
||||
@@ -62,10 +62,7 @@ const makeJUnitOutput = async (results, outputPath) => {
|
||||
suite.testcase.push(testcase);
|
||||
});
|
||||
|
||||
if (result?.skipped) {
|
||||
suite['@skipped'] = 1;
|
||||
}
|
||||
else if (result.error) {
|
||||
if (result.error) {
|
||||
suite['@errors'] = 1;
|
||||
suite['@tests'] = 1;
|
||||
suite.testcase = [
|
||||
|
||||
@@ -40,13 +40,11 @@ const runSingleRequest = async function (
|
||||
brunoConfig,
|
||||
collectionRoot,
|
||||
runtime,
|
||||
collection,
|
||||
runSingleRequestByPathname
|
||||
collection
|
||||
) {
|
||||
try {
|
||||
let request;
|
||||
let nextRequestName;
|
||||
let shouldStopRunnerExecution = false;
|
||||
let item = {
|
||||
pathname: path.join(collectionPath, filename),
|
||||
...bruJson
|
||||
@@ -70,41 +68,11 @@ const runSingleRequest = async function (
|
||||
collectionPath,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
scriptingConfig
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
|
||||
if (result?.skipRequest) {
|
||||
return {
|
||||
test: {
|
||||
filename: filename
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: 'skipped',
|
||||
statusText: 'request skipped via pre-request script',
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: 'Request has been skipped from pre-request script',
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@@ -265,30 +233,7 @@ const runSingleRequest = async function (
|
||||
if (!options.disableCookies) {
|
||||
const cookieString = getCookieStringForUrl(request.url);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
const existingCookieHeaderName = Object.keys(request.headers).find(
|
||||
name => name.toLowerCase() === 'cookie'
|
||||
);
|
||||
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
|
||||
|
||||
// Helper function to parse cookies into an object
|
||||
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
|
||||
const [name, ...rest] = cookie.split('=');
|
||||
if (name && name.trim()) {
|
||||
cookies[name.trim()] = rest.join('=').trim();
|
||||
}
|
||||
return cookies;
|
||||
}, {});
|
||||
|
||||
const mergedCookies = {
|
||||
...parseCookies(existingCookieString),
|
||||
...parseCookies(cookieString),
|
||||
};
|
||||
|
||||
const combinedCookieString = Object.entries(mergedCookies)
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
|
||||
request.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,8 +323,7 @@ const runSingleRequest = async function (
|
||||
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
nextRequestName: nextRequestName
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -419,16 +363,11 @@ const runSingleRequest = async function (
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
scriptingConfig
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
}
|
||||
|
||||
// run assertions
|
||||
@@ -469,18 +408,13 @@ const runSingleRequest = async function (
|
||||
collectionPath,
|
||||
null,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runSingleRequestByPathname
|
||||
scriptingConfig
|
||||
);
|
||||
testResults = get(result, 'results', []);
|
||||
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
|
||||
if (result?.stopExecution) {
|
||||
shouldStopRunnerExecution = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (testResults?.length) {
|
||||
@@ -513,8 +447,7 @@ const runSingleRequest = async function (
|
||||
error: null,
|
||||
assertionResults,
|
||||
testResults,
|
||||
nextRequestName: nextRequestName,
|
||||
shouldStopRunnerExecution
|
||||
nextRequestName: nextRequestName
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
|
||||
@@ -204,6 +204,5 @@ module.exports = {
|
||||
mergeHeaders,
|
||||
mergeVars,
|
||||
mergeScripts,
|
||||
findItemInCollection,
|
||||
getTreePathFromCollectionToItem
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"dev": "electron .",
|
||||
"debug": "electron . --inspect=9229",
|
||||
"dist:mac": "electron-builder --mac --config electron-builder-config.js",
|
||||
"dist:win": "electron-builder --win --config electron-builder-config.js",
|
||||
"dist:linux": "electron-builder --linux AppImage --config electron-builder-config.js",
|
||||
@@ -34,7 +33,7 @@
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"about-window": "^1.15.2",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.5",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
const { sizeInMB } = require("../../utils/filesystem");
|
||||
const WorkerQueue = require("../../workers");
|
||||
const path = require("path");
|
||||
|
||||
const getSize = (data) => {
|
||||
return sizeInMB(typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8'));
|
||||
return typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -13,13 +12,7 @@ const getSize = (data) => {
|
||||
* This helps with parsing performance.
|
||||
*/
|
||||
const LANES = [{
|
||||
maxSize: 0.005
|
||||
},{
|
||||
maxSize: 0.1
|
||||
},{
|
||||
maxSize: 1
|
||||
},{
|
||||
maxSize: 10
|
||||
},{
|
||||
maxSize: 100
|
||||
}];
|
||||
|
||||
@@ -20,6 +20,7 @@ const {
|
||||
normalizeWslPath,
|
||||
normalizeAndResolvePath,
|
||||
safeToRename,
|
||||
sanitizeCollectionName,
|
||||
isWindowsOS,
|
||||
isValidFilename,
|
||||
hasSubDirectories,
|
||||
@@ -61,11 +62,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
});
|
||||
|
||||
// browse directory for file
|
||||
ipcMain.handle('renderer:browse-files', async (_, filters, properties) => {
|
||||
ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters) => {
|
||||
try {
|
||||
return await browseFiles(mainWindow, filters, properties);
|
||||
const filePaths = await browseFiles(mainWindow, filters);
|
||||
|
||||
return filePaths;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,6 +78,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
async (event, collectionName, collectionFolderName, collectionLocation) => {
|
||||
try {
|
||||
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
|
||||
collectionName = sanitizeCollectionName(collectionName);
|
||||
const dirPath = path.join(collectionLocation, collectionFolderName);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
@@ -116,7 +120,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle(
|
||||
'renderer:clone-collection',
|
||||
async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {
|
||||
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
|
||||
collectionFolderName = sanitizeCollectionName(collectionFolderName);
|
||||
const dirPath = path.join(collectionLocation, collectionFolderName);
|
||||
if (fs.existsSync(dirPath)) {
|
||||
throw new Error(`collection: ${dirPath} already exists`);
|
||||
@@ -137,7 +141,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// Change new name of collection
|
||||
let brunoConfig = JSON.parse(content);
|
||||
brunoConfig.name = collectionName;
|
||||
const cont = await stringifyJson(brunoConfig);
|
||||
const cont = await stringifyJson(json);
|
||||
|
||||
// write the bruno.json to new dir
|
||||
await writeFile(path.join(dirPath, 'bruno.json'), cont);
|
||||
@@ -166,6 +170,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// rename collection
|
||||
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
|
||||
try {
|
||||
newName = sanitizeCollectionName(newName);
|
||||
const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json');
|
||||
const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
|
||||
const json = JSON.parse(content);
|
||||
@@ -516,13 +521,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:update-collection-paths', async (_, collectionPaths) => {
|
||||
lastOpenedCollections.update(collectionPaths);
|
||||
})
|
||||
|
||||
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
|
||||
try {
|
||||
let collectionName = sanitizeDirectoryName(collection.name);
|
||||
let collectionName = sanitizeCollectionName(collection.name);
|
||||
let collectionPath = path.join(collectionLocation, collectionName);
|
||||
|
||||
if (fs.existsSync(collectionPath)) {
|
||||
@@ -908,18 +909,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync);
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:show-in-folder', async (event, filePath) => {
|
||||
try {
|
||||
if (!filePath) {
|
||||
throw new Error('File path is required');
|
||||
}
|
||||
shell.showItemInFolder(filePath);
|
||||
} catch (error) {
|
||||
console.error('Error in show-in-folder: ', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
|
||||
@@ -28,7 +28,7 @@ const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
||||
const { addDigestInterceptor } = require('./digestauth-helper');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
|
||||
const { chooseFileToSave, writeFile } = require('../../utils/filesystem');
|
||||
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
|
||||
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
|
||||
const {
|
||||
resolveOAuth2AuthorizationCodeAccessToken,
|
||||
@@ -274,6 +274,7 @@ const configureRequest = async (
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let axiosInstance = makeAxiosInstance();
|
||||
|
||||
if (request.ntlmConfig) {
|
||||
@@ -332,30 +333,7 @@ const configureRequest = async (
|
||||
if (preferencesUtil.shouldSendCookies()) {
|
||||
const cookieString = getCookieStringForUrl(request.url);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
const existingCookieHeaderName = Object.keys(request.headers).find(
|
||||
name => name.toLowerCase() === 'cookie'
|
||||
);
|
||||
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
|
||||
|
||||
// Helper function to parse cookies into an object
|
||||
const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
|
||||
const [name, ...rest] = cookie.split('=');
|
||||
if (name && name.trim()) {
|
||||
cookies[name.trim()] = rest.join('=').trim();
|
||||
}
|
||||
return cookies;
|
||||
}, {});
|
||||
|
||||
const mergedCookies = {
|
||||
...parseCookies(existingCookieString),
|
||||
...parseCookies(cookieString),
|
||||
};
|
||||
|
||||
const combinedCookieString = Object.entries(mergedCookies)
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
|
||||
request.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +403,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -459,8 +437,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@@ -494,7 +470,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -531,8 +507,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (result?.error) {
|
||||
mainWindow.webContents.send('main:display-error', result.error);
|
||||
}
|
||||
|
||||
collection.globalEnvironmentVariables = result.globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
// run post-response script
|
||||
@@ -563,13 +537,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:global-environment-variables-update', {
|
||||
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
}
|
||||
return scriptResult;
|
||||
};
|
||||
|
||||
const runRequest = async ({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground = false }) => {
|
||||
const runRequest = async ({ item, collection, environment, runtimeVariables, runInBackground = false }) => {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const cancelTokenUid = uuid();
|
||||
@@ -581,9 +553,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
|
||||
const _item = findItemInCollectionByPathname(collection, itemPathname);
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
|
||||
const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
@@ -598,23 +570,26 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const request = prepareRequest(item, collection);
|
||||
request.__bruno__executionMode = 'standalone';
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
try {
|
||||
request.signal = abortController.signal;
|
||||
saveCancelToken(cancelTokenUid, abortController);
|
||||
const controller = new AbortController();
|
||||
request.signal = controller.signal;
|
||||
saveCancelToken(cancelTokenUid, controller);
|
||||
|
||||
await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -637,7 +612,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
data: request.mode == 'file'? "<request body redacted>": safeParseJSON(safeStringifyJSON(request.data)) ,
|
||||
data: safeParseJSON(safeStringifyJSON(request.data)),
|
||||
timestamp: Date.now()
|
||||
},
|
||||
collectionUid,
|
||||
@@ -699,7 +674,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -783,10 +758,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
|
||||
const collectionUid = collection.uid;
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });
|
||||
return await runRequest({ item, collection, environment, runtimeVariables });
|
||||
});
|
||||
|
||||
ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => {
|
||||
@@ -810,7 +782,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -846,7 +818,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -916,7 +888,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -940,7 +912,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -977,8 +949,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
let stopRunnerExecution = false;
|
||||
|
||||
const abortController = new AbortController();
|
||||
@@ -990,9 +961,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (itemPathname && !itemPathname?.endsWith('.bru')) {
|
||||
itemPathname = `${itemPathname}.bru`;
|
||||
}
|
||||
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
|
||||
const _item = findItemInCollectionByPathname(collection, itemPathname);
|
||||
if(_item) {
|
||||
const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
|
||||
const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
|
||||
resolve(res);
|
||||
}
|
||||
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
|
||||
@@ -1012,6 +983,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
try {
|
||||
const envVars = getEnvVars(environment);
|
||||
let folderRequests = [];
|
||||
|
||||
if (recursive) {
|
||||
@@ -1059,10 +1031,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
const request = prepareRequest(item, collection);
|
||||
request.__bruno__executionMode = 'runner';
|
||||
|
||||
const requestUid = uuid();
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
|
||||
try {
|
||||
const preRequestScriptResult = await runPreRequest(
|
||||
@@ -1070,7 +1043,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -1208,7 +1181,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionRoot,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
@@ -1394,7 +1367,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
if (encoding === 'utf-8') {
|
||||
await writeFile(filePath, data);
|
||||
} else {
|
||||
await writeFile(filePath, data, true);
|
||||
await writeBinaryFile(filePath, data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -65,11 +65,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
/*
|
||||
We explicitly avoid interpolating buffer values because the file content is read as a buffer object in raw body mode.
|
||||
Even if the selected file's content type is JSON, this prevents the buffer object from being interpolated.
|
||||
*/
|
||||
if (contentType.includes('json') && !Buffer.isBuffer(request.data)) {
|
||||
if (contentType.includes('json')) {
|
||||
if (typeof request.data === 'string') {
|
||||
if (request.data.length) {
|
||||
request.data = _interpolate(request.data);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
const { get, each, filter, find } = require('lodash');
|
||||
const { get, each, filter } = require('lodash');
|
||||
const decomment = require('decomment');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs/promises');
|
||||
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
|
||||
const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
|
||||
const path = require('node:path');
|
||||
|
||||
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
|
||||
@@ -176,10 +174,10 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
return axiosRequest;
|
||||
};
|
||||
|
||||
const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
const prepareRequest = (item, collection) => {
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const collectionPath = collection?.pathname;
|
||||
const collectionPath = collection.pathname;
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
let url = request.url;
|
||||
@@ -191,7 +189,7 @@ const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
}
|
||||
});
|
||||
|
||||
const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich';
|
||||
const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
mergeHeaders(collection, request, requestTreePath);
|
||||
@@ -253,31 +251,6 @@ const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
axiosRequest.data = request.body.sparql;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'file') {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads
|
||||
}
|
||||
|
||||
const bodyFile = find(request.body.file, (param) => param.selected);
|
||||
if (bodyFile) {
|
||||
let { filePath, contentType } = bodyFile;
|
||||
|
||||
axiosRequest.headers['content-type'] = contentType;
|
||||
if (filePath) {
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
filePath = path.join(collectionPath, filePath);
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath);
|
||||
axiosRequest.data = fileContent;
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request.body.mode === 'formUrlEncoded') {
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
|
||||
@@ -16,20 +16,18 @@ class LastOpenedCollections {
|
||||
}
|
||||
|
||||
add(collectionPath) {
|
||||
const collections = this.getAll();
|
||||
const collections = this.store.get('lastOpenedCollections') || [];
|
||||
|
||||
if (isDirectory(collectionPath) && !collections.includes(collectionPath)) {
|
||||
collections.push(collectionPath);
|
||||
this.store.set('lastOpenedCollections', collections);
|
||||
if (isDirectory(collectionPath)) {
|
||||
if (!collections.includes(collectionPath)) {
|
||||
collections.push(collectionPath);
|
||||
this.store.set('lastOpenedCollections', collections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(collectionPaths) {
|
||||
this.store.set('lastOpenedCollections', collectionPaths);
|
||||
}
|
||||
|
||||
remove(collectionPath) {
|
||||
let collections = this.getAll();
|
||||
let collections = this.store.get('lastOpenedCollections') || [];
|
||||
|
||||
if (collections.includes(collectionPath)) {
|
||||
collections = _.filter(collections, (c) => c !== collectionPath);
|
||||
@@ -38,7 +36,7 @@ class LastOpenedCollections {
|
||||
}
|
||||
|
||||
removeAll() {
|
||||
this.store.set('lastOpenedCollections', []);
|
||||
return this.store.set('lastOpenedCollections', []);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let collectionHeaders = get(collection, 'root.request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name?.toLowerCase?.(), header.value);
|
||||
headers.set(header.name, header.value);
|
||||
if (header?.name?.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
@@ -23,14 +23,14 @@ const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let _headers = get(i, 'root.request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name?.toLowerCase?.(), header.value);
|
||||
headers.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name?.toLowerCase?.(), header.value);
|
||||
headers.set(header.name, header.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -240,7 +240,6 @@ const hydrateRequestWithUuid = (request, pathname) => {
|
||||
const assertions = get(request, 'request.assertions', []);
|
||||
const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []);
|
||||
const bodyMultipartForm = get(request, 'request.body.multipartForm', []);
|
||||
const file = get(request, 'request.body.file', []);
|
||||
|
||||
params.forEach((param) => (param.uid = uuid()));
|
||||
headers.forEach((header) => (header.uid = uuid()));
|
||||
@@ -249,7 +248,6 @@ const hydrateRequestWithUuid = (request, pathname) => {
|
||||
assertions.forEach((assertion) => (assertion.uid = uuid()));
|
||||
bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
|
||||
bodyMultipartForm.forEach((param) => (param.uid = uuid()));
|
||||
file.forEach((param) => (param.uid = uuid()));
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
@@ -68,13 +68,20 @@ function normalizeWslPath(pathname) {
|
||||
return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
const writeFile = async (pathname, content, isBinary = false) => {
|
||||
const writeFile = async (pathname, content) => {
|
||||
try {
|
||||
await fs.writeFile(pathname, content, {
|
||||
encoding: !isBinary ? "utf-8" : null
|
||||
fs.writeFileSync(pathname, content, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error writing file at ${pathname}:`, err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
const writeBinaryFile = async (pathname, content) => {
|
||||
try {
|
||||
fs.writeFileSync(pathname, content);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
};
|
||||
@@ -114,9 +121,9 @@ const browseDirectory = async (win) => {
|
||||
return isDirectory(resolvedPath) ? resolvedPath : false;
|
||||
};
|
||||
|
||||
const browseFiles = async (win, filters = [], properties = []) => {
|
||||
const browseFiles = async (win, filters) => {
|
||||
const { filePaths } = await dialog.showOpenDialog(win, {
|
||||
properties: ['openFile', ...properties],
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters
|
||||
});
|
||||
|
||||
@@ -154,6 +161,10 @@ const searchForBruFiles = (dir) => {
|
||||
return searchForFiles(dir, '.bru');
|
||||
};
|
||||
|
||||
const sanitizeCollectionName = (name) => {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
const sanitizeDirectoryName = (name) => {
|
||||
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim();
|
||||
};
|
||||
@@ -254,6 +265,7 @@ module.exports = {
|
||||
isWSLPath,
|
||||
normalizeWslPath,
|
||||
writeFile,
|
||||
writeBinaryFile,
|
||||
hasJsonExtension,
|
||||
hasBruExtension,
|
||||
createDirectory,
|
||||
@@ -263,6 +275,7 @@ module.exports = {
|
||||
searchForFiles,
|
||||
searchForBruFiles,
|
||||
sanitizeDirectoryName,
|
||||
sanitizeCollectionName,
|
||||
isWindowsOS,
|
||||
safeToRename,
|
||||
isValidFilename,
|
||||
|
||||
@@ -7,17 +7,15 @@ describe('prepare-request: prepareRequest', () => {
|
||||
describe('Decomments request body', () => {
|
||||
it('If request body is valid JSON', async () => {
|
||||
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
|
||||
const expected = `{
|
||||
\"test\": \"{{someVar}}\"
|
||||
}`;
|
||||
const result = await prepareRequest({ request: { body }, collection: { pathname: '' } });
|
||||
const expected = '{\n"test": "{{someVar}}" \n}';
|
||||
const result = prepareRequest({ request: { body } }, {});
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
|
||||
it('If request body is not valid JSON', async () => {
|
||||
const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' };
|
||||
const expected = '{\n"test": {{someVar}} \n}';
|
||||
const result = await prepareRequest({ request: { body }, collection: { pathname: '' } });
|
||||
const result = prepareRequest({ request: { body } }, {});
|
||||
expect(result.data).toEqual(expected);
|
||||
});
|
||||
|
||||
|
||||
@@ -21,11 +21,10 @@
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.5",
|
||||
"btoa": "^1.2.1",
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"json-query": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -35,8 +34,7 @@
|
||||
"node-vault": "^0.10.2",
|
||||
"path": "^0.12.7",
|
||||
"quickjs-emscripten": "^0.29.2",
|
||||
"uuid": "^9.0.0",
|
||||
"xml2js": "^0.6.2"
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const { get } = require('@usebruno/query');
|
||||
|
||||
class BrunoResponse {
|
||||
constructor(res) {
|
||||
this.res = res;
|
||||
@@ -8,13 +6,6 @@ class BrunoResponse {
|
||||
this.headers = res ? res.headers : null;
|
||||
this.body = res ? res.data : null;
|
||||
this.responseTime = res ? res.responseTime : null;
|
||||
|
||||
// Make the instance callable
|
||||
const callable = (...args) => get(this.body, ...args);
|
||||
Object.setPrototypeOf(callable, this.constructor.prototype);
|
||||
Object.assign(callable, this);
|
||||
|
||||
return callable;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
|
||||
@@ -28,8 +28,6 @@ const fetch = require('node-fetch');
|
||||
const chai = require('chai');
|
||||
const CryptoJS = require('crypto-js');
|
||||
const NodeVault = require('node-vault');
|
||||
const xml2js = require('xml2js');
|
||||
const cheerio = require('cheerio');
|
||||
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
||||
|
||||
class ScriptRuntime {
|
||||
@@ -147,8 +145,6 @@ class ScriptRuntime {
|
||||
chai,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS,
|
||||
'xml2js': xml2js,
|
||||
cheerio,
|
||||
...whitelistedModules,
|
||||
fs: allowScriptFilesystemAccess ? fs : undefined,
|
||||
'node-vault': NodeVault
|
||||
|
||||
@@ -30,8 +30,6 @@ const axios = require('axios');
|
||||
const fetch = require('node-fetch');
|
||||
const CryptoJS = require('crypto-js');
|
||||
const NodeVault = require('node-vault');
|
||||
const xml2js = require('xml2js');
|
||||
const cheerio = require('cheerio');
|
||||
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
||||
|
||||
const getResultsSummary = (results) => {
|
||||
@@ -207,8 +205,6 @@ class TestRuntime {
|
||||
chai,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS,
|
||||
'xml2js': xml2js,
|
||||
cheerio,
|
||||
...whitelistedModules,
|
||||
fs: allowScriptFilesystemAccess ? fs : undefined,
|
||||
'node-vault': NodeVault
|
||||
|
||||
@@ -25,7 +25,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
|
||||
auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey
|
||||
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
|
||||
bodyforms = bodyformurlencoded | bodymultipart | bodyfile
|
||||
bodyforms = bodyformurlencoded | bodymultipart
|
||||
params = paramspath | paramsquery
|
||||
|
||||
nl = "\\r"? "\\n"
|
||||
@@ -102,8 +102,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
|
||||
bodyformurlencoded = "body:form-urlencoded" dictionary
|
||||
bodymultipart = "body:multipart-form" dictionary
|
||||
bodyfile = "body:file" dictionary
|
||||
|
||||
|
||||
script = scriptreq | scriptres
|
||||
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
|
||||
scriptres = "script:post-response" st* "{" nl* textblock tagend
|
||||
@@ -174,19 +173,6 @@ const multipartExtractContentType = (pair) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fileExtractContentType = (pair) => {
|
||||
if (_.isString(pair.value)) {
|
||||
const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
|
||||
if (match && match.length > 2) {
|
||||
pair.value = match[1].trim();
|
||||
pair.contentType = match[2].trim();
|
||||
} else {
|
||||
pair.contentType = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
|
||||
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
|
||||
|
||||
@@ -204,27 +190,6 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) =
|
||||
});
|
||||
};
|
||||
|
||||
const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => {
|
||||
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
|
||||
return pairs.map((pair) => {
|
||||
fileExtractContentType(pair);
|
||||
|
||||
if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
|
||||
let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
|
||||
pair.filePath = filePath;
|
||||
pair.selected = pair.enabled
|
||||
|
||||
// Remove pair.value as it only contains the file path reference
|
||||
delete pair.value;
|
||||
// Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.)
|
||||
delete pair.name;
|
||||
delete pair.enabled;
|
||||
}
|
||||
|
||||
return pair;
|
||||
});
|
||||
};
|
||||
|
||||
const concatArrays = (objValue, srcValue) => {
|
||||
if (_.isArray(objValue) && _.isArray(srcValue)) {
|
||||
return objValue.concat(srcValue);
|
||||
@@ -609,13 +574,6 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
}
|
||||
};
|
||||
},
|
||||
bodyfile(_1, dictionary) {
|
||||
return {
|
||||
body: {
|
||||
file: mapPairListToKeyValPairsFile(dictionary.ast)
|
||||
}
|
||||
};
|
||||
},
|
||||
body(_1, _2, _3, _4, textblock, _5) {
|
||||
return {
|
||||
http: {
|
||||
@@ -750,4 +708,3 @@ const parser = (input) => {
|
||||
};
|
||||
|
||||
module.exports = parser;
|
||||
|
||||
@@ -2,8 +2,8 @@ const _ = require('lodash');
|
||||
|
||||
const { indentString } = require('../../v1/src/utils');
|
||||
|
||||
const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
|
||||
const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
|
||||
const enabled = (items = []) => items.filter((item) => item.enabled);
|
||||
const disabled = (items = []) => items.filter((item) => !item.enabled);
|
||||
|
||||
// remove the last line if two new lines are found
|
||||
const stripLastLine = (text) => {
|
||||
@@ -313,30 +313,6 @@ ${indentString(body.sparql)}
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
|
||||
if (body && body.file && body.file.length) {
|
||||
bru += `body:file {`;
|
||||
const files = enabled(body.file, "selected").concat(disabled(body.file, "selected"));
|
||||
|
||||
if (files.length) {
|
||||
bru += `\n${indentString(
|
||||
files
|
||||
.map((item) => {
|
||||
const selected = item.selected ? '' : '~';
|
||||
const contentType =
|
||||
item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
|
||||
const filePath = item.filePath || '';
|
||||
const value = `@file(${filePath})`;
|
||||
const itemName = "file";
|
||||
return `${selected}${itemName}: ${value}${contentType}`;
|
||||
})
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
if (body && body.graphql && body.graphql.query) {
|
||||
bru += `body:graphql {\n`;
|
||||
bru += `${indentString(body.graphql.query)}`;
|
||||
|
||||
@@ -102,12 +102,6 @@ body:multipart-form {
|
||||
~message: hello
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(path/to/file.json) @contentType(application/json)
|
||||
file: @file(path/to/file.json) @contentType(application/json)
|
||||
~file: @file(path/to/file2.json) @contentType(application/json)
|
||||
}
|
||||
|
||||
body:graphql {
|
||||
{
|
||||
launchesPast {
|
||||
|
||||
@@ -137,23 +137,6 @@
|
||||
"enabled": false,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"file" : [
|
||||
{
|
||||
"filePath": "path/to/file.json",
|
||||
"contentType": "application/json",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"filePath": "path/to/file.json",
|
||||
"contentType": "application/json",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"filePath": "path/to/file2.json",
|
||||
"contentType": "application/json",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"vars": {
|
||||
|
||||
@@ -74,19 +74,9 @@ const multipartFormSchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
|
||||
const fileSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
filePath: Yup.string().nullable(),
|
||||
contentType: Yup.string().nullable(),
|
||||
selected: Yup.boolean()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const requestBodySchema = Yup.object({
|
||||
mode: Yup.string()
|
||||
.oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'file'])
|
||||
.oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql'])
|
||||
.required('mode is required'),
|
||||
json: Yup.string().nullable(),
|
||||
text: Yup.string().nullable(),
|
||||
@@ -94,8 +84,7 @@ const requestBodySchema = Yup.object({
|
||||
sparql: Yup.string().nullable(),
|
||||
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
|
||||
multipartForm: Yup.array().of(multipartFormSchema).nullable(),
|
||||
graphql: graphqlBodySchema.nullable(),
|
||||
file: Yup.array().of(fileSchema).nullable()
|
||||
graphql: graphqlBodySchema.nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: echo binary
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: file
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(bruno.png) @contentType(image/png)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
file.txt
|
||||
|
||||
hello, bruno
|
||||
@@ -9,7 +9,3 @@ get {
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.runner.stopExecution();
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
meta {
|
||||
name: runRequest-1
|
||||
type: http
|
||||
seq: 10
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:text {
|
||||
bruno
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
// reset values
|
||||
bru.setVar('run-request-runtime-var', null);
|
||||
bru.setEnvVar('run-request-env-var', null);
|
||||
bru.setGlobalEnvVar('run-request-global-env-var', null);
|
||||
|
||||
// the above vars will be set in the below request
|
||||
const resp = await bru.runRequest('scripting/api/bru/runRequest-2');
|
||||
|
||||
bru.setVar('run-request-resp', {
|
||||
data: resp?.data,
|
||||
statusText: resp?.statusText,
|
||||
status: resp?.status
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
test("should get runtime var set in runRequest-2", function() {
|
||||
const val = bru.getVar("run-request-runtime-var");
|
||||
expect(val).to.equal("run-request-runtime-var-value");
|
||||
});
|
||||
|
||||
test("should get env var set in runRequest-2", function() {
|
||||
const val = bru.getEnvVar("run-request-env-var");
|
||||
expect(val).to.equal("run-request-env-var-value");
|
||||
});
|
||||
|
||||
test("should get global env var set in runRequest-2", function() {
|
||||
const val = bru.getGlobalEnvVar("run-request-global-env-var");
|
||||
const executionMode = req.getExecutionMode();
|
||||
if (executionMode == 'runner') {
|
||||
expect(val).to.equal("run-request-global-env-var-value");
|
||||
}
|
||||
});
|
||||
|
||||
test("should get response of runRequest-2", function() {
|
||||
const val = bru.getVar('run-request-resp');
|
||||
expect(JSON.stringify(val)).to.equal(JSON.stringify({
|
||||
"data": "bruno",
|
||||
"statusText": "OK",
|
||||
"status": 200
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
meta {
|
||||
name: runRequest-2
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:text {
|
||||
bruno
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar('run-request-runtime-var', 'run-request-runtime-var-value');
|
||||
bru.setEnvVar('run-request-env-var', 'run-request-env-var-value');
|
||||
bru.setGlobalEnvVar('run-request-global-env-var', 'run-request-global-env-var-value');
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
meta {
|
||||
name: runRequest
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
foo: bar
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: asd
|
||||
password: j
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token:
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"hello": "bruno"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar("runRequest-ping-res-1", null);
|
||||
bru.setVar("runRequest-ping-res-2", null);
|
||||
bru.setVar("runRequest-ping-res-3", null);
|
||||
|
||||
let pingRes = await bru.runRequest('ping');
|
||||
bru.setVar('runRequest-ping-res-1', {
|
||||
data: pingRes?.data,
|
||||
statusText: pingRes?.statusText,
|
||||
status: pingRes?.status
|
||||
});
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
let pingRes = await bru.runRequest('ping');
|
||||
bru.setVar('runRequest-ping-res-2', {
|
||||
data: pingRes?.data,
|
||||
statusText: pingRes?.statusText,
|
||||
status: pingRes?.status
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
const pingRes = await bru.runRequest('ping');
|
||||
bru.setVar('runRequest-ping-res-3', {
|
||||
data: pingRes?.data,
|
||||
statusText: pingRes?.statusText,
|
||||
status: pingRes?.status
|
||||
});
|
||||
|
||||
test("should run request and return valid response in pre-request script", function() {
|
||||
const expectedPingRes = {
|
||||
data: "pong",
|
||||
statusText: "OK",
|
||||
status: 200
|
||||
};
|
||||
const pingRes = bru.getVar('runRequest-ping-res-1');
|
||||
expect(pingRes).to.eql(expectedPingRes);
|
||||
});
|
||||
|
||||
test("should run request and return valid response in post-response script", function() {
|
||||
const expectedPingRes = {
|
||||
data: "pong",
|
||||
statusText: "OK",
|
||||
status: 200
|
||||
};
|
||||
const pingRes = bru.getVar('runRequest-ping-res-2');
|
||||
expect(pingRes).to.eql(expectedPingRes);
|
||||
});
|
||||
|
||||
test("should run request and return valid response in tests script", function() {
|
||||
const expectedPingRes = {
|
||||
data: "pong",
|
||||
statusText: "OK",
|
||||
status: 200
|
||||
};
|
||||
const pingRes = bru.getVar('runRequest-ping-res-3');
|
||||
expect(pingRes).to.eql(expectedPingRes);
|
||||
});
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
meta {
|
||||
name: 1
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar('bru-runner-req', 1);
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setVar('bru.runner.skipRequest', true);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
meta {
|
||||
name: 2
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.runner.skipRequest();
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.setVar('bru.runner.skipRequest', false);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
meta {
|
||||
name: 3
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
meta {
|
||||
name: cheerio
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: text
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:text {
|
||||
<h2 class="title">Hello Bruno!</h2>
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
const $ = cheerio.load('<h2 class="title">Hello world</h2>');
|
||||
|
||||
$('h2.title').text('Hello there!');
|
||||
$('h2').addClass('welcome');
|
||||
|
||||
bru.setVar("cheerio-test-html", $.html());
|
||||
}
|
||||
|
||||
tests {
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
test("cheerio html - from scripts", function() {
|
||||
const expected = '<html><head></head><body><h2 class="title welcome">Hello there!</h2></body></html>';
|
||||
const html = bru.getVar('cheerio-test-html');
|
||||
expect(html).to.eql(expected);
|
||||
});
|
||||
|
||||
test("cheerio html - from tests", function() {
|
||||
const expected = '<html><head></head><body><h2 class="title">Hello Bruno!</h2></body></html>';
|
||||
const $ = cheerio.load(res.body);
|
||||
expect($.html()).to.eql(expected);
|
||||
});
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
meta {
|
||||
name: xml2js
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
var parseString = require('xml2js').parseString;
|
||||
var xml = "<root>Hello xml2js!</root>"
|
||||
parseString(xml, function (err, result) {
|
||||
bru.setVar("xml2js-test-result", result);
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
var parseString = require('xml2js').parseString;
|
||||
|
||||
test("xml2js parseString in scripts", function() {
|
||||
const expected = {
|
||||
root: 'Hello xml2js!'
|
||||
};
|
||||
const result = bru.getVar('xml2js-test-result');
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
|
||||
test("xml2js parseString in tests", async function() {
|
||||
var xml = "<root>Hello inside test!</root>"
|
||||
const expected = {
|
||||
root: 'Hello inside test!'
|
||||
};
|
||||
parseString(xml, function (err, result) {
|
||||
expect(result).to.eql(expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/usebruno/bruno-testbench#readme",
|
||||
"dependencies": {
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.5",
|
||||
"body-parser": "1.20.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -19,17 +19,6 @@ router.post('/xml-raw', (req, res) => {
|
||||
return res.send(req.rawBody);
|
||||
});
|
||||
|
||||
router.post('/bin', (req, res) => {
|
||||
const rawBody = req.body;
|
||||
|
||||
if (!rawBody || rawBody.length === 0) {
|
||||
return res.status(400).send('No data received');
|
||||
}
|
||||
|
||||
res.set('Content-Type', req.headers['content-type'] || 'application/octet-stream');
|
||||
res.send(rawBody);
|
||||
});
|
||||
|
||||
router.get('/bom-json-test', (req, res) => {
|
||||
const jsonData = {
|
||||
message: 'Hello!',
|
||||
|
||||
@@ -10,7 +10,6 @@ const multipartRouter = require('./multipart');
|
||||
const app = new express();
|
||||
const port = process.env.PORT || 8080;
|
||||
|
||||
app.use(express.raw({type: '*/*', limit: '100mb'}));
|
||||
app.use(cors());
|
||||
app.use(xmlParser());
|
||||
app.use(bodyParser.text());
|
||||
|
||||
Reference in New Issue
Block a user