diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 08a657144..0fa88640a 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -58,6 +58,8 @@ jobs:
run: npm run test --workspace=packages/bruno-converters
- name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron
+ - name: Test Package bruno-requests
+ run: npm run test --workspace=packages/bruno-requests
cli-test:
name: CLI Tests
diff --git a/.gitignore b/.gitignore
index d2f68f452..fb650b306 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,7 @@ yarn-error.log*
bruno.iml
.idea
.vscode
+.cursor
# Playwright
/blob-report/
diff --git a/assets/images/landing-2-dark.png b/assets/images/landing-2-dark.png
new file mode 100644
index 000000000..8f69d5278
Binary files /dev/null and b/assets/images/landing-2-dark.png differ
diff --git a/assets/images/landing-2-light.png b/assets/images/landing-2-light.png
new file mode 100644
index 000000000..e18b23ab3
Binary files /dev/null and b/assets/images/landing-2-light.png differ
diff --git a/docs/readme/readme_ar.md b/docs/readme/readme_ar.md
index 6c03277df..b9f2f4aa0 100644
--- a/docs/readme/readme_ar.md
+++ b/docs/readme/readme_ar.md
@@ -3,7 +3,7 @@
### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs).
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_bn.md b/docs/readme/readme_bn.md
index cf9c2110f..601e2c98e 100644
--- a/docs/readme/readme_bn.md
+++ b/docs/readme/readme_bn.md
@@ -3,7 +3,7 @@
### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE।
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_cn.md b/docs/readme/readme_cn.md
index f52a6f23f..01f86c137 100644
--- a/docs/readme/readme_cn.md
+++ b/docs/readme/readme_cn.md
@@ -3,7 +3,7 @@
### Bruno - 开源 IDE,用于探索和测试 API。
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_de.md b/docs/readme/readme_de.md
index 93eb09b22..572b7f25d 100644
--- a/docs/readme/readme_de.md
+++ b/docs/readme/readme_de.md
@@ -3,7 +3,7 @@
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_es.md b/docs/readme/readme_es.md
index bf747d49f..40e5f7c2b 100644
--- a/docs/readme/readme_es.md
+++ b/docs/readme/readme_es.md
@@ -3,7 +3,7 @@
### Bruno - IDE de código abierto para explorar y probar APIs.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_fa.md b/docs/readme/readme_fa.md
index 3e495d426..9335dc580 100644
--- a/docs/readme/readme_fa.md
+++ b/docs/readme/readme_fa.md
@@ -3,7 +3,7 @@
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_fr.md b/docs/readme/readme_fr.md
index 777ad42fc..0cd8897b3 100644
--- a/docs/readme/readme_fr.md
+++ b/docs/readme/readme_fr.md
@@ -3,7 +3,7 @@
### Bruno - IDE Opensource pour explorer et tester des APIs.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_it.md b/docs/readme/readme_it.md
index c74e01cdc..12e0888b2 100644
--- a/docs/readme/readme_it.md
+++ b/docs/readme/readme_it.md
@@ -3,7 +3,7 @@
### Bruno - Opensource IDE per esplorare e testare gli APIs.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_ja.md b/docs/readme/readme_ja.md
index 1c60c25ce..39598a0a6 100644
--- a/docs/readme/readme_ja.md
+++ b/docs/readme/readme_ja.md
@@ -3,7 +3,7 @@
### Bruno - API の検証・動作テストのためのオープンソース IDE.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_ka.md b/docs/readme/readme_ka.md
index ba8deb717..38c495e2f 100644
--- a/docs/readme/readme_ka.md
+++ b/docs/readme/readme_ka.md
@@ -3,7 +3,7 @@
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_kr.md b/docs/readme/readme_kr.md
index 219617a0a..1b63f518f 100644
--- a/docs/readme/readme_kr.md
+++ b/docs/readme/readme_kr.md
@@ -3,7 +3,7 @@
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_nl.md b/docs/readme/readme_nl.md
index 35fe292f8..6d35625f8 100644
--- a/docs/readme/readme_nl.md
+++ b/docs/readme/readme_nl.md
@@ -3,7 +3,7 @@
### Bruno - Open source IDE voor het verkennen en testen van API's.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_pl.md b/docs/readme/readme_pl.md
index 8c455a460..4193cfe68 100644
--- a/docs/readme/readme_pl.md
+++ b/docs/readme/readme_pl.md
@@ -3,7 +3,7 @@
### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_pt_br.md b/docs/readme/readme_pt_br.md
index 3e1d0793e..fdc504abe 100644
--- a/docs/readme/readme_pt_br.md
+++ b/docs/readme/readme_pt_br.md
@@ -3,7 +3,7 @@
### Bruno - IDE de código aberto para explorar e testar APIs.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_ro.md b/docs/readme/readme_ro.md
index 1c2ecf244..5e129d1a7 100644
--- a/docs/readme/readme_ro.md
+++ b/docs/readme/readme_ro.md
@@ -3,7 +3,7 @@
### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_ru.md b/docs/readme/readme_ru.md
index 9963d504c..30e7692aa 100644
--- a/docs/readme/readme_ru.md
+++ b/docs/readme/readme_ru.md
@@ -3,7 +3,7 @@
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_tr.md b/docs/readme/readme_tr.md
index ed7153bde..4738e9255 100644
--- a/docs/readme/readme_tr.md
+++ b/docs/readme/readme_tr.md
@@ -3,7 +3,7 @@
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_ua.md b/docs/readme/readme_ua.md
index a8a4bfd5a..ae0ab57fe 100644
--- a/docs/readme/readme_ua.md
+++ b/docs/readme/readme_ua.md
@@ -3,7 +3,7 @@
### Bruno - IDE із відкритим кодом для тестування та дослідження API
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/docs/readme/readme_zhtw.md b/docs/readme/readme_zhtw.md
index 183b7d25d..550bd5d21 100644
--- a/docs/readme/readme_zhtw.md
+++ b/docs/readme/readme_zhtw.md
@@ -3,7 +3,7 @@
### Bruno - 探索和測試 API 的開源 IDE 工具
-[](https://badge.fury.io/gh/usebruno%bruno)
+[](https://badge.fury.io/gh/usebruno%2Fbruno)
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[](https://github.com/usebruno/bruno/pulse)
[](https://twitter.com/use_bruno)
diff --git a/package-lock.json b/package-lock.json
index e7838e281..b3035fead 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6458,56 +6458,6 @@
"url": "https://opencollective.com/popperjs"
}
},
- "node_modules/@postman/form-data": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz",
- "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==",
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/@postman/tough-cookie": {
- "version": "4.1.3-postman.1",
- "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz",
- "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "psl": "^1.1.33",
- "punycode": "^2.1.1",
- "universalify": "^0.2.0",
- "url-parse": "^1.5.3"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/@postman/tough-cookie/node_modules/universalify": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
- "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
- "license": "MIT",
- "engines": {
- "node": ">= 4.0.0"
- }
- },
- "node_modules/@postman/tunnel-agent": {
- "version": "0.6.4",
- "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.4.tgz",
- "integrity": "sha512-CJJlq8V7rNKhAw4sBfjixKpJW00SHqebqNUQKxMoepgeWZIbdPcD+rguRcivGhS4N12PymDcKgUgSD4rVC+RjQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/@prantlf/jsonlint": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@prantlf/jsonlint/-/jsonlint-16.0.0.tgz",
@@ -11429,15 +11379,6 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
- "node_modules/asn1": {
- "version": "0.2.6",
- "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
- "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": "~2.1.0"
- }
- },
"node_modules/asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
@@ -11476,6 +11417,7 @@
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"license": "MIT",
+ "optional": true,
"engines": {
"node": ">=0.8"
}
@@ -11626,15 +11568,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/aws-sign2": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
- "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
- "license": "Apache-2.0",
- "engines": {
- "node": "*"
- }
- },
"node_modules/aws4": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
@@ -12028,15 +11961,6 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
- "node_modules/bcrypt-pbkdf": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
- "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "tweetnacl": "^0.14.3"
- }
- },
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -12229,15 +12153,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/brotli": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
- "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.1.2"
- }
- },
"node_modules/browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -12750,12 +12665,6 @@
"node": ">=4"
}
},
- "node_modules/caseless": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
- "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
- "license": "Apache-2.0"
- },
"node_modules/chai": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
@@ -14375,18 +14284,6 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
- "node_modules/dashdash": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
- "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
- "license": "MIT",
- "dependencies": {
- "assert-plus": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -15110,22 +15007,6 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
- "node_modules/ecc-jsbn": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
- "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
- "license": "MIT",
- "dependencies": {
- "jsbn": "~0.1.0",
- "safer-buffer": "^2.1.0"
- }
- },
- "node_modules/ecc-jsbn/node_modules/jsbn": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
- "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
- "license": "MIT"
- },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -16424,12 +16305,6 @@
"node": ">= 0.6"
}
},
- "node_modules/extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "license": "MIT"
- },
"node_modules/extract-files": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
@@ -16555,6 +16430,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "devOptional": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
@@ -17037,15 +16913,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/forever-agent": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
- "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
- "license": "Apache-2.0",
- "engines": {
- "node": "*"
- }
- },
"node_modules/fork-ts-checker-webpack-plugin": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz",
@@ -17524,15 +17391,6 @@
"node": ">=6.0"
}
},
- "node_modules/getpass": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
- "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
- "license": "MIT",
- "dependencies": {
- "assert-plus": "^1.0.0"
- }
- },
"node_modules/github-markdown-css": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz",
@@ -17815,57 +17673,12 @@
"@grpc/grpc-js": "^1.12.6"
}
},
- "node_modules/har-schema": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
- "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
- "license": "ISC",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/har-validator": {
- "version": "5.1.5",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
- "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
- "deprecated": "this library is no longer supported",
- "license": "MIT",
- "dependencies": {
- "ajv": "^6.12.3",
- "har-schema": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/har-validator-compiled": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/har-validator-compiled/-/har-validator-compiled-1.0.0.tgz",
"integrity": "sha512-dher7nFSx+Ef6OoqVveLClh8itAR3vd8Qx70Lh/hEgP1iGeARAolbci7Y8JBrHIYgFCT6xRdvvL16AR9Zh07Dw==",
"license": "MIT"
},
- "node_modules/har-validator/node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/har-validator/node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "license": "MIT"
- },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -18338,20 +18151,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
- "node_modules/http-signature": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
- "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
- "license": "MIT",
- "dependencies": {
- "assert-plus": "^1.0.0",
- "jsprim": "^2.0.2",
- "sshpk": "^1.14.1"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
@@ -19168,12 +18967,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-typedarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
- "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
- "license": "MIT"
- },
"node_modules/is-valid-path": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz",
@@ -19236,12 +19029,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/isstream": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
- "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
- "license": "MIT"
- },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -20822,12 +20609,6 @@
"node": "*"
}
},
- "node_modules/json-schema": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
- "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
- "license": "(AFL-2.1 OR BSD-3-Clause)"
- },
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -20851,7 +20632,9 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
- "license": "ISC"
+ "dev": true,
+ "license": "ISC",
+ "optional": true
},
"node_modules/json5": {
"version": "2.2.3",
@@ -20892,44 +20675,6 @@
"node": "*"
}
},
- "node_modules/jsprim": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
- "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
- "engines": [
- "node >=0.6.0"
- ],
- "license": "MIT",
- "dependencies": {
- "assert-plus": "1.0.0",
- "extsprintf": "1.3.0",
- "json-schema": "0.4.0",
- "verror": "1.10.0"
- }
- },
- "node_modules/jsprim/node_modules/extsprintf": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
- "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
- "engines": [
- "node >=0.6.0"
- ],
- "license": "MIT"
- },
- "node_modules/jsprim/node_modules/verror": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
- "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
- "engines": [
- "node >=0.6.0"
- ],
- "license": "MIT",
- "dependencies": {
- "assert-plus": "^1.0.0",
- "core-util-is": "1.0.2",
- "extsprintf": "^1.2.0"
- }
- },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -22030,15 +21775,6 @@
"node": ">= 6.0.0"
}
},
- "node_modules/mustache": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
- "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
- "license": "MIT",
- "bin": {
- "mustache": "bin/mustache"
- }
- },
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -22259,44 +21995,6 @@
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"license": "MIT"
},
- "node_modules/node-vault": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/node-vault/-/node-vault-0.10.2.tgz",
- "integrity": "sha512-//uc9/YImE7Dx0QHdwMiAzLaOumiKUnOUP8DymgtkZ8nsq6/V2LKvEu6kw91Lcruw8lWUfj4DO7CIXNPRWBuuA==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.3.4",
- "mustache": "^4.2.0",
- "postman-request": "^2.88.1-postman.33",
- "tv4": "^1.3.0"
- },
- "engines": {
- "node": ">= 16.0.0"
- }
- },
- "node_modules/node-vault/node_modules/debug": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
- "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/node-vault/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -22367,15 +22065,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/oauth-sign": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
- "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
- "license": "Apache-2.0",
- "engines": {
- "node": "*"
- }
- },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -23090,12 +22779,6 @@
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
- "node_modules/performance-now": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
- "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
- "license": "MIT"
- },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -24075,57 +23758,6 @@
"node": ">=15.0.0"
}
},
- "node_modules/postman-request": {
- "version": "2.88.1-postman.40",
- "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.40.tgz",
- "integrity": "sha512-uE4AiIqhjtHKp4pj9ei7fkdfNXEX9IqDBlK1plGAQne6y79UUlrTdtYLhwXoO0AMOvqyl9Ar+BU6Eo6P/MPgfg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@postman/form-data": "~3.1.1",
- "@postman/tough-cookie": "~4.1.3-postman.1",
- "@postman/tunnel-agent": "^0.6.4",
- "aws-sign2": "~0.7.0",
- "aws4": "^1.12.0",
- "brotli": "^1.3.3",
- "caseless": "~0.12.0",
- "combined-stream": "~1.0.6",
- "extend": "~3.0.2",
- "forever-agent": "~0.6.1",
- "har-validator": "~5.1.3",
- "http-signature": "~1.3.1",
- "is-typedarray": "~1.0.0",
- "isstream": "~0.1.2",
- "json-stringify-safe": "~5.0.1",
- "mime-types": "^2.1.35",
- "oauth-sign": "~0.9.0",
- "performance-now": "^2.1.0",
- "qs": "~6.5.3",
- "safe-buffer": "^5.1.2",
- "stream-length": "^1.0.2",
- "uuid": "^8.3.2"
- },
- "engines": {
- "node": ">= 16"
- }
- },
- "node_modules/postman-request/node_modules/qs": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
- "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/postman-request/node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -24427,6 +24059,7 @@
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
@@ -24471,6 +24104,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -27386,37 +27020,6 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
- "node_modules/sshpk": {
- "version": "1.18.0",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
- "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
- "license": "MIT",
- "dependencies": {
- "asn1": "~0.2.3",
- "assert-plus": "^1.0.0",
- "bcrypt-pbkdf": "^1.0.0",
- "dashdash": "^1.12.0",
- "ecc-jsbn": "~0.1.1",
- "getpass": "^0.1.1",
- "jsbn": "~0.1.0",
- "safer-buffer": "^2.0.2",
- "tweetnacl": "~0.14.0"
- },
- "bin": {
- "sshpk-conv": "bin/sshpk-conv",
- "sshpk-sign": "bin/sshpk-sign",
- "sshpk-verify": "bin/sshpk-verify"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/sshpk/node_modules/jsbn": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
- "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
- "license": "MIT"
- },
"node_modules/stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@@ -27587,21 +27190,6 @@
"node": ">= 6"
}
},
- "node_modules/stream-length": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz",
- "integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==",
- "license": "WTFPL",
- "dependencies": {
- "bluebird": "^2.6.2"
- }
- },
- "node_modules/stream-length/node_modules/bluebird": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
- "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==",
- "license": "MIT"
- },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -29184,12 +28772,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/tweetnacl": {
- "version": "0.14.5",
- "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
- "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
- "license": "Unlicense"
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -29454,6 +29036,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
@@ -30477,7 +30060,7 @@
"polished": "^4.3.1",
"posthog-node": "4.2.1",
"prettier": "^2.7.1",
- "qs": "^6.11.0",
+ "qs": "^6.14.1",
"query-string": "^7.0.1",
"react": "19.0.0",
"react-copy-to-clipboard": "^5.1.0",
@@ -30492,6 +30075,7 @@
"react-player": "^2.16.0",
"react-redux": "^7.2.9",
"react-tooltip": "^5.5.2",
+ "react-virtuoso": "^4.18.1",
"sass": "^1.46.0",
"semver": "^7.7.1",
"shell-quote": "^1.8.3",
@@ -31932,6 +31516,31 @@
"url": "https://opencollective.com/express"
}
},
+ "packages/bruno-app/node_modules/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "packages/bruno-app/node_modules/react-virtuoso": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.18.1.tgz",
+ "integrity": "sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16 || >=17 || >= 18 || >= 19",
+ "react-dom": ">=16 || >=17 || >= 18 || >=19"
+ }
+ },
"packages/bruno-app/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -32017,7 +31626,7 @@
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.1",
"lodash": "^4.17.21",
- "qs": "^6.11.0",
+ "qs": "^6.14.1",
"socks-proxy-agent": "^8.0.2",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
@@ -33067,6 +32676,21 @@
"node": ">=12"
}
},
+ "packages/bruno-cli/node_modules/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"packages/bruno-common": {
"name": "@usebruno/common",
"version": "0.1.0",
@@ -33652,9 +33276,9 @@
"license": "MIT"
},
"packages/bruno-converters/node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -33757,7 +33381,7 @@
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"nanoid": "3.3.8",
- "qs": "^6.11.0",
+ "qs": "^6.14.1",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^6.0.0",
"uuid": "^9.0.0",
@@ -35243,6 +34867,21 @@
"dev": true,
"license": "MIT"
},
+ "packages/bruno-electron/node_modules/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"packages/bruno-electron/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -35492,7 +35131,6 @@
"moment": "^2.29.4",
"nanoid": "3.3.8",
"node-fetch": "^2.7.0",
- "node-vault": "^0.10.2",
"path": "^0.12.7",
"quickjs-emscripten": "^0.29.2",
"tv4": "^1.3.0",
@@ -35607,7 +35245,10 @@
"debug": "^4.4.3",
"google-protobuf": "^4.0.0",
"grpc-js-reflection-client": "^1.3.0",
+ "http-proxy-agent": "~7.0.2",
+ "https-proxy-agent": "~7.0.6",
"is-ip": "^5.0.1",
+ "socks-proxy-agent": "~8.0.5",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0",
"ws": "^8.18.3"
@@ -35707,9 +35348,7 @@
"version": "0.7.0",
"license": "MIT",
"dependencies": {
- "nanoid": "3.3.8"
- },
- "peerDependencies": {
+ "nanoid": "3.3.8",
"yup": "^0.32.11"
}
},
@@ -36004,4 +35643,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index b15e02d9d..46fc41de5 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"build:electron:rpm": "./scripts/build-electron.sh rpm",
"build:electron:snap": "./scripts/build-electron.sh snap",
"watch:common": "npm run watch --workspace=packages/bruno-common",
+ "watch:requests": "npm run watch --workspace=packages/bruno-requests",
"test:codegen": "node playwright/codegen.ts",
"test:e2e": "playwright test --project=default",
"test:e2e:ssl": "playwright test --project=ssl",
diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json
index b866aa15c..70a7c8663 100644
--- a/packages/bruno-app/package.json
+++ b/packages/bruno-app/package.json
@@ -69,7 +69,7 @@
"polished": "^4.3.1",
"posthog-node": "4.2.1",
"prettier": "^2.7.1",
- "qs": "^6.11.0",
+ "qs": "^6.14.1",
"query-string": "^7.0.1",
"react": "19.0.0",
"react-copy-to-clipboard": "^5.1.0",
@@ -84,6 +84,7 @@
"react-player": "^2.16.0",
"react-redux": "^7.2.9",
"react-tooltip": "^5.5.2",
+ "react-virtuoso": "^4.18.1",
"sass": "^1.46.0",
"semver": "^7.7.1",
"shell-quote": "^1.8.3",
diff --git a/packages/bruno-app/src/components/AppTitleBar/index.js b/packages/bruno-app/src/components/AppTitleBar/index.js
index f2a621b29..0e40c9ea9 100644
--- a/packages/bruno-app/src/components/AppTitleBar/index.js
+++ b/packages/bruno-app/src/components/AppTitleBar/index.js
@@ -18,9 +18,9 @@ import ImportWorkspace from 'components/WorkspaceSidebar/ImportWorkspace';
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
import StyledWrapper from './StyledWrapper';
-import { toTitleCase } from 'utils/common/index';
import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';
import { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform';
+import classNames from 'classnames';
const getOsClass = () => {
if (isMacOS()) return 'os-mac';
@@ -29,6 +29,12 @@ const getOsClass = () => {
return 'os-other';
};
+// Helper to get display name for workspace
+export const getWorkspaceDisplayName = (name) => {
+ if (!name) return 'Untitled Workspace';
+ return name;
+};
+
const AppTitleBar = () => {
const dispatch = useDispatch();
const [isFullScreen, setIsFullScreen] = useState(false);
@@ -115,7 +121,7 @@ const AppTitleBar = () => {
const WorkspaceName = forwardRef((props, ref) => {
return (
- {toTitleCase(activeWorkspace?.name) || 'Default Workspace'}
+ {getWorkspaceDisplayName(activeWorkspace?.name)}
);
@@ -127,7 +133,7 @@ const AppTitleBar = () => {
const handleWorkspaceSwitch = (workspaceUid) => {
dispatch(switchWorkspace(workspaceUid));
- toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
+ toast.success(`Switched to ${getWorkspaceDisplayName(workspaces.find((w) => w.uid === workspaceUid)?.name)}`);
};
const handleOpenWorkspace = async () => {
@@ -178,7 +184,7 @@ const AppTitleBar = () => {
return {
id: workspace.uid,
- label: toTitleCase(workspace.name),
+ label: getWorkspaceDisplayName(workspace.name),
onClick: () => handleWorkspaceSwitch(workspace.uid),
className: `workspace-item ${isActive ? 'active' : ''}`,
rightSection: (
@@ -190,11 +196,7 @@ const AppTitleBar = () => {
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
size="sm"
>
- {isPinned ? (
-
- ) : (
-
- )}
+ {isPinned ? : }
)}
{isActive && }
@@ -247,12 +249,7 @@ const AppTitleBar = () => {
{/* Left section: Home + Workspace */}
-
+
diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js
index 0c2d9b759..3adb1fbf5 100644
--- a/packages/bruno-app/src/components/CodeEditor/index.js
+++ b/packages/bruno-app/src/components/CodeEditor/index.js
@@ -8,7 +8,7 @@
import React from 'react';
import { isEqual, escapeRegExp } from 'lodash';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
-import { setupAutoComplete } from 'utils/codemirror/autocomplete';
+import { setupAutoComplete, showRootHints } from 'utils/codemirror/autocomplete';
import StyledWrapper from './StyledWrapper';
import * as jsonlint from '@prantlf/jsonlint';
import { JSHINT } from 'jshint';
@@ -111,8 +111,12 @@ export default class CodeEditor extends React.Component {
: cm.replaceSelection(' ', 'end');
},
'Shift-Tab': 'indentLess',
- 'Ctrl-Space': 'autocomplete',
- 'Cmd-Space': 'autocomplete',
+ 'Ctrl-Space': (cm) => {
+ showRootHints(cm, this.props.showHintsFor);
+ },
+ 'Cmd-Space': (cm) => {
+ showRootHints(cm, this.props.showHintsFor);
+ },
'Ctrl-Y': 'foldAll',
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
index 2c558152a..abb6ccdcd 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
@@ -59,7 +59,7 @@ const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
return (
{children}
, (prevProps, nextProps) => {
+ const prevUid = prevProps?.item?.uid;
+ const nextUid = nextProps?.item?.uid;
+ return prevUid === nextUid && prevProps.children === nextProps.children;
+});
+
+const MIN_H = 35 * 2; // 2 rows worth of height
+
const EnvironmentVariables = ({ environment, setIsModified, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
+ const [tableHeight, setTableHeight] = React.useState(MIN_H);
+
const environmentsDraft = collection?.environmentsDraft;
const hasDraftForThisEnv = environmentsDraft?.environmentUid === environment.uid;
+ const handleTotalHeightChanged = React.useCallback((h) => {
+ setTableHeight(h);
+ }, []);
+
// Track environment changes for draft restoration
const prevEnvUidRef = React.useRef(null);
const mountedRef = React.useRef(false);
@@ -384,111 +399,114 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => {
return (
-
+ return (
+ <>
+
+ {!isLastEmptyRow && (
+
+ )}
+ |
+
+
+ handleNameChange(index, e)}
+ onBlur={() => handleNameBlur(index)}
+ onKeyDown={(e) => handleNameKeyDown(index, e)}
+ />
+
+
+ |
+
+
+ formik.setFieldValue(`${index}.value`, newValue, true)}
+ onSave={handleSave}
+ />
+
+ {typeof variable.value !== 'string' && (
+
+
+
+
+ )}
+ {!variable.secret && hasSensitiveUsage(variable.name) && (
+
+ )}
+ |
+
+ {!isLastEmptyRow && (
+
+ )}
+ |
+
+ {!isLastEmptyRow && (
+
+ )}
+ |
+ >
+ );
+ }}
+ />
diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js
index d91ba6089..a1995a205 100644
--- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js
@@ -3,7 +3,7 @@ import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
-import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
+import { updateFolderAuth as _updateFolderAuth } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
@@ -20,7 +20,7 @@ import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
import Button from 'ui/Button';
-const GrantTypeComponentMap = ({ collection, folder }) => {
+const GrantTypeComponentMap = ({ collection, folder, updateFolderAuth }) => {
const dispatch = useDispatch();
const save = () => {
@@ -90,6 +90,13 @@ const Auth = ({ collection, folder }) => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
+ const updateFolderAuth = ({ itemUid, ...rest }) => {
+ return _updateFolderAuth({
+ ...rest,
+ folderUid: folder.uid
+ });
+ };
+
const getAuthView = () => {
switch (authMode) {
case 'basic': {
@@ -178,7 +185,7 @@ const Auth = ({ collection, folder }) => {
collection={collection}
item={folder}
/>
-
+
>
);
}
diff --git a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
index 8b92c93cb..ee8197ede 100644
--- a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js
@@ -14,6 +14,10 @@ const Wrapper = styled.div`
z-index: 0;
}
+ .scroll-chevrons.hidden {
+ display: none;
+ }
+
.tabs-scroll-container {
overflow-x: auto;
overflow-y: clip;
@@ -192,10 +196,6 @@ const Wrapper = styled.div`
}
}
- &.has-chevrons ul {
- padding-left: 0;
- }
-
.special-tab-icon {
color: ${(props) => props.theme.primary.text};
}
diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js
index 0de864955..5f7fa68c5 100644
--- a/packages/bruno-app/src/components/RequestTabs/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/index.js
@@ -103,14 +103,9 @@ const RequestTabs = () => {
});
};
- const getRootClassname = () => {
- return classnames({
- 'has-chevrons': showChevrons
- });
- };
// Todo: Must support ephemeral requests
return (
-
+
{newRequestModalOpen && (
setNewRequestModalOpen(false)} />
)}
@@ -118,12 +113,11 @@ const RequestTabs = () => {
<>
-
- {showChevrons ? (
+
{/* Moved to post mvp */}
{/*
@@ -175,11 +169,11 @@ const RequestTabs = () => {
)}
- {showChevrons ? (
+
{/* Moved to post mvp */}
{/*
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
index 6d62f38c0..b6f40be58 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
@@ -49,11 +49,11 @@ export const useInitialResponseFormat = (dataBuffer, headers) => {
// Wait until both content types are available
if (detectedContentType === null || contentType === undefined) {
- return { initialFormat: null, initialTab: null };
+ return { initialFormat: null, initialTab: null, contentType: contentType };
}
const initial = getDefaultResponseFormat(contentType);
- return { initialFormat: initial.format, initialTab: initial.tab };
+ return { initialFormat: initial.format, initialTab: initial.tab, contentType: contentType };
}, [dataBuffer, headers]);
};
@@ -66,6 +66,7 @@ export const useResponsePreviewFormatOptions = (dataBuffer, headers) => {
const byteFormatTypes = ['image', 'video', 'audio', 'pdf', 'zip'];
const isByteFormatType = (contentType) => {
+ if (contentType.toLowerCase().includes('svg')) return false; // SVG is text-based
return byteFormatTypes.some((type) => contentType.includes(type));
};
@@ -203,7 +204,7 @@ const QueryResult = ({
dataBuffer={dataBuffer}
formattedData={formattedData}
item={item}
- contentType={contentType}
+ contentType={detectedContentType ?? contentType}
previewMode={previewMode}
codeMirrorMode={codeMirrorMode}
collection={collection}
diff --git a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/StyledWrapper.js
index 19098970a..142b45b7d 100644
--- a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/StyledWrapper.js
+++ b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/StyledWrapper.js
@@ -53,6 +53,17 @@ const StyledWrapper = styled.div`
align-items: center;
margin-left: 10px;
}
+
+ div.tabs .action-icon {
+ color: ${(props) => props.theme.dropdown.iconColor};
+ opacity: 0.8;
+
+ &:hover {
+ color: ${(props) => props.theme.text};
+ opacity: 1;
+ background-color: ${(props) => props.theme.workspace.button.bg};
+ }
+ }
`;
export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/StyledWrapper.js
index a0e8c744e..00175777c 100644
--- a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/StyledWrapper.js
+++ b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/StyledWrapper.js
@@ -1,7 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
- overflow-y: auto;
+ flex: 1;
+ min-height: 0;
+ height: 100%;
.empty-state {
padding: 1rem;
diff --git a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js
index 233be1988..e30e102b1 100644
--- a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js
@@ -1,13 +1,11 @@
-import React from 'react';
+import React, { useState, useRef, useEffect, useCallback, memo } from 'react';
import classnames from 'classnames';
import StyledWrapper from './StyledWrapper';
import { IconExclamationCircle, IconChevronRight, IconInfoCircle, IconChevronDown, IconArrowUpRight, IconArrowDownLeft } from '@tabler/icons';
import CodeEditor from 'components/CodeEditor/index';
import { useTheme } from 'providers/Theme';
-import { useState } from 'react';
import { useSelector } from 'react-redux';
-import { useRef } from 'react';
-import { useEffect } from 'react';
+import { Virtuoso } from 'react-virtuoso';
const getContentMeta = (content) => {
if (typeof content === 'object') {
@@ -61,8 +59,7 @@ const TypeIcon = ({ type }) => {
}[type];
};
-const WSMessageItem = ({ message, inFocus }) => {
- const [isOpen, setIsOpen] = useState(false);
+const WSMessageItem = memo(({ message, isOpen, onToggle }) => {
const [showHex, setShowHex] = useState(false);
const preferences = useSelector((state) => state.app.preferences);
const { displayedTheme } = useTheme();
@@ -82,21 +79,23 @@ const WSMessageItem = ({ message, inFocus }) => {
const dateDiff = Date.now() - new Date(message.timestamp).getTime();
if (dateDiff < 1000 * 10) {
setIsNew(true);
- setTimeout(() => {
+ const timer = setTimeout(() => {
notified.current = true;
setIsNew(false);
}, 2500);
+ return () => clearTimeout(timer);
}
- }, [message]);
+ }, [message.timestamp]);
const canOpenMessage = !isInfo && !isError;
+ const handleToggle = () => {
+ if (!canOpenMessage) return;
+ onToggle?.(message.timestamp);
+ };
+
return (
{
- if (!node) return;
- if (inFocus) node.scrollIntoView();
- }}
className={classnames('ws-message flex flex-col p-2', {
'ws-incoming': isIncoming,
'ws-outgoing': isOutgoing,
@@ -111,10 +110,7 @@ const WSMessageItem = ({ message, inFocus }) => {
'cursor-pointer': canOpenMessage,
'cursor-not-allowed': !canOpenMessage
})}
- onClick={(e) => {
- if (!canOpenMessage) return;
- setIsOpen(!isOpen);
- }}
+ onClick={handleToggle}
>
@@ -176,23 +172,87 @@ const WSMessageItem = ({ message, inFocus }) => {
)}
);
-};
+});
+
+const WSMessagesList = ({ messages = [] }) => {
+ const virtuosoRef = useRef(null);
+ const [scrollerElement, setScrollerElement] = useState(null);
+ const [openMessages, setOpenMessages] = useState(new Set());
+ const userScrolledAwayRef = useRef(false);
+
+ // Toggle message open/closed state by timestamp
+ const handleMessageToggle = useCallback((timestamp) => {
+ setOpenMessages((prev) => {
+ const next = new Set(prev);
+ if (next.has(timestamp)) {
+ next.delete(timestamp);
+ } else {
+ next.add(timestamp);
+ }
+ return next;
+ });
+ }, []);
+
+ useEffect(() => {
+ if (!scrollerElement) return;
+
+ const handleWheel = (e) => {
+ // deltaY < 0 means scrolling up
+ if (e.deltaY < 0) {
+ userScrolledAwayRef.current = true;
+ }
+ };
+
+ scrollerElement.addEventListener('wheel', handleWheel, { passive: true });
+
+ return () => {
+ scrollerElement.removeEventListener('wheel', handleWheel);
+ };
+ }, [scrollerElement]);
+
+ const handleAtBottomStateChange = useCallback((atBottom) => {
+ if (atBottom) {
+ // User scrolled back to bottom, re-enable auto-scroll
+ userScrolledAwayRef.current = false;
+ }
+ }, []);
+
+ const followOutput = useCallback((isAtBottom) => {
+ // Don't auto-scroll if user has scrolled away or has messages open
+ if (userScrolledAwayRef.current || openMessages.size > 0) {
+ return false;
+ }
+ if (isAtBottom) {
+ return 'smooth';
+ }
+ return false;
+ }, [openMessages.size]);
+
+ const renderItem = useCallback((_, msg) => {
+ const isOpen = openMessages.has(msg.timestamp);
+ return
;
+ }, [openMessages, handleMessageToggle]);
+
+ const computeItemKey = useCallback((_, msg) => {
+ return msg.seq ?? msg.timestamp;
+ }, []);
-const WSMessagesList = ({ order = -1, messages = [] }) => {
if (!messages.length) {
return
No messages yet.
;
}
- // sort based on order, seq was newly added and might be missing in some cases and when missing,
- // the timestamp will be used instead
- const ordered = messages.toSorted((x, y) => ((x.seq ?? x.timestamp) - (y.seq ?? y.timestamp)) * (-order));
-
return (
- {ordered.map((msg, idx, src) => {
- const inFocus = order === -1 ? src.length - 1 === idx : idx === 0;
- return ;
- })}
+
);
};
diff --git a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/index.js
index 28cc50efc..5722a4166 100644
--- a/packages/bruno-app/src/components/ResponsePane/WsResponsePane/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/WsResponsePane/index.js
@@ -13,11 +13,10 @@ import StyledWrapper from './StyledWrapper';
import ResponseLayoutToggle from '../ResponseLayoutToggle';
import ResponsiveTabs from 'ui/ResponsiveTabs';
import WSMessagesList from './WSMessagesList';
-import WSResponseSortOrder from './WSResponseSortOrder';
import WSResponseHeaders from './WSResponseHeaders';
const WSResult = ({ response }) => {
- return
;
+ return
;
};
const WSResponsePane = ({ item, collection }) => {
@@ -116,7 +115,6 @@ const WSResponsePane = ({ item, collection }) => {
<>
-
{
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
// Initialize format and tab only once when data loads.
- const { initialFormat, initialTab } = useInitialResponseFormat(response?.dataBuffer, response?.headers);
+ const { initialFormat, initialTab, contentType } = useInitialResponseFormat(response?.dataBuffer, response?.headers);
const previewFormatOptions = useResponsePreviewFormatOptions(response?.dataBuffer, response?.headers);
+ // Track previous response headers to detect when content-type changes
+ const previousContentRef = useRef(contentType);
+
const persistedFormat = focusedTab?.responseFormat;
const persistedViewTab = focusedTab?.responseViewTab;
@@ -56,13 +59,19 @@ const ResponsePane = ({ item, collection }) => {
if (!focusedTab || initialFormat === null || initialTab === null) {
return;
}
- if (persistedFormat === null) {
+
+ // Check if response headers (content-type) changed using deep comparison
+ const contentTypeChanged = contentType !== previousContentRef.current;
+ if (contentTypeChanged) {
+ previousContentRef.current = contentType;
+ }
+ if (contentTypeChanged || persistedFormat === null) {
dispatch(updateResponseFormat({ uid: item.uid, responseFormat: initialFormat }));
}
- if (persistedViewTab === null) {
+ if (contentTypeChanged || persistedViewTab === null) {
dispatch(updateResponseViewTab({ uid: item.uid, responseViewTab: initialTab }));
}
- }, [initialFormat, initialTab, persistedFormat, persistedViewTab, focusedTab, item.uid, dispatch]);
+ }, [contentType, initialFormat, initialTab, persistedFormat, persistedViewTab, focusedTab, item.uid, dispatch]);
const handleFormatChange = useCallback((newFormat) => {
dispatch(updateResponseFormat({ uid: item.uid, responseFormat: newFormat }));
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
index 66c0fcd62..6dbb69064 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
@@ -20,7 +20,14 @@ const CloneCollection = ({ onClose, collectionUid }) => {
const [isEditing, toggleEditing] = useState(false);
const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));
const preferences = useSelector((state) => state.app.preferences);
- const defaultLocation = get(preferences, 'general.defaultCollectionLocation', '');
+ const workspaces = useSelector((state) => state.workspaces?.workspaces || []);
+ const workspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid);
+ const activeWorkspace = workspaces.find((w) => w.uid === workspaceUid);
+ const isDefaultWorkspace = activeWorkspace?.type === 'default';
+
+ const defaultLocation = isDefaultWorkspace
+ ? get(preferences, 'general.defaultCollectionLocation', '')
+ : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : '');
const { name } = collection;
const formik = useFormik({
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
index d7372784a..660d7ea91 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js
@@ -110,12 +110,14 @@ const GenerateCodeItem = ({ collectionUid, item, onClose, isExample = false, exa
// Resolve auth inheritance
const resolvedRequest = resolveInheritedAuth(item, collection);
- // Create the final item for code generation
+ // requestData.request contains either the normal request or example request data.
+ // We explicitly set auth from resolvedRequest to ensure inherited auth
+ // (from folders/collection) is resolved correctly in generated code.
const finalItem = {
...item,
request: {
- ...resolvedRequest,
...requestData.request,
+ auth: resolvedRequest.auth,
url: finalUrl
}
};
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js
index 03c4cd973..f52faf118 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js
@@ -20,7 +20,7 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
// Add auth headers if needed
if (request.auth && request.auth.mode !== 'none') {
const collectionAuth = collection?.draft?.root ? get(collection, 'draft.root.request.auth', null) : get(collection, 'root.request.auth', null);
- const authHeaders = getAuthHeaders(collectionAuth, request.auth);
+ const authHeaders = getAuthHeaders(collectionAuth, request.auth, collection, item);
headers = [...headers, ...authHeaders];
}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js
index e20f35a22..0946ce2bc 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js
@@ -554,3 +554,223 @@ describe('generateSnippet with edge-case bodies', () => {
expect(result).toMatch(/^curl -X POST/);
});
});
+
+describe('generateSnippet with OAuth2 authentication', () => {
+ const language = { target: 'shell', client: 'curl' };
+ const baseCollection = { root: { request: { auth: { mode: 'none' }, headers: [] } } };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Mock getAuthHeaders to return OAuth2 headers based on the auth config
+ const authUtils = require('utils/codegenerator/auth');
+ authUtils.getAuthHeaders.mockImplementation((collectionRootAuth, requestAuth, collection = null, item = null) => {
+ if (requestAuth?.mode === 'oauth2') {
+ const oauth2Config = requestAuth.oauth2 || {};
+ const tokenPlacement = oauth2Config.tokenPlacement || 'header';
+ // Use the actual value from config, defaulting to 'Bearer' only if undefined
+ // Empty string should be preserved to test no-prefix scenarios
+ const tokenHeaderPrefix = oauth2Config.tokenHeaderPrefix !== undefined
+ ? oauth2Config.tokenHeaderPrefix
+ : 'Bearer';
+ let accessToken = oauth2Config.accessToken || '';
+
+ // If collection and item are provided, try to look up stored credentials
+ if (collection && item && collection.oauth2Credentials) {
+ const grantType = oauth2Config.grantType || '';
+ const urlToLookup = grantType === 'implicit'
+ ? oauth2Config.authorizationUrl || ''
+ : oauth2Config.accessTokenUrl || '';
+ const credentialsId = oauth2Config.credentialsId || 'credentials';
+ const collectionUid = collection.uid;
+
+ if (urlToLookup && collectionUid) {
+ // Look up stored credentials (simplified - assumes URL is already interpolated in test data)
+ const credentialsData = collection.oauth2Credentials.find(
+ (creds) =>
+ creds?.url === urlToLookup
+ && creds?.collectionUid === collectionUid
+ && creds?.credentialsId === credentialsId
+ );
+
+ if (credentialsData?.credentials?.access_token) {
+ accessToken = credentialsData.credentials.access_token;
+ }
+ }
+ }
+
+ if (tokenPlacement === 'header') {
+ // Always trim the final result for consistent formatting
+ const headerValue = tokenHeaderPrefix
+ ? `${tokenHeaderPrefix} ${accessToken}`.trim()
+ : accessToken.trim();
+ return [
+ {
+ enabled: true,
+ name: 'Authorization',
+ value: headerValue
+ }
+ ];
+ }
+ }
+ return [];
+ });
+ });
+
+ it('should include OAuth2 Bearer token in Authorization header when tokenPlacement is header', () => {
+ const item = {
+ uid: 'oauth-req',
+ request: {
+ method: 'GET',
+ url: 'https://api.example.com/users',
+ headers: [],
+ auth: {
+ mode: 'oauth2',
+ oauth2: {
+ grantType: 'client_credentials',
+ tokenPlacement: 'header',
+ tokenHeaderPrefix: 'Bearer',
+ accessToken: 'test-access-token-123'
+ }
+ }
+ }
+ };
+
+ generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
+
+ const harUtils = require('utils/codegenerator/har');
+ const harCall = harUtils.buildHarRequest.mock.calls[0][0];
+ expect(harCall.headers).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'Authorization',
+ value: 'Bearer test-access-token-123'
+ })
+ ])
+ );
+ });
+
+ it('should use custom tokenHeaderPrefix when provided', () => {
+ const item = {
+ uid: 'oauth-req-custom',
+ request: {
+ method: 'GET',
+ url: 'https://api.example.com/users',
+ headers: [],
+ auth: {
+ mode: 'oauth2',
+ oauth2: {
+ grantType: 'client_credentials',
+ tokenPlacement: 'header',
+ tokenHeaderPrefix: 'OAuth',
+ accessToken: 'custom-token-456'
+ }
+ }
+ }
+ };
+
+ generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
+
+ const harUtils = require('utils/codegenerator/har');
+ const harCall = harUtils.buildHarRequest.mock.calls[0][0];
+ expect(harCall.headers).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'Authorization',
+ value: 'OAuth custom-token-456'
+ })
+ ])
+ );
+ });
+
+ it('should not include Authorization header when tokenPlacement is url', () => {
+ const item = {
+ uid: 'oauth-req-url',
+ request: {
+ method: 'GET',
+ url: 'https://api.example.com/users',
+ headers: [],
+ auth: {
+ mode: 'oauth2',
+ oauth2: {
+ grantType: 'client_credentials',
+ tokenPlacement: 'url',
+ tokenQueryKey: 'access_token',
+ accessToken: 'token-in-url'
+ }
+ }
+ }
+ };
+
+ generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
+
+ const harUtils = require('utils/codegenerator/har');
+ const harCall = harUtils.buildHarRequest.mock.calls[0][0];
+ const authHeader = harCall.headers.find((h) => h.name === 'Authorization');
+ expect(authHeader).toBeUndefined();
+ });
+
+ it('should use placeholder when accessToken is not available', () => {
+ const item = {
+ uid: 'oauth-req-placeholder',
+ request: {
+ method: 'GET',
+ url: 'https://api.example.com/users',
+ headers: [],
+ auth: {
+ mode: 'oauth2',
+ oauth2: {
+ grantType: 'client_credentials',
+ tokenPlacement: 'header',
+ tokenHeaderPrefix: 'Bearer'
+ }
+ }
+ }
+ };
+
+ generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
+
+ const harUtils = require('utils/codegenerator/har');
+ const harCall = harUtils.buildHarRequest.mock.calls[0][0];
+ expect(harCall.headers).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'Authorization',
+ value: 'Bearer '
+ })
+ ])
+ );
+ });
+
+ it('should handle empty tokenHeaderPrefix', () => {
+ const item = {
+ uid: 'oauth-req-no-prefix',
+ request: {
+ method: 'GET',
+ url: 'https://api.example.com/users',
+ headers: [],
+ auth: {
+ mode: 'oauth2',
+ oauth2: {
+ grantType: 'client_credentials',
+ tokenPlacement: 'header',
+ tokenHeaderPrefix: '',
+ accessToken: 'token-without-prefix'
+ }
+ }
+ }
+ };
+
+ generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
+
+ const harUtils = require('utils/codegenerator/har');
+ const harCall = harUtils.buildHarRequest.mock.calls[0][0];
+ expect(harCall.headers).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'Authorization',
+ value: 'token-without-prefix'
+ })
+ ])
+ );
+ });
+});
diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
index 95ea0bae4..73a741948 100644
--- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
+++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js
@@ -1,4 +1,5 @@
import React, { useCallback, useRef } from 'react';
+import { TableVirtuoso } from 'react-virtuoso';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
@@ -19,6 +20,14 @@ import { Tooltip } from 'react-tooltip';
import { getGlobalEnvironmentVariables } from 'utils/collections';
import Button from 'ui/Button';
+const MIN_H = 35 * 2;
+
+const TableRow = React.memo(({ children, item }) => {children}
, (prevProps, nextProps) => {
+ const prevUid = prevProps?.item?.uid;
+ const nextUid = nextProps?.item?.uid;
+ return prevUid === nextUid && prevProps.children === nextProps.children;
+});
+
const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
@@ -28,6 +37,12 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
const hasDraftForThisEnv = globalEnvironmentDraft?.environmentUid === environment.uid;
+ const [tableHeight, setTableHeight] = React.useState(MIN_H);
+
+ const handleTotalHeightChanged = React.useCallback((h) => {
+ setTableHeight(h);
+ }, []);
+
// Track environment changes for draft restoration
const prevEnvUidRef = React.useRef(null);
const mountedRef = React.useRef(false);
@@ -322,109 +337,108 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
return (
-
+ return (
+ <>
+
+ {!isLastEmptyRow && (
+
+ )}
+ |
+
+
+ handleNameChange(index, e)}
+ onBlur={() => handleNameBlur(index)}
+ onKeyDown={(e) => handleNameKeyDown(index, e)}
+ />
+
+
+ |
+
+
+ formik.setFieldValue(`${index}.value`, newValue, true)}
+ onSave={handleSave}
+ />
+
+ {typeof variable.value !== 'string' && (
+
+
+
+
+ )}
+ |
+
+ {!isLastEmptyRow && (
+
+ )}
+ |
+
+ {!isLastEmptyRow && (
+
+ )}
+ |
+ >
+ );
+ }}
+ />
diff --git a/packages/bruno-app/src/components/WorkspaceHome/index.js b/packages/bruno-app/src/components/WorkspaceHome/index.js
index db27824b0..4ec91aefe 100644
--- a/packages/bruno-app/src/components/WorkspaceHome/index.js
+++ b/packages/bruno-app/src/components/WorkspaceHome/index.js
@@ -11,6 +11,8 @@ import WorkspaceTabs from 'components/WorkspaceTabs';
import StyledWrapper from './StyledWrapper';
import Dropdown from 'components/Dropdown';
import { getRevealInFolderLabel } from 'utils/common/platform';
+import { getWorkspaceDisplayName } from 'components/AppTitleBar';
+import classNames from 'classnames';
const WorkspaceHome = () => {
const dispatch = useDispatch();
@@ -208,7 +210,7 @@ const WorkspaceHome = () => {
) : (
- {activeWorkspace.name}
+ {getWorkspaceDisplayName(activeWorkspace.name)}
)}
diff --git a/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js b/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js
index fdb32d86d..d28517238 100644
--- a/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js
+++ b/packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js
@@ -4,10 +4,11 @@ import filter from 'lodash/filter';
import groupBy from 'lodash/groupBy';
import { useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
-import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges } from 'utils/collections';
+import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges, findEnvironmentInCollection } from 'utils/collections';
import { pluralizeWord } from 'utils/common';
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
-import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders } from 'providers/ReduxStore/slices/collections/actions';
+import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders, saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
+import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { IconAlertTriangle } from '@tabler/icons';
import Modal from 'components/Modal';
import Button from 'ui/Button';
@@ -16,12 +17,15 @@ const SaveRequestsModal = ({ onClose }) => {
const MAX_UNSAVED_ITEMS_TO_SHOW = 5;
const collections = useSelector((state) => state.collections.collections);
const tabs = useSelector((state) => state.tabs.tabs);
+ const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
+ const globalEnvironmentDraft = useSelector((state) => state.globalEnvironments.globalEnvironmentDraft);
const dispatch = useDispatch();
const allDrafts = useMemo(() => {
const requestDrafts = [];
const collectionDrafts = [];
const folderDrafts = [];
+ const environmentDrafts = [];
const tabsByCollection = groupBy(tabs, (t) => t.collectionUid);
Object.keys(tabsByCollection).forEach((collectionUid) => {
@@ -36,6 +40,21 @@ const SaveRequestsModal = ({ onClose }) => {
});
}
+ // Check for collection environment draft
+ if (collection.environmentsDraft) {
+ const { environmentUid, variables } = collection.environmentsDraft;
+ const environment = findEnvironmentInCollection(collection, environmentUid);
+ if (environment && variables) {
+ environmentDrafts.push({
+ type: 'collection-environment',
+ name: environment.name,
+ environmentUid,
+ variables,
+ collectionUid: collectionUid
+ });
+ }
+ }
+
// Check for request and folder drafts
const items = flattenItems(collection.items);
@@ -62,8 +81,22 @@ const SaveRequestsModal = ({ onClose }) => {
}
});
- return [...collectionDrafts, ...folderDrafts, ...requestDrafts];
- }, [collections, tabs]);
+ // Check for global environment draft
+ if (globalEnvironmentDraft) {
+ const { environmentUid, variables } = globalEnvironmentDraft;
+ const environment = globalEnvironments?.find((env) => env.uid === environmentUid);
+ if (environment && variables) {
+ environmentDrafts.push({
+ type: 'global-environment',
+ name: environment.name,
+ environmentUid,
+ variables
+ });
+ }
+ }
+
+ return [...collectionDrafts, ...folderDrafts, ...environmentDrafts, ...requestDrafts];
+ }, [collections, tabs, globalEnvironments, globalEnvironmentDraft]);
const totalDraftsCount = allDrafts.length;
@@ -84,6 +117,8 @@ const SaveRequestsModal = ({ onClose }) => {
const collectionDrafts = allDrafts.filter((d) => d.type === 'collection');
const folderDrafts = allDrafts.filter((d) => d.type === 'folder');
const requestDrafts = allDrafts.filter((d) => d.type === 'request');
+ const collectionEnvironmentDrafts = allDrafts.filter((d) => d.type === 'collection-environment');
+ const globalEnvironmentDrafts = allDrafts.filter((d) => d.type === 'global-environment');
// Save all collection drafts
if (collectionDrafts.length > 0) {
@@ -100,6 +135,16 @@ const SaveRequestsModal = ({ onClose }) => {
await dispatch(saveMultipleRequests(requestDrafts));
}
+ // Save all collection environment drafts
+ for (const draft of collectionEnvironmentDrafts) {
+ await dispatch(saveEnvironment(draft.variables, draft.environmentUid, draft.collectionUid));
+ }
+
+ // Save all global environment drafts
+ for (const draft of globalEnvironmentDrafts) {
+ await dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }));
+ }
+
dispatch(completeQuitFlow());
onClose();
} catch (error) {
@@ -134,12 +179,23 @@ const SaveRequestsModal = ({ onClose }) => {
{allDrafts.slice(0, MAX_UNSAVED_ITEMS_TO_SHOW).map((item, index) => {
- const prefix
- = item.type === 'collection'
- ? 'Collection: '
- : item.type === 'folder'
- ? 'Folder: '
- : 'Request: ';
+ let prefix;
+ switch (item.type) {
+ case 'collection':
+ prefix = 'Collection: ';
+ break;
+ case 'folder':
+ prefix = 'Folder: ';
+ break;
+ case 'collection-environment':
+ prefix = 'Collection Environment: ';
+ break;
+ case 'global-environment':
+ prefix = 'Global Environment: ';
+ break;
+ default:
+ prefix = 'Request: ';
+ }
return (
-
{prefix}
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js
index da9c55bf6..096e13244 100644
--- a/packages/bruno-app/src/providers/App/useIpcEvents.js
+++ b/packages/bruno-app/src/providers/App/useIpcEvents.js
@@ -15,6 +15,7 @@ import {
collectionUnlinkEnvFileEvent,
collectionUnlinkFileEvent,
processEnvUpdateEvent,
+ workspaceEnvUpdateEvent,
requestCancelled,
runFolderEvent,
runRequestEvent,
@@ -23,6 +24,7 @@ import {
} from 'providers/ReduxStore/slices/collections';
import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { workspaceOpenedEvent, workspaceConfigUpdatedEvent } from 'providers/ReduxStore/slices/workspaces/actions';
+import { workspaceDotEnvUpdateEvent } from 'providers/ReduxStore/slices/workspaces';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { isElectron } from 'utils/common/platform';
@@ -214,6 +216,11 @@ const useIpcEvents = () => {
dispatch(processEnvUpdateEvent(val));
});
+ const removeWorkspaceDotEnvUpdatesListener = ipcRenderer.on('main:workspace-dotenv-update', (val) => {
+ dispatch(workspaceDotEnvUpdateEvent(val));
+ dispatch(workspaceEnvUpdateEvent({ processEnvVariables: val.processEnvVariables }));
+ });
+
const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {
console[val.type](...val.args);
dispatch(addLog({
@@ -293,6 +300,7 @@ const useIpcEvents = () => {
removeRunFolderEventListener();
removeRunRequestEventListener();
removeProcessEnvUpdatesListener();
+ removeWorkspaceDotEnvUpdatesListener();
removeConsoleLogListener();
removeConfigUpdatesListener();
removeShowPreferencesListener();
diff --git a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js
index 74aa2970a..291476a82 100644
--- a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js
+++ b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js
@@ -32,7 +32,10 @@ const KeyMapping = {
name: 'Move Tab Right'
},
closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' },
- collapseSidebar: { mac: 'command+\\', windows: 'ctrl+\\', name: 'Collapse Sidebar' }
+ collapseSidebar: { mac: 'command+\\', windows: 'ctrl+\\', name: 'Collapse Sidebar' },
+ zoomIn: { mac: 'command+=', windows: 'ctrl+=', name: 'Zoom In' },
+ zoomOut: { mac: 'command+-', windows: 'ctrl+-', name: 'Zoom Out' },
+ resetZoom: { mac: 'command+0', windows: 'ctrl+0', name: 'Reset Zoom' }
};
/**
diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js
index ad3312b54..1357c95b2 100644
--- a/packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js
+++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js
@@ -1,4 +1,5 @@
-import { saveRequest, saveCollectionSettings, saveFolderRoot } from '../../slices/collections/actions';
+import { saveRequest, saveCollectionSettings, saveFolderRoot, saveEnvironment } from '../../slices/collections/actions';
+import { saveGlobalEnvironment } from '../../slices/global-environments';
import { flattenItems, isItemARequest, isItemAFolder } from 'utils/collections';
const actionsToIntercept = [
@@ -46,6 +47,11 @@ const actionsToIntercept = [
'collections/updateRequestDocs',
'collections/runRequestEvent',
'collections/updateCollectionPresets',
+ 'collections/setRequestVars',
+ 'collections/setRequestAssertions',
+ 'collections/updateItemSettings',
+ 'collections/addRequestTag',
+ 'collections/deleteRequestTag',
// Folder-level actions
'collections/addFolderHeader',
@@ -80,7 +86,11 @@ const actionsToIntercept = [
'collections/updateCollectionDocs',
'collections/updateCollectionClientCertificates',
'collections/updateCollectionProtobuf',
- 'collections/updateCollectionProxy'
+ 'collections/updateCollectionProxy',
+
+ // Environment draft actions
+ 'collections/setEnvironmentsDraft',
+ 'globalEnvironments/setGlobalEnvironmentDraft'
];
// Simple object to track pending save timers
@@ -100,7 +110,8 @@ const scheduleAutoSave = (key, save, interval) => {
// Helper to find and schedule saves for all existing drafts
const saveExistingDrafts = (dispatch, getState, interval) => {
- const collections = getState().collections.collections;
+ const state = getState();
+ const collections = state.collections.collections;
collections.forEach((collection) => {
// Check collection-level draft
@@ -109,6 +120,15 @@ const saveExistingDrafts = (dispatch, getState, interval) => {
scheduleAutoSave(key, () => dispatch(saveCollectionSettings(collection.uid, null, true)), interval);
}
+ // Check collection environment drafts
+ if (collection.environmentsDraft) {
+ const { environmentUid, variables } = collection.environmentsDraft;
+ if (environmentUid && variables) {
+ const key = `environment-${collection.uid}-${environmentUid}`;
+ scheduleAutoSave(key, () => dispatch(saveEnvironment(variables, environmentUid, collection.uid)), interval);
+ }
+ }
+
// Check all items (requests and folders) for drafts
const allItems = flattenItems(collection.items);
allItems.forEach((item) => {
@@ -123,6 +143,77 @@ const saveExistingDrafts = (dispatch, getState, interval) => {
}
});
});
+
+ // Check global environment drafts
+ const globalEnvironmentDraft = state.globalEnvironments?.globalEnvironmentDraft;
+ if (globalEnvironmentDraft) {
+ const { environmentUid, variables } = globalEnvironmentDraft;
+ if (environmentUid && variables) {
+ const key = `global-environment-${environmentUid}`;
+ scheduleAutoSave(key, () => dispatch(saveGlobalEnvironment({ variables, environmentUid })), interval);
+ }
+ }
+};
+
+// Helper to determine entity type and create save handler
+const determineSaveHandler = (actionType, payload, dispatch, getState) => {
+ const { itemUid, folderUid, collectionUid, environmentUid } = payload;
+
+ // Handle environment drafts
+ if (actionType === 'collections/setEnvironmentsDraft') {
+ if (!environmentUid || !collectionUid) return null;
+ return {
+ key: `environment-${collectionUid}-${environmentUid}`,
+ save: () => {
+ const state = getState();
+ const collection = state.collections.collections.find((c) => c.uid === collectionUid);
+ const draft = collection?.environmentsDraft;
+ if (draft?.environmentUid === environmentUid && draft?.variables) {
+ dispatch(saveEnvironment(draft.variables, environmentUid, collectionUid));
+ }
+ }
+ };
+ }
+
+ if (actionType === 'globalEnvironments/setGlobalEnvironmentDraft') {
+ if (!environmentUid) return null;
+ return {
+ key: `global-environment-${environmentUid}`,
+ save: () => {
+ const state = getState();
+ const draft = state.globalEnvironments?.globalEnvironmentDraft;
+ if (draft?.environmentUid === environmentUid && draft?.variables) {
+ dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid }));
+ }
+ }
+ };
+ }
+
+ // Handle folder actions
+ if (folderUid) {
+ return {
+ key: `folder-${folderUid}`,
+ save: () => dispatch(saveFolderRoot(collectionUid, folderUid, true))
+ };
+ }
+
+ // Handle request actions
+ if (itemUid) {
+ return {
+ key: `request-${itemUid}`,
+ save: () => dispatch(saveRequest(itemUid, collectionUid, true))
+ };
+ }
+
+ // Handle collection-level changes
+ if (collectionUid) {
+ return {
+ key: `collection-${collectionUid}`,
+ save: () => dispatch(saveCollectionSettings(collectionUid, null, true))
+ };
+ }
+
+ return null;
};
export const autosaveMiddleware = ({ dispatch, getState }) => (next) => (action) => {
@@ -150,28 +241,9 @@ export const autosaveMiddleware = ({ dispatch, getState }) => (next) => (action)
// Only handle actions that create dirty state
if (!actionsToIntercept.includes(action.type)) return result;
- const { itemUid, folderUid, collectionUid } = action.payload;
- const interval = autoSave.interval;
-
- // Determine what to save based on what IDs are present
- let key, save;
-
- if (itemUid) {
- // Request change
- key = `request-${itemUid}`;
- save = () => dispatch(saveRequest(itemUid, collectionUid, true));
- } else if (folderUid) {
- // Folder change
- key = `folder-${folderUid}`;
- save = () => dispatch(saveFolderRoot(collectionUid, folderUid, true));
- } else if (collectionUid) {
- // Collection change
- key = `collection-${collectionUid}`;
- save = () => dispatch(saveCollectionSettings(collectionUid, null, true));
- }
-
- if (key && save) {
- scheduleAutoSave(key, save, interval);
+ const handler = determineSaveHandler(action.type, action.payload, dispatch, getState);
+ if (handler) {
+ scheduleAutoSave(handler.key, handler.save, autoSave.interval);
}
return result;
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index e5748b3ac..ec1f071eb 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -35,6 +35,7 @@ import {
sortCollections as _sortCollections,
updateCollectionMountStatus,
moveCollection,
+ workspaceEnvUpdateEvent,
requestCancelled,
resetRunResults,
responseReceived,
@@ -816,7 +817,6 @@ export const renameItem
return ipcRenderer
.invoke('renderer:rename-item-filename', { oldPath: item.pathname, newPath, newName, newFilename, collectionPathname: collection.pathname })
.catch((err) => {
- toast.error('Failed to rename the file');
console.error(err);
throw new Error('Failed to rename the file');
});
@@ -2255,6 +2255,7 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
return new Promise((resolve, reject) => {
const state = getState();
const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);
+ const workspaceProcessEnvVariables = activeWorkspace?.processEnvVariables || {};
// Check if collection already exists in Redux state
const existingCollection = state.collections.collections.find(
@@ -2296,6 +2297,8 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
});
}
+ dispatch(workspaceEnvUpdateEvent({ processEnvVariables: workspaceProcessEnvVariables }));
+
resolve();
return;
}
@@ -2308,6 +2311,7 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
pathname: pathname,
items: [],
runtimeVariables: {},
+ workspaceProcessEnvVariables,
brunoConfig: brunoConfig
};
@@ -2326,6 +2330,9 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
);
if (currentWorkspace) {
+ // Set collection-workspace mapping for workspace env vars
+ ipcRenderer.invoke('renderer:set-collection-workspace', uid, currentWorkspace.pathname);
+
const alreadyInWorkspace = currentWorkspace.collections?.some(
(c) => normalizePath(c.path) === normalizePath(pathname)
);
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index 68c0e12ad..7fe15aa08 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -405,6 +405,12 @@ export const collectionsSlice = createSlice({
collection.processEnvVariables = processEnvVariables;
}
},
+ workspaceEnvUpdateEvent: (state, action) => {
+ const { processEnvVariables } = action.payload;
+ state.collections.forEach((collection) => {
+ collection.workspaceProcessEnvVariables = processEnvVariables;
+ });
+ },
requestCancelled: (state, action) => {
const { itemUid, collectionUid, seq, timestamp } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -2332,7 +2338,7 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (!collection) return;
- const folder = collection ? findItemInCollection(collection, action.payload.itemUid) : null;
+ const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (!folder) return;
if (folder) {
@@ -3148,13 +3154,13 @@ export const collectionsSlice = createSlice({
const item = findItemInCollection(collection, itemUid);
if (data.data) {
item.response.data ||= [];
- item.response.data = [{
+ item.response.data.push({
type: 'incoming',
seq,
message: data.data,
messageHexdump: hexdump(data.data),
timestamp: timestamp || Date.now()
- }].concat(item.response.data);
+ });
}
if (item.response.dataBuffer && item.response.dataBuffer.length && data.dataBuffer) {
item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]);
@@ -3424,6 +3430,7 @@ export const {
cloneItem,
scriptEnvironmentUpdateEvent,
processEnvUpdateEvent,
+ workspaceEnvUpdateEvent,
requestCancelled,
responseReceived,
runGrpcRequestEvent,
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js
index f1f35298a..1f439acbd 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js
@@ -169,8 +169,8 @@ export const removeCollectionFromWorkspaceAction = (workspaceUid, collectionPath
};
const loadWorkspaceCollectionsForSwitch = async (dispatch, workspace) => {
- const openCollectionsFunction = (collectionPaths, workspaceId) => {
- return dispatch(openMultipleCollections(collectionPaths, { workspaceId }));
+ const openCollectionsFunction = (collectionPaths, workspacePath) => {
+ return dispatch(openMultipleCollections(collectionPaths, { workspacePath }));
};
try {
@@ -418,7 +418,7 @@ export const workspaceConfigUpdatedEvent = (workspacePath, workspaceUid, workspa
if (uniqueNewCollectionPaths.length > 0) {
try {
- await dispatch(openMultipleCollections(uniqueNewCollectionPaths, { workspaceId: workspace.pathname }));
+ await dispatch(openMultipleCollections(uniqueNewCollectionPaths, { workspacePath: workspace.pathname }));
} catch (error) {
}
}
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/index.js
index e25e3920a..d8bb63f1f 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/index.js
@@ -76,6 +76,14 @@ export const workspacesSlice = createSlice({
if (workspace) {
workspace.loadingState = loadingState;
}
+ },
+
+ workspaceDotEnvUpdateEvent: (state, action) => {
+ const { workspaceUid, processEnvVariables } = action.payload;
+ const workspace = state.workspaces.find((w) => w.uid === workspaceUid);
+ if (workspace) {
+ workspace.processEnvVariables = processEnvVariables;
+ }
}
}
});
@@ -87,7 +95,8 @@ export const {
updateWorkspace,
addCollectionToWorkspace,
removeCollectionFromWorkspace,
- updateWorkspaceLoadingState
+ updateWorkspaceLoadingState,
+ workspaceDotEnvUpdateEvent
} = workspacesSlice.actions;
export default workspacesSlice.reducer;
diff --git a/packages/bruno-app/src/utils/codegenerator/auth.js b/packages/bruno-app/src/utils/codegenerator/auth.js
index ad28970eb..eefb82cfe 100644
--- a/packages/bruno-app/src/utils/codegenerator/auth.js
+++ b/packages/bruno-app/src/utils/codegenerator/auth.js
@@ -1,6 +1,9 @@
import get from 'lodash/get';
+import { find } from 'lodash';
+import { interpolate } from '@usebruno/common';
+import { getAllVariables } from 'utils/collections/index';
-export const getAuthHeaders = (collectionRootAuth, requestAuth) => {
+export const getAuthHeaders = (collectionRootAuth, requestAuth, collection = null, item = null) => {
// Discovered edge case where code generation fails when you create a collection which has not been saved yet:
// Collection auth therefore null, and request inherits from collection, therefore it is also null
// TypeError: Cannot read properties of undefined (reading 'mode')
@@ -48,6 +51,72 @@ export const getAuthHeaders = (collectionRootAuth, requestAuth) => {
];
}
return [];
+ case 'oauth2': {
+ const oauth2Config = get(auth, 'oauth2', {});
+ const tokenPlacement = get(oauth2Config, 'tokenPlacement', 'header');
+ const tokenHeaderPrefix = get(oauth2Config, 'tokenHeaderPrefix', 'Bearer');
+
+ // Only add header if token placement is 'header'
+ if (tokenPlacement === 'header') {
+ // Try to get access token from persisted credentials
+ let accessToken = '';
+
+ if (collection && item) {
+ try {
+ const grantType = get(oauth2Config, 'grantType', '');
+ // For implicit grant type, use authorizationUrl; for others, use accessTokenUrl
+ const urlToLookup = grantType === 'implicit'
+ ? get(oauth2Config, 'authorizationUrl', '')
+ : get(oauth2Config, 'accessTokenUrl', '');
+ const credentialsId = get(oauth2Config, 'credentialsId', 'credentials');
+ const collectionUid = get(collection, 'uid');
+
+ if (urlToLookup && collectionUid) {
+ // Interpolate the URL with variables
+ const variables = getAllVariables(collection, item);
+ const interpolatedUrl = interpolate(urlToLookup, variables);
+
+ // Look up stored credentials
+ const credentialsData = find(
+ collection?.oauth2Credentials || [],
+ (creds) =>
+ creds?.url === interpolatedUrl
+ && creds?.collectionUid === collectionUid
+ && creds?.credentialsId === credentialsId
+ );
+
+ if (credentialsData?.credentials?.access_token) {
+ accessToken = credentialsData.credentials.access_token;
+ }
+ }
+ } catch (error) {
+ console.error('Error retrieving OAuth2 access token:', error);
+ // Fall back to placeholder if lookup fails
+ }
+ }
+
+ // Build the authorization header value
+ // If tokenHeaderPrefix is empty, just use the token
+ // Otherwise, use the format: "prefix token"
+ // Always trim the final result for consistent formatting
+ const headerValue = (
+ tokenHeaderPrefix
+ ? `${tokenHeaderPrefix} ${accessToken}`
+ : accessToken
+ ).trim();
+
+ return [
+ {
+ enabled: true,
+ name: 'Authorization',
+ value: headerValue
+ }
+ ];
+ }
+ // If tokenPlacement is 'url', this function does not add any auth headers;
+ // token placement in the URL/query params must be handled elsewhere.
+ return [];
+ }
default:
return [];
}
diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js
index 8776d7cc3..6db1fd370 100644
--- a/packages/bruno-app/src/utils/codemirror/autocomplete.js
+++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js
@@ -80,6 +80,7 @@ const STATIC_API_HINTS = {
'bru.getTestResults()',
'bru.sleep(ms)',
'bru.getCollectionName()',
+ 'bru.isSafeMode()',
'bru.getGlobalEnvVar(key)',
'bru.setGlobalEnvVar(key, value)',
'bru.runner',
@@ -298,9 +299,14 @@ const calculateWordReplacementPositions = (cursor, start, end, word) => {
* @returns {string} The determined context
*/
const determineWordContext = (word) => {
- if (word.startsWith('req') || word.startsWith('res') || word.startsWith('bru')) {
+ const isApiHint = Object.keys(STATIC_API_HINTS).some(
+ (apiRoot) => apiRoot.toLowerCase().startsWith(word.toLowerCase()) || word.toLowerCase().startsWith(apiRoot.toLowerCase())
+ );
+
+ if (isApiHint) {
return 'api';
}
+
return 'anyword';
};
@@ -517,6 +523,34 @@ const createStandardHintList = (filteredHints, from, to) => {
};
};
+/**
+ * Show root-level API hints when the editor is empty
+ * @param {Object} cm - CodeMirror instance
+ * @param {string[]} showHintsFor - Array of hint types to show (e.g., ['req', 'res', 'bru'])
+ * @returns {boolean} True if hints were shown, false otherwise
+ */
+export const showRootHints = (cm, showHintsFor = []) => {
+ const wordInfo = getCurrentWordWithContext(cm);
+ // If user is currently typing a word, let handleKeyupForAutocomplete
+ // handle it instead of showing root hints.
+ if (wordInfo) {
+ return false;
+ }
+
+ const hints = Object.keys(STATIC_API_HINTS).filter((rootHint) => showHintsFor.includes(rootHint));
+
+ if (hints.length === 0) return false;
+
+ const cursor = cm.getCursor();
+ const hintList = createStandardHintList(hints, cursor, cursor);
+
+ cm.showHint({
+ hint: () => hintList,
+ completeSingle: false
+ });
+ return true;
+};
+
/**
* Bruno AutoComplete Helper - Main function with context awareness
* @param {Object} cm - CodeMirror instance
@@ -628,7 +662,8 @@ const handleKeyupForAutocomplete = (cm, event, options) => {
const hints = getAutoCompleteHints(cm, allVariables, anywordAutocompleteHints, options);
if (!hints) {
- if (cm.state.completionActive) {
+ const wordInfo = getCurrentWordWithContext(cm);
+ if (cm.state.completionActive && wordInfo) {
cm.state.completionActive.close();
}
return;
diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.spec.js b/packages/bruno-app/src/utils/codemirror/autocomplete.spec.js
index 124fc0cb0..16e5a2882 100644
--- a/packages/bruno-app/src/utils/codemirror/autocomplete.spec.js
+++ b/packages/bruno-app/src/utils/codemirror/autocomplete.spec.js
@@ -482,7 +482,7 @@ describe('Bruno Autocomplete', () => {
mockedCodemirror.state.completionActive = mockCompletion;
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });
- mockedCodemirror.getLine.mockReturnValue(' ');
+ mockedCodemirror.getLine.mockReturnValue('req.bodyy');
mockedCodemirror.getRange.mockReturnValue('');
const mockEvent = { key: 'a' };
diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
index d26a1eb5e..2a4b32659 100644
--- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
+++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
@@ -6,7 +6,7 @@
* LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3
*/
-import { interpolate, mockDataFunctions } from '@usebruno/common';
+import { interpolate, mockDataFunctions, timeBasedDynamicVars } from '@usebruno/common';
import { getVariableScope, isVariableSecret, getAllVariables } from 'utils/collections';
import { updateVariableInScope } from 'providers/ReduxStore/slices/collections/actions';
import store from 'providers/ReduxStore';
@@ -194,11 +194,13 @@ export const renderVarInfo = (token, options) => {
} else if (variableName.startsWith('$')) {
const fakerKeyword = variableName.substring(1); // Remove the $ prefix
const fakerFunction = mockDataFunctions[fakerKeyword];
+ const isTimeBased = timeBasedDynamicVars.has(fakerKeyword);
scopeInfo = {
type: 'dynamic',
value: '',
data: null,
- isValidFakerVariable: !!fakerFunction
+ isValidDynamicVariable: !!fakerFunction,
+ isTimeBased
};
} else if (variableName.startsWith('process.env.')) {
// Check if this is a process.env variable (starts with "process.env.")
@@ -300,8 +302,8 @@ export const renderVarInfo = (token, options) => {
return into;
}
- // Show warning for invalid faker variable (starts with $ but not a valid faker function)
- if (scopeInfo.type === 'dynamic' && !scopeInfo.isValidFakerVariable) {
+ // Show warning for invalid dynamic variable (starts with $ but not a valid dynamic function)
+ if (scopeInfo.type === 'dynamic' && !scopeInfo.isValidDynamicVariable) {
const warningNote = document.createElement('div');
warningNote.className = 'var-warning-note';
warningNote.textContent = `Unknown dynamic variable "${variableName}". Check the variable name.`;
@@ -309,11 +311,13 @@ export const renderVarInfo = (token, options) => {
return into;
}
- // For valid dynamic variables, just show the read-only note (no value display since it's generated at runtime)
- if (scopeInfo.type === 'dynamic' && scopeInfo.isValidFakerVariable) {
+ // For valid dynamic variables, show appropriate read-only note based on type
+ if (scopeInfo.type === 'dynamic' && scopeInfo.isValidDynamicVariable) {
const readOnlyNote = document.createElement('div');
readOnlyNote.className = 'var-readonly-note';
- readOnlyNote.textContent = 'Generates random value on each request';
+ readOnlyNote.textContent = scopeInfo.isTimeBased
+ ? 'Generates current timestamp on each request'
+ : 'Generates random value on each request';
into.appendChild(readOnlyNote);
return into;
}
@@ -678,54 +682,89 @@ if (!SERVER_RENDERED) {
const state = cm.state.brunoVarInfo;
const options = state.options;
- let token = cm.getTokenAt(pos, true);
- if (token) {
- const line = cm.getLine(pos.line);
+ // Get the full line text where the hover happened
+ const line = cm.getLine(pos.line);
+ if (!line) return;
- // Find the opening {{ before the cursor
- let start = token.start;
- while (start > 0 && !line.substring(start - 2, start).includes('{{')) {
- // Stop if we encounter }} - we've gone past the start of our variable
- if (line.substring(start - 2, start) === '}}') {
- break;
- }
- start--;
- }
- if (line.substring(start - 2, start) === '{{') {
- start = start - 2;
+ // If the line doesn't even contain both braces, no need to run loops
+ if (!line.includes('{{') || !line.includes('}}')) {
+ return;
+ }
+
+ // lastIndexOf searches backward from the cursor indexOf searches forward
+ if (line.lastIndexOf('{{', pos.ch) === -1 || line.indexOf('}}', pos.ch) === -1) {
+ return;
+ }
+ let start = pos.ch;
+ let end = pos.ch;
+
+ // ---------- Find opening '{{' to the LEFT ----------
+ while (start > 0) {
+ const leftTwo = line.substring(start - 2, start);
+
+ // If we find opening braces, stop
+ if (leftTwo === '{{') {
+ start -= 2;
+ break;
}
- // Find the closing }} after the cursor
- let end = token.end;
- while (end < line.length && !line.substring(end, end + 2).includes('}}')) {
- // Stop if we encounter {{ - we've gone past the end of our variable
- if (line.substring(end, end + 2) === '{{') {
- break;
- }
- end++;
- }
- if (line.substring(end, end + 2) === '}}') {
- end = end + 2;
+ // If we cross a closing braces before finding '{{', we're not inside a variable
+ if (leftTwo === '}}') {
+ return;
}
- // Extract the full variable string including {{ and }}
- const fullVariableString = line.substring(start, end);
+ start--;
+ }
- // Only use the expanded string if it looks like a complete variable
- if (fullVariableString.startsWith('{{') && fullVariableString.endsWith('}}')) {
- token = {
- ...token,
- string: fullVariableString,
- start: start,
- end: end
- };
+ // If we reached the start of the line and didn't match '{{', return
+ if (start < 0 || line.substring(start, start + 2) !== '{{') {
+ return;
+ }
+
+ // ---------- Find closing '}}' to the RIGHT ----------
+ while (end < line.length) {
+ const rightTwo = line.substring(end, end + 2);
+
+ // If we find closing braces, stop
+ if (rightTwo === '}}') {
+ end += 2;
+ break;
}
- const brunoVarInfo = renderVarInfo(token, options);
- if (brunoVarInfo) {
- showPopup(cm, box, brunoVarInfo);
+ // If we hit another '{{' before a '}}', then this isn't a valid enclosing pair
+ if (rightTwo === '{{') {
+ return;
}
+
+ end++;
+ }
+ // If we reached end-of-line without finding '}}', return
+ if (end > line.length || line.substring(end - 2, end) !== '}}') {
+ return;
+ }
+
+ const fullVariableString = line.substring(start, end);
+
+ // Basic validation to ensure it's a non-empty variable
+ if (!fullVariableString.startsWith('{{') || !fullVariableString.endsWith('}}')) {
+ return;
+ }
+
+ // Prevent tooltips for empty variables like {{ }}
+ const inner = fullVariableString.slice(2, -2).trim();
+ if (!inner) return;
+
+ // Build a token object compatible with renderVarInfo
+ const token = {
+ string: fullVariableString,
+ start: start,
+ end: end
+ };
+
+ const brunoVarInfo = renderVarInfo(token, options);
+ if (brunoVarInfo) {
+ showPopup(cm, box, brunoVarInfo);
}
}
diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js
index 5595b826d..412bd715d 100644
--- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js
+++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js
@@ -8,8 +8,11 @@ jest.mock('@usebruno/common', () => ({
randomFirstName: jest.fn(() => 'John'),
randomLastName: jest.fn(() => 'Doe'),
randomEmail: jest.fn(() => 'john.doe@example.com'),
- randomUUID: jest.fn(() => '123e4567-e89b-12d3-a456-426614174000')
- }
+ randomUUID: jest.fn(() => '123e4567-e89b-12d3-a456-426614174000'),
+ timestamp: jest.fn(() => '1704067200'),
+ isoTimestamp: jest.fn(() => '2024-01-01T00:00:00.000Z')
+ },
+ timeBasedDynamicVars: new Set(['timestamp', 'isoTimestamp'])
}));
jest.mock('providers/ReduxStore', () => ({
@@ -467,6 +470,29 @@ describe('renderVarInfo', () => {
expect(warningNote).not.toBeNull();
expect(warningNote.textContent).toContain('Unknown dynamic variable');
});
+
+ it('should show time-based note for $timestamp variable', () => {
+ const { readOnlyNote, scopeBadge } = setupDynamicRender('$timestamp');
+
+ expect(scopeBadge.textContent).toBe('Dynamic');
+ expect(readOnlyNote).not.toBeNull();
+ expect(readOnlyNote.textContent).toBe('Generates current timestamp on each request');
+ });
+
+ it('should show time-based note for $isoTimestamp variable', () => {
+ const { readOnlyNote, scopeBadge } = setupDynamicRender('$isoTimestamp');
+
+ expect(scopeBadge.textContent).toBe('Dynamic');
+ expect(readOnlyNote).not.toBeNull();
+ expect(readOnlyNote.textContent).toBe('Generates current timestamp on each request');
+ });
+
+ it('should show random note for non-time-based dynamic variables', () => {
+ const { readOnlyNote } = setupDynamicRender('$randomEmail');
+
+ expect(readOnlyNote).not.toBeNull();
+ expect(readOnlyNote.textContent).toBe('Generates random value on each request');
+ });
});
describe('OAuth2 variable rendering', () => {
diff --git a/packages/bruno-app/src/utils/codemirror/linkAware.js b/packages/bruno-app/src/utils/codemirror/linkAware.js
index 3849abeeb..501cfd7b1 100644
--- a/packages/bruno-app/src/utils/codemirror/linkAware.js
+++ b/packages/bruno-app/src/utils/codemirror/linkAware.js
@@ -59,7 +59,18 @@ function markUrls(editor, linkify, linkClass, linkHint) {
const matches = linkify.match(lineContent);
if (!matches) continue;
+ const variablePatterns = [];
+ const variablePattern = /\{\{[^}]*\}\}/g;
+ let varMatch;
+ while ((varMatch = variablePattern.exec(lineContent)) !== null) {
+ variablePatterns.push({ start: varMatch.index, end: varMatch.index + varMatch[0].length });
+ }
matches.forEach(({ index, lastIndex, url }) => {
+ const isInVariable = variablePatterns.some(
+ ({ start, end }) => index < end && lastIndex > start
+ );
+ if (isInVariable) return;
+
try {
editor.markText(
{ line: lineNum, ch: index },
diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js
index 669446a29..43b25efb2 100644
--- a/packages/bruno-app/src/utils/collections/export.js
+++ b/packages/bruno-app/src/utils/collections/export.js
@@ -99,6 +99,7 @@ export const exportCollection = (collection, version) => {
// delete process variables
delete collection.processEnvVariables;
+ delete collection.workspaceProcessEnvVariables;
deleteUidsInItems(collection.items);
deleteUidsInEnvs(collection.environments);
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 6eb2bd78d..dcad3b168 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -1174,7 +1174,14 @@ export const getAllVariables = (collection, item) => {
const pathParams = getPathParams(item);
const { globalEnvironmentVariables = {} } = collection;
- const { processEnvVariables = {}, runtimeVariables = {}, promptVariables = {} } = collection;
+ const { processEnvVariables = {}, runtimeVariables = {}, promptVariables = {}, workspaceProcessEnvVariables = {} } = collection;
+
+ // Merge workspace and collection processEnvVariables (collection takes priority)
+ const mergedProcessEnvVariables = {
+ ...workspaceProcessEnvVariables,
+ ...processEnvVariables
+ };
+
const mergedVariables = {
...folderVariables,
...requestVariables,
@@ -1216,7 +1223,7 @@ export const getAllVariables = (collection, item) => {
maskedEnvVariables: uniqueMaskedVariables,
process: {
env: {
- ...processEnvVariables
+ ...mergedProcessEnvVariables
}
}
};
diff --git a/packages/bruno-app/src/utils/response/index.js b/packages/bruno-app/src/utils/response/index.js
index 5e19bb2f2..18ab27fe3 100644
--- a/packages/bruno-app/src/utils/response/index.js
+++ b/packages/bruno-app/src/utils/response/index.js
@@ -92,6 +92,36 @@ const isLikelyText = (buffer) => {
return (textChars / sampleSize) > 0.85;
};
+/**
+ * Helper to detect SVG content from text buffer
+ * SVG files may start with XML declaration, comments, or whitespace before the