mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 01:48:33 +00:00
Compare commits
40 Commits
v2.14.1
...
feature/ht
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a188575a0 | ||
|
|
76a1532695 | ||
|
|
efad149afc | ||
|
|
2d2a17c90f | ||
|
|
3d8d93f20d | ||
|
|
94c33e6833 | ||
|
|
2ef451c80b | ||
|
|
044fcce49f | ||
|
|
dffb600dab | ||
|
|
99478b7068 | ||
|
|
252fd386b7 | ||
|
|
b982f6db16 | ||
|
|
3b4e5686b8 | ||
|
|
2ef1a1948b | ||
|
|
f2273821b0 | ||
|
|
8a22f6acb8 | ||
|
|
6049530634 | ||
|
|
5784b04129 | ||
|
|
fec37f43e0 | ||
|
|
b8fef7b796 | ||
|
|
04f8dba1b1 | ||
|
|
cd1500bd01 | ||
|
|
e8a8b5d220 | ||
|
|
bc3dfc59f6 | ||
|
|
2c399ca33c | ||
|
|
ccac4d6112 | ||
|
|
fc5093eab4 | ||
|
|
631b05330d | ||
|
|
be34c86c47 | ||
|
|
67c9f1373e | ||
|
|
6628f95677 | ||
|
|
44ed0b01d8 | ||
|
|
45cfbc5c49 | ||
|
|
14bece8696 | ||
|
|
9e19244665 | ||
|
|
f439f2de9a | ||
|
|
e844d35b03 | ||
|
|
26e140aca0 | ||
|
|
b15c421270 | ||
|
|
1656e951fb |
@@ -16,6 +16,7 @@
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Dutch](docs/contributing/contributing_nl.md)
|
||||
| [فارسی](docs/contributing/contributing_fa.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
@@ -74,6 +75,7 @@ npm run build:bruno-filestore
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
@@ -94,18 +96,22 @@ npm run dev:electron
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
# run electron and react app concurrently
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
92
docs/contributing/contributing_fa.md
Normal file
92
docs/contributing/contributing_fa.md
Normal file
@@ -0,0 +1,92 @@
|
||||
[English](../../contributing.md)
|
||||
|
||||
## با هم، Bruno را بهتر میکنیم!
|
||||
|
||||
خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راهاندازی Bruno روی سیستم شما آورده شده است.
|
||||
|
||||
### فناوریهای استفادهشده
|
||||
|
||||
به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بستهبندی نسخه دسکتاپ (که امکان مجموعههای محلی را فراهم میکند) استفاده میکنیم.
|
||||
|
||||
کتابخانههایی که استفاده میکنیم:
|
||||
|
||||
- CSS - Tailwind استایل
|
||||
- Codemirror - ویرایشگر کد
|
||||
- Redux - مدیریت وضعیت
|
||||
- Tabler Icons - آیکونها
|
||||
- formik - فرمها
|
||||
- Yup اعتبارسنجی اسکیمـا
|
||||
- axios - کلاینت درخواست
|
||||
- chokidar - پایشگر سیستم فایل
|
||||
|
||||
### پیشنیازها
|
||||
|
||||
شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده میکنیم.
|
||||
|
||||
### شروع به کدنویسی
|
||||
|
||||
برای راهاندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:
|
||||
|
||||
### ارسال Pull Request
|
||||
|
||||
1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>
|
||||
2 - لطفاً از فرمت نامگذاری شاخهها استفاده کنید:
|
||||
|
||||
- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.
|
||||
- feature/dark-mode : مثال
|
||||
- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.
|
||||
- bugfix/bug-1 : مثال
|
||||
|
||||
## توسعه
|
||||
|
||||
به فارسی برونو یا Bruno بهصورت یک اپلیکیشن «سنگین» توسعه داده میشود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راهاندازی نمایید.
|
||||
|
||||
### نیازمندی توسعه
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### اجرای محلی
|
||||
|
||||
```bash
|
||||
# از ورژن NodeJS 18 استفاده کنید
|
||||
nvm use
|
||||
|
||||
# نصب وابستگیها
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# ساخت مستندات GraphQL
|
||||
npm run build:graphql-docs
|
||||
|
||||
# ساخت bruno-query
|
||||
npm run build:bruno-query
|
||||
|
||||
# اجرای اپ Next (ترمینال 1)
|
||||
npm run dev:web
|
||||
|
||||
# اجرای اپ Electron (ترمینال 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### عیبیابی
|
||||
|
||||
ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیجهای لازم را نصب میکند.
|
||||
|
||||
```shell
|
||||
# حذف پوشه node_modules در زیردایرکتوریها
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# حذف فایل package-lock.json در زیردایرکتوریها
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### تستها
|
||||
|
||||
```bash
|
||||
# اجرای تستهای schema مربوط به bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# اجرای تستها در همه فضاهای کاری (در صورت وجود)
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
8
docs/publishing/publishing_fa.md
Normal file
8
docs/publishing/publishing_fa.md
Normal file
@@ -0,0 +1,8 @@
|
||||
[English](../../publishing.md)
|
||||
|
||||
### انتشار Bruno در یک پکیج منیجر جدید
|
||||
|
||||
اگرچه کد ما متنباز است و همه میتوانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بستههای جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیتهاب ثبت کنید.
|
||||
|
||||
اگرچه بیشتر قابلیتهای ما رایگان و متنباز هستند (شامل REST و GraphQL Apis)،
|
||||
ما تلاش میکنیم بین اصول متنباز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269
|
||||
143
docs/readme/readme_fa.md
Normal file
143
docs/readme/readme_fa.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md)
|
||||
| [Українська](./readme_ua.md)
|
||||
| [Русский](./readme_ru.md)
|
||||
| [Türkçe](./readme_tr.md)
|
||||
| [Deutsch](./readme_de.md)
|
||||
| [Français](./readme_fr.md)
|
||||
| [Português (BR)](./readme_pt_br.md)
|
||||
| [한국어](./readme_kr.md)
|
||||
| [বাংলা](./readme_bn.md)
|
||||
| [Español](./readme_es.md)
|
||||
| **فارسی**
|
||||
| [Română](./readme_ro.md)
|
||||
| [Polski](./readme_pl.md)
|
||||
| [简体中文](./readme_cn.md)
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.
|
||||
|
||||
برونو مجموعههای شما را مستقیماً در یک پوشه روی فایلسیستم شما ذخیره میکند. ما از یک زبان نشانهگذاری ساده به نام Bru برای ذخیره اطلاعات درخواستهای API استفاده میکنیم.
|
||||
|
||||
شما میتوانید برای همکاری روی مجموعههای API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.
|
||||
|
||||
برونو فقط به صورت آفلاین کار میکند. هیچ برنامهای برای اضافه کردن همگامسازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی دادههای شما اهمیت میدهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. میتوانید چشمانداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.
|
||||
[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### نصب
|
||||
|
||||
برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.
|
||||
|
||||
همچنین میتوانید برونو را از طریق مدیر بستههایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.
|
||||
|
||||
```sh
|
||||
# بر روی مک از طریق brew
|
||||
brew install bruno
|
||||
|
||||
# بر روی ویندوز از طریق Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Snap
|
||||
snap install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### روی پلتفرمهای مختلف کار میکند 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### همکاری از طریق گیت 👩💻🧑💻
|
||||
|
||||
یا هر سیستم کنترل نسخهای که ترجیح میدهید
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### لینکهای مهم 📌
|
||||
|
||||
- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [مستندات](https://docs.usebruno.com)
|
||||
- [وبسایت](https://www.usebruno.com)
|
||||
- [اشتراک ها](https://www.usebruno.com/pricing)
|
||||
- [دانلود](https://www.usebruno.com/downloads)
|
||||
|
||||
### ویدیوها 🎥
|
||||
|
||||
- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### حمایت ❤️
|
||||
|
||||
جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!
|
||||
|
||||
### تجربههای به اشتراک گذاشتهشده 📣
|
||||
|
||||
اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربههای خود را به اشتراک بگذارید. [تجربههای خود را در بحث گیتهاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).
|
||||
|
||||
### انتشار برونو در یک پکیچ منیجر جدید
|
||||
|
||||
لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.
|
||||
|
||||
### مشارکت 👩💻🧑💻
|
||||
|
||||
خوشحالم که میخواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).
|
||||
|
||||
حتی اگر نمیتوانید از طریق کدنویسی مشارکت کنید، در گزارش باگها و درخواست قابلیتهای جدید که به حل نیازهای شما کمک میکند تردید نکنید.
|
||||
|
||||
### نویسنده ها
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### در ارتباط باشید 🌐
|
||||
|
||||
[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />
|
||||
[وبسایت](https://www.usebruno.com) <br />
|
||||
[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[لینکدین](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### برند
|
||||
|
||||
**نام**
|
||||
|
||||
به فارسی برونو - `Bruno` یک علامت تجاری ثبتشده متعلق به [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**لوگو**
|
||||
|
||||
لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### مجوز 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
63
package-lock.json
generated
63
package-lock.json
generated
@@ -8232,12 +8232,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/google-protobuf": {
|
||||
"version": "3.15.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
|
||||
"integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
@@ -8373,9 +8367,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
|
||||
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
@@ -8388,15 +8382,6 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.set": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.9.tgz",
|
||||
"integrity": "sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "12.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
||||
@@ -14235,9 +14220,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-format": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-format/-/fast-json-format-0.3.0.tgz",
|
||||
"integrity": "sha512-B95psGYXJ5XItmxLR6JFcQRQafDyfy8ecHiV/jWCJF9oCIA9/o+wt89cGW61D04xf07yCpIaevvCQbgeJ9w8lQ==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-format/-/fast-json-format-0.4.0.tgz",
|
||||
"integrity": "sha512-HEomBtr2fYaVX3iaRdcVLU7Qd3SQhCYvXlMMM9RNaihfIaj5bIC7ADqw/bAPSg/uyX6FIBPq69ioXq0B4Cb6eA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
@@ -15183,9 +15168,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/google-protobuf": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz",
|
||||
"integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-4.0.0.tgz",
|
||||
"integrity": "sha512-b8wmenhUMf2WNL+xIJ/slvD/hEE6V3nRnG86O2bzkBrMweM9gnqZE1dfXlDjibY3aXJXDNbAHepevYyQ7qWKsQ==",
|
||||
"license": "(BSD-3-Clause AND Apache-2.0)"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
@@ -15312,20 +15297,18 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/grpc-reflection-js": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/grpc-reflection-js/-/grpc-reflection-js-0.3.0.tgz",
|
||||
"integrity": "sha512-3lhTlQluPxVgbowCXA3tAZC3RJW+GSOUkguLNYl1QffYRiutUB3RDfPkQFTcrCFJgNiIIxx+iJkr8s3uSp3zWA==",
|
||||
"node_modules/grpc-js-reflection-client": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/grpc-js-reflection-client/-/grpc-js-reflection-client-1.3.0.tgz",
|
||||
"integrity": "sha512-eJ5/m1pXpcheSjOGExktU69WPUKnL4Su3IxGJYYYjy3/w19vE8dH7Wi46G5T92bpM0eZWftjiM5HduX8CjPq9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/lodash.set": "^4.3.6",
|
||||
"google-protobuf": "^3.12.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"protobufjs": "^7.2.2"
|
||||
"@types/lodash": "^4.17.15",
|
||||
"lodash": "^4.17.21",
|
||||
"protobufjs": "^7.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grpc/grpc-js": "^1.0.0"
|
||||
"@grpc/grpc-js": "^1.12.6"
|
||||
}
|
||||
},
|
||||
"node_modules/har-schema": {
|
||||
@@ -18634,12 +18617,6 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
@@ -26874,7 +26851,7 @@
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"fast-json-format": "~0.3.0",
|
||||
"fast-json-format": "~0.4.0",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -26883,6 +26860,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"hexy": "^0.3.5",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
@@ -32129,7 +32107,8 @@
|
||||
"@types/qs": "^6.9.18",
|
||||
"axios": "^1.9.0",
|
||||
"debug": "^4.4.3",
|
||||
"grpc-reflection-js": "^0.3.0",
|
||||
"google-protobuf": "^4.0.0",
|
||||
"grpc-js-reflection-client": "^1.3.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"system-ca": "^2.0.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"fast-json-format": "~0.3.0",
|
||||
"fast-json-format": "~0.4.0",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -37,6 +37,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"hexy": "^0.3.5",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrappe
|
||||
|
||||
const AccordionContext = createContext();
|
||||
|
||||
const Accordion = ({ children, defaultIndex }) => {
|
||||
const Accordion = ({ children, defaultIndex, dataTestId }) => {
|
||||
const [openIndex, setOpenIndex] = useState(defaultIndex);
|
||||
|
||||
const toggleItem = (index) => {
|
||||
@@ -13,7 +13,7 @@ const Accordion = ({ children, defaultIndex }) => {
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
|
||||
<div>{children}</div>
|
||||
<div data-testid={dataTestId}>{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import Dropdown from 'components/Dropdown';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
|
||||
|
||||
@@ -16,9 +16,9 @@ const ApiKeyAuth = ({ collection }) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const apikeyAuth = get(collection, 'root.request.auth.apikey', {});
|
||||
const apikeyAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.apikey', {}) : get(collection, 'root.request.auth.apikey', {});
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,7 @@ const AuthMode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AwsV4Auth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
||||
const awsv4Auth = collection.draft?.root ? get(collection, 'draft.root.request.auth.awsv4', {}) : get(collection, 'root.request.auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BasicAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(collection, 'root.request.auth.basic', {});
|
||||
const basicAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.basic', {}) : get(collection, 'root.request.auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BearerAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
|
||||
const bearerToken = collection.draft?.root ? get(collection, 'draft.root.request.auth.bearer.token', '') : get(collection, 'root.request.auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DigestAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(collection, 'root.request.auth.digest', {});
|
||||
const digestAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.digest', {}) : get(collection, 'root.request.auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ const NTLMAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
|
||||
const ntlmAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.ntlm', {}) : get(collection, 'root.request.auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -14,10 +14,10 @@ const GrantTypeComponentMap = ({collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
};
|
||||
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
|
||||
switch (grantType) {
|
||||
@@ -40,7 +40,7 @@ const GrantTypeComponentMap = ({collection }) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ collection }) => {
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WsseAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
|
||||
const wsseAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.wsse', {}) : get(collection, 'root.request.auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUserChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -8,17 +8,17 @@ import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import WsseAuth from './WsseAuth';
|
||||
import ApiKeyAuth from './ApiKeyAuth/';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
|
||||
@@ -9,8 +9,18 @@ import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index';
|
||||
import { useTheme } from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const ClientCertSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Get client certs from draft if exists, otherwise from brunoConfig
|
||||
const clientCertConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
|
||||
: get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const certFilePathInputRef = useRef();
|
||||
const keyFilePathInputRef = useRef();
|
||||
const pfxFilePathInputRef = useRef();
|
||||
@@ -63,7 +73,19 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
|
||||
passphrase: values.passphrase
|
||||
};
|
||||
}
|
||||
onUpdate(relevantValues);
|
||||
|
||||
// Add the new cert to the existing certs in draft
|
||||
const updatedCerts = [...clientCertConfig, relevantValues];
|
||||
const clientCertificates = {
|
||||
enabled: true,
|
||||
certs: updatedCerts
|
||||
};
|
||||
|
||||
dispatch(updateCollectionClientCertificates({
|
||||
collectionUid: collection.uid,
|
||||
clientCertificates
|
||||
}));
|
||||
|
||||
formik.resetForm();
|
||||
resetFileInputFields();
|
||||
}
|
||||
@@ -81,9 +103,15 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
|
||||
};
|
||||
|
||||
const resetFileInputFields = () => {
|
||||
certFilePathInputRef.current.value = '';
|
||||
keyFilePathInputRef.current.value = '';
|
||||
pfxFilePathInputRef.current.value = '';
|
||||
if (certFilePathInputRef.current) {
|
||||
certFilePathInputRef.current.value = '';
|
||||
}
|
||||
if (keyFilePathInputRef.current) {
|
||||
keyFilePathInputRef.current.value = '';
|
||||
}
|
||||
if (pfxFilePathInputRef.current) {
|
||||
pfxFilePathInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleTypeChange = (e) => {
|
||||
@@ -99,6 +127,21 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (indexToRemove) => {
|
||||
const updatedCerts = clientCertConfig.filter((cert, index) => index !== indexToRemove);
|
||||
const clientCertificates = {
|
||||
enabled: true,
|
||||
certs: updatedCerts
|
||||
};
|
||||
|
||||
dispatch(updateCollectionClientCertificates({
|
||||
collectionUid: collection.uid,
|
||||
clientCertificates
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full">
|
||||
<div className="text-xs mb-4 text-muted">Add client certificates to be used for specific domains.</div>
|
||||
@@ -118,9 +161,9 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
|
||||
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
|
||||
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
|
||||
</div>
|
||||
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
|
||||
<button onClick={() => handleRemove(index)} className="remove-certificate ml-2">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -329,10 +372,14 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
|
||||
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<div className="mt-6 flex flex-row gap-2 items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Add
|
||||
</button>
|
||||
<div className="h-4 border-l border-gray-600"></div>
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -14,7 +14,7 @@ const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme } = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = get(collection, 'root.docs', '');
|
||||
const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', '');
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const toggleViewMode = () => {
|
||||
@@ -31,17 +31,17 @@ const Docs = ({ collection }) => {
|
||||
};
|
||||
|
||||
const handleDiscardChanges = () => {
|
||||
dispatch(
|
||||
dispatch((
|
||||
updateCollectionDocs({
|
||||
collectionUid: collection.uid,
|
||||
docs: docs
|
||||
})
|
||||
}))
|
||||
);
|
||||
toggleViewMode();
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
toggleViewMode();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
deleteCollectionHeader,
|
||||
setCollectionHeaders
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
@@ -21,7 +21,7 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
const Headers = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
@@ -40,7 +40,7 @@ const Headers = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
brunoConfig: { presets: presets = {} }
|
||||
} = collection;
|
||||
const initialPresets = { requestType: 'http', requestUrl: '' };
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestType: presets.requestType || 'http',
|
||||
requestUrl: presets.requestUrl || ''
|
||||
},
|
||||
onSubmit: (newPresets) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.presets = newPresets;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
toast.success('Collection presets updated');
|
||||
}
|
||||
});
|
||||
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentPresets = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.presets', initialPresets)
|
||||
: get(collection, 'brunoConfig.presets', initialPresets);
|
||||
|
||||
// Helper to update presets config
|
||||
const updatePresets = (updates) => {
|
||||
const updatedPresets = { ...currentPresets, ...updates };
|
||||
dispatch(updateCollectionPresets({
|
||||
collectionUid: collection.uid,
|
||||
presets: updatedPresets
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleRequestTypeChange = (e) => {
|
||||
updatePresets({ requestType: e.target.value });
|
||||
};
|
||||
|
||||
const handleRequestUrlChange = (e) => {
|
||||
updatePresets({ requestUrl: e.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
These presets will be used as the default values for new requests in this collection.
|
||||
</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Request Type
|
||||
@@ -42,9 +49,9 @@ const PresetsSettings = ({ collection }) => {
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleRequestTypeChange}
|
||||
value="http"
|
||||
checked={formik.values.requestType === 'http'}
|
||||
checked={(currentPresets.requestType || 'http') === 'http'}
|
||||
/>
|
||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
@@ -55,9 +62,9 @@ const PresetsSettings = ({ collection }) => {
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleRequestTypeChange}
|
||||
value="graphql"
|
||||
checked={formik.values.requestType === 'graphql'}
|
||||
checked={(currentPresets.requestType || 'http') === 'graphql'}
|
||||
/>
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
@@ -68,9 +75,9 @@ const PresetsSettings = ({ collection }) => {
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleRequestTypeChange}
|
||||
value="grpc"
|
||||
checked={formik.values.requestType === 'grpc'}
|
||||
checked={(currentPresets.requestType || 'http') === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
@@ -93,8 +100,8 @@ const PresetsSettings = ({ collection }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
onChange={handleRequestUrlChange}
|
||||
value={currentPresets.requestUrl || ''}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -102,11 +109,11 @@ const PresetsSettings = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import {
|
||||
IconTrash,
|
||||
@@ -10,8 +11,10 @@ import {
|
||||
import { getBasename } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const ProtobufSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
protoFiles,
|
||||
importPaths,
|
||||
@@ -27,6 +30,8 @@ const ProtobufSettings = ({ collection }) => {
|
||||
} = useProtoFileManagement(collection);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
// Get file path using the ipcRenderer
|
||||
const getProtoFile = async (event) => {
|
||||
const files = event?.files;
|
||||
@@ -164,7 +169,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="flex items-center">
|
||||
<IconFile size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100" data-testid="protobuf-proto-file-name">
|
||||
{getBasename(collection.pathname, file.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
@@ -329,6 +334,12 @@ const ProtobufSettings = ({ collection }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,106 +1,155 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import React from 'react';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
const proxySchema = Yup.object({
|
||||
enabled: Yup.string().oneOf(['global', 'true', 'false']),
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('enabled', {
|
||||
is: 'true',
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.min(1)
|
||||
.max(65535)
|
||||
.typeError('Specify port between 1 and 65535')
|
||||
.nullable()
|
||||
.transform((_, val) => (val ? Number(val) : null)),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: 'true',
|
||||
then: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (username) => username.required('Specify username for proxy authentication.')
|
||||
})
|
||||
.max(1024),
|
||||
password: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (password) => password.required('Specify password for proxy authentication.')
|
||||
})
|
||||
.max(1024)
|
||||
})
|
||||
})
|
||||
.optional(),
|
||||
bypassProxy: Yup.string().optional().max(1024)
|
||||
});
|
||||
const ProxySettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialProxyConfig = { enabled: 'global', protocol: 'http', hostname: '', port: '', auth: { enabled: false, username: '', password: '' }, bypassProxy: '' };
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
enabled: proxyConfig.enabled || 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
auth: {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
},
|
||||
validationSchema: proxySchema,
|
||||
onSubmit: (values) => {
|
||||
proxySchema
|
||||
.validate(values, { abortEarly: true })
|
||||
.then((validatedProxy) => {
|
||||
// serialize 'enabled' to boolean
|
||||
if (validatedProxy.enabled === 'true') {
|
||||
validatedProxy.enabled = true;
|
||||
} else if (validatedProxy.enabled === 'false') {
|
||||
validatedProxy.enabled = false;
|
||||
}
|
||||
// Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentProxyConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.proxy', initialProxyConfig)
|
||||
: get(collection, 'brunoConfig.proxy', initialProxyConfig);
|
||||
|
||||
onUpdate(validatedProxy);
|
||||
})
|
||||
.catch((error) => {
|
||||
let errMsg = error.message || 'Preferences validation error';
|
||||
toast.error(errMsg);
|
||||
});
|
||||
}
|
||||
});
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: proxyConfig.enabled === true ? 'true' : proxyConfig.enabled === false ? 'false' : 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
const validateHostnameOnChange = (hostname) => {
|
||||
if (hostname && hostname.length > 1024) {
|
||||
toast.error('Hostname must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validatePortOnChange = (port) => {
|
||||
if (!port || port === '') {
|
||||
return true; // Allow empty port during typing
|
||||
}
|
||||
const portNum = Number(port);
|
||||
if (isNaN(portNum)) {
|
||||
toast.error('Port must be a valid number');
|
||||
return false;
|
||||
}
|
||||
if (portNum < 1 || portNum > 65535) {
|
||||
toast.error('Port must be between 1 and 65535');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateAuthUsernameOnChange = (username) => {
|
||||
if (username && username.length > 1024) {
|
||||
toast.error('Username must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateAuthPasswordOnChange = (password) => {
|
||||
if (password && password.length > 1024) {
|
||||
toast.error('Password must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateBypassProxyOnChange = (bypassProxy) => {
|
||||
if (bypassProxy && bypassProxy.length > 1024) {
|
||||
toast.error('Bypass proxy must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Helper to update proxy config
|
||||
const updateProxy = (updates) => {
|
||||
const updatedProxy = { ...currentProxyConfig, ...updates };
|
||||
dispatch(updateCollectionProxy({
|
||||
collectionUid: collection.uid,
|
||||
proxy: updatedProxy
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleEnabledChange = (e) => {
|
||||
const value = e.target.value;
|
||||
// Convert string to boolean or keep as 'global'
|
||||
const enabled = value === 'true' ? true : value === 'false' ? false : 'global';
|
||||
updateProxy({ enabled });
|
||||
};
|
||||
|
||||
const handleProtocolChange = (e) => {
|
||||
updateProxy({ protocol: e.target.value });
|
||||
};
|
||||
|
||||
const handleHostnameChange = (e) => {
|
||||
const hostname = e.target.value;
|
||||
if (validateHostnameOnChange(hostname)) {
|
||||
updateProxy({ hostname });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePortChange = (e) => {
|
||||
const port = e.target.value ? Number(e.target.value) : '';
|
||||
if (validatePortOnChange(port)) {
|
||||
updateProxy({ port });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthEnabledChange = (e) => {
|
||||
updateProxy({
|
||||
auth: {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
...currentProxyConfig.auth,
|
||||
enabled: e.target.checked
|
||||
}
|
||||
});
|
||||
}, [proxyConfig]);
|
||||
};
|
||||
|
||||
const handleAuthUsernameChange = (e) => {
|
||||
const username = e.target.value;
|
||||
if (validateAuthUsernameOnChange(username)) {
|
||||
updateProxy({
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
username
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthPasswordChange = (e) => {
|
||||
const password = e.target.value;
|
||||
if (validateAuthPasswordOnChange(password)) {
|
||||
updateProxy({
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
password
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBypassProxyChange = (e) => {
|
||||
const bypassProxy = e.target.value;
|
||||
if (validateBypassProxyOnChange(bypassProxy)) {
|
||||
updateProxy({ bypassProxy });
|
||||
}
|
||||
};
|
||||
|
||||
const enabledValue = currentProxyConfig.enabled === true ? 'true' : currentProxyConfig.enabled === false ? 'false' : 'global';
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">Configure proxy settings for this collection.</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
@@ -120,8 +169,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value="global"
|
||||
checked={formik.values.enabled === 'global'}
|
||||
onChange={formik.handleChange}
|
||||
checked={enabledValue === 'global'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
global
|
||||
@@ -130,9 +179,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value={'true'}
|
||||
checked={formik.values.enabled === 'true'}
|
||||
onChange={formik.handleChange}
|
||||
value="true"
|
||||
checked={enabledValue === 'true'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
enabled
|
||||
@@ -141,9 +190,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value={'false'}
|
||||
checked={formik.values.enabled === 'false'}
|
||||
onChange={formik.handleChange}
|
||||
value="false"
|
||||
checked={enabledValue === 'false'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
disabled
|
||||
@@ -160,8 +209,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={formik.values.protocol === 'http'}
|
||||
onChange={formik.handleChange}
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'http'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
@@ -171,8 +220,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={formik.values.protocol === 'https'}
|
||||
onChange={formik.handleChange}
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'https'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
@@ -182,8 +231,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={formik.values.protocol === 'socks4'}
|
||||
onChange={formik.handleChange}
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'socks4'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
@@ -193,8 +242,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={formik.values.protocol === 'socks5'}
|
||||
onChange={formik.handleChange}
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'socks5'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
@@ -214,12 +263,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.hostname || ''}
|
||||
onChange={handleHostnameChange}
|
||||
value={currentProxyConfig.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
@@ -234,12 +280,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
onChange={handlePortChange}
|
||||
value={currentProxyConfig.port || ''}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
@@ -248,8 +291,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={formik.values.auth.enabled}
|
||||
onChange={formik.handleChange}
|
||||
checked={currentProxyConfig.auth?.enabled || false}
|
||||
onChange={handleAuthEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -266,12 +309,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.username}
|
||||
onChange={formik.handleChange}
|
||||
value={currentProxyConfig.auth?.username || ''}
|
||||
onChange={handleAuthUsernameChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
@@ -287,8 +327,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.password}
|
||||
onChange={formik.handleChange}
|
||||
value={currentProxyConfig.auth?.password || ''}
|
||||
onChange={handleAuthPasswordChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -298,9 +338,6 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
@@ -316,19 +353,16 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.bypassProxy || ''}
|
||||
onChange={handleBypassProxyChange}
|
||||
value={currentProxyConfig.bypassProxy || ''}
|
||||
/>
|
||||
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = get(collection, 'root.request.script.req', '');
|
||||
const responseScript = get(collection, 'root.request.script.res', '');
|
||||
const [activeTab, setActiveTab] = useState('pre-request');
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');
|
||||
const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
// Refresh CodeMirror when tab becomes visible
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {
|
||||
preRequestEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
|
||||
postResponseEditorRef.current.editor.refresh();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeTab]);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateCollectionRequestScript({
|
||||
@@ -34,42 +51,51 @@ const Script = ({ collection }) => {
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<StyledWrapper className="w-full flex flex-col h-full pt-4">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Write pre and post-request scripts that will run before and after any request in this collection is sent.
|
||||
</div>
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pre-request">Pre Request</TabsTrigger>
|
||||
<TabsTrigger value="post-response">Post Response</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={preRequestEditorRef}
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="post-response" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={postResponseEditorRef}
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -3,13 +3,13 @@ import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = get(collection, 'root.request.tests', '');
|
||||
const tests = collection.draft?.root ? get(collection, 'draft.root.request.tests', '') : get(collection, 'root.request.tests', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
@@ -23,7 +23,7 @@ const Tests = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
|
||||
@@ -3,7 +3,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -28,7 +28,7 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const onSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ProxySettings from './ProxySettings';
|
||||
@@ -31,65 +28,26 @@ const CollectionSettings = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const root = collection?.root;
|
||||
const root = collection?.draft?.root || collection?.root;
|
||||
const hasScripts = root?.request?.script?.res || root?.request?.script?.req;
|
||||
const hasTests = root?.request?.tests;
|
||||
const hasDocs = root?.docs;
|
||||
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
const activeHeadersCount = headers.filter((header) => header.enabled).length;
|
||||
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})).mode || 'none';
|
||||
|
||||
const presets = get(collection, 'brunoConfig.presets', []);
|
||||
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', []) : get(collection, 'brunoConfig.presets', []);
|
||||
const hasPresets = presets && presets.requestUrl !== '';
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyEnabled = proxyConfig.hostname ? true : false;
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = get(collection, 'brunoConfig.protobuf', {});
|
||||
|
||||
const onProxySettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.proxy = config;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully.');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const onClientCertSettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
if (!brunoConfig.clientCertificates) {
|
||||
brunoConfig.clientCertificates = {
|
||||
enabled: true,
|
||||
certs: [config]
|
||||
};
|
||||
} else {
|
||||
brunoConfig.clientCertificates.certs.push(config);
|
||||
}
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const onClientCertSettingsRemove = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.clientCertificates.certs = brunoConfig.clientCertificates.certs.filter(
|
||||
(item) => item.domain != config.domain
|
||||
);
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.protobuf', {}) : get(collection, 'brunoConfig.protobuf', {});
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -115,15 +73,12 @@ const CollectionSettings = ({ collection }) => {
|
||||
return <Presets collection={collection} />;
|
||||
}
|
||||
case 'proxy': {
|
||||
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
||||
return <ProxySettings collection={collection} />;
|
||||
}
|
||||
case 'clientCert': {
|
||||
return (
|
||||
<ClientCertSettings
|
||||
collection={collection}
|
||||
clientCertConfig={clientCertConfig}
|
||||
onUpdate={onClientCertSettingsUpdate}
|
||||
onRemove={onClientCertSettingsRemove}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
let request = get(folder, 'root.request', {});
|
||||
const folderRoot = folder?.draft || folder?.root;
|
||||
let request = get(folderRoot, 'request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
|
||||
|
||||
switch (grantType) {
|
||||
@@ -45,13 +46,15 @@ const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
|
||||
const Auth = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
let request = get(folder, 'root.request', {});
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
const folderRoot = folder?.draft || folder?.root;
|
||||
let request = get(folderRoot, 'request', {});
|
||||
const authMode = get(folderRoot, 'request.auth.mode');
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
@@ -66,7 +69,8 @@ const Auth = ({ collection, folder }) => {
|
||||
for (let i = 0; i < folderTreePath.length - 1; i++) {
|
||||
const parentFolder = folderTreePath[i];
|
||||
if (parentFolder.type === 'folder') {
|
||||
const folderAuth = get(parentFolder, 'root.request.auth');
|
||||
const parentFolderRoot = parentFolder?.draft || parentFolder?.root;
|
||||
const folderAuth = get(parentFolderRoot, 'request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
|
||||
@@ -11,7 +11,7 @@ const AuthMode = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
const authMode = folder.draft ? get(folder, 'draft.request.auth.mode') : get(folder, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,7 @@ const Documentation = ({ collection, folder }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = get(folder, 'root.docs', '');
|
||||
const docs = folder.draft ? get(folder, 'draft.docs', '') : get(folder, 'root.docs', '');
|
||||
|
||||
const toggleViewMode = () => {
|
||||
setIsEditing((prev) => !prev);
|
||||
|
||||
@@ -16,7 +16,7 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
const Headers = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
const headers = folder.draft ? get(folder, 'draft.request.headers', []) : get(folder, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateFolderRequestScript, updateFolderResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = get(folder, 'root.request.script.req', '');
|
||||
const responseScript = get(folder, 'root.request.script.res', '');
|
||||
const [activeTab, setActiveTab] = useState('pre-request');
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const requestScript = folder.draft ? get(folder, 'draft.request.script.req', '') : get(folder, 'root.request.script.req', '');
|
||||
const responseScript = folder.draft ? get(folder, 'draft.request.script.res', '') : get(folder, 'root.request.script.res', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
// Refresh CodeMirror when tab becomes visible
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {
|
||||
preRequestEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
|
||||
postResponseEditorRef.current.editor.refresh();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeTab]);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderRequestScript({
|
||||
@@ -40,38 +57,47 @@ const Script = ({ collection, folder }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<StyledWrapper className="w-full flex flex-col h-full pt-4">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Pre and post-request scripts that will run before and after any request inside this folder is sent.
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pre-request">Pre Request</TabsTrigger>
|
||||
<TabsTrigger value="post-response">Post Response</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={preRequestEditorRef}
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="post-response" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={postResponseEditorRef}
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -9,7 +9,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = get(folder, 'root.request.tests', '');
|
||||
const tests = folder.draft ? get(folder, 'draft.request.tests', '') : get(folder, 'root.request.tests', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useDispatch } from 'react-redux';
|
||||
|
||||
const Vars = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = get(folder, 'root.request.vars.req', []);
|
||||
const responseVars = get(folder, 'root.request.vars.res', []);
|
||||
const requestVars = folder.draft ? get(folder, 'draft.request.vars.req', []) : get(folder, 'root.request.vars.req', []);
|
||||
const responseVars = folder.draft ? get(folder, 'draft.request.vars.res', []) : get(folder, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
|
||||
@@ -20,7 +20,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
tab = folderLevelSettingsSelectedTab[folder?.uid];
|
||||
}
|
||||
|
||||
const folderRoot = folder?.root;
|
||||
const folderRoot = folder?.draft || folder?.root;
|
||||
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
|
||||
const hasTests = folderRoot?.request?.tests;
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ const Auth = ({ item, collection }) => {
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
|
||||
@@ -6,9 +6,9 @@ import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/colle
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import { IconWand } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
|
||||
const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -19,8 +19,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const onPrettify = () => {
|
||||
if (!variables) return;
|
||||
try {
|
||||
const edits = format(variables, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyVariables = applyEdits(variables, edits);
|
||||
const prettyVariables = prettifyJsonString(variables);
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
variables: prettyVariables,
|
||||
|
||||
@@ -12,9 +12,9 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { IconSend, IconRefresh, IconWand, IconPlus, IconTrash, IconChevronDown, IconChevronUp } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import toast from 'react-hot-toast'
|
||||
import { getAbsoluteFilePath } from 'utils/common/path';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
|
||||
const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCollapsed, onToggleCollapse, handleRun, canClientSendMultipleMessages }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -130,8 +130,7 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
|
||||
|
||||
const onPrettify = () => {
|
||||
try {
|
||||
const edits = format(content, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyBodyJson = applyEdits(content, edits);
|
||||
const prettyBodyJson = prettifyJsonString(content);
|
||||
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
currentMessages[index] = {
|
||||
@@ -187,6 +186,7 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
|
||||
onClick={onSend}
|
||||
disabled={!isConnectionActive}
|
||||
className={`p-1 rounded ${isConnectionActive ? 'hover:bg-zinc-200 dark:hover:bg-zinc-600' : 'opacity-50 cursor-not-allowed'} transition-colors`}
|
||||
data-testid={`grpc-send-message-${index}`}
|
||||
>
|
||||
<IconSend
|
||||
size={16}
|
||||
@@ -300,6 +300,7 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
id="grpc-messages-container"
|
||||
data-testid="grpc-messages-container"
|
||||
className={`flex-1 ${body.grpc.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'overflow-y-auto'} ${canClientSendMultipleMessages && 'pb-16'}`}
|
||||
>
|
||||
{body.grpc
|
||||
@@ -326,6 +327,7 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="add-message-btn flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors shadow-md"
|
||||
data-testid="grpc-add-message-button"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-sm text-neutral-700 dark:text-neutral-300">Add Message</span>
|
||||
|
||||
@@ -73,7 +73,7 @@ const MethodDropdown = ({
|
||||
|
||||
const MethodsDropdownIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center ml-2 cursor-pointer select-none">
|
||||
<div ref={ref} className="flex items-center justify-center ml-2 cursor-pointer select-none" data-testid="grpc-method-dropdown-trigger">
|
||||
{selectedGrpcMethod && <div className="mr-2">{getIconForMethodType(selectedGrpcMethod.type)}</div>}
|
||||
<span className="text-xs">
|
||||
{selectedGrpcMethod ? (
|
||||
|
||||
@@ -39,7 +39,7 @@ const ProtoFileDropdown = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { success: addSuccess, relativePath, alreadyExists, error: addError } = await protoFileManagement.addProtoFileToCollection(filePath);
|
||||
const { success: addSuccess, relativePath, alreadyExists, error: addError } = await protoFileManagement.addProtoFileFromRequest(filePath);
|
||||
if (!addSuccess) {
|
||||
if (addError) {
|
||||
toast.error(`Failed to add proto file: ${addError.message}`);
|
||||
@@ -91,7 +91,7 @@ const ProtoFileDropdown = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { success: addSuccess, error: addError } = await protoFileManagement.addImportPathToCollection(directoryPath);
|
||||
const { success: addSuccess, error: addError } = await protoFileManagement.addImportPathFromRequest(directoryPath);
|
||||
if (!addSuccess) {
|
||||
if (addError) {
|
||||
toast.error(`Failed to add import path: ${addError.message}`);
|
||||
@@ -103,7 +103,7 @@ const ProtoFileDropdown = ({
|
||||
};
|
||||
|
||||
const handleToggleImportPath = async (index) => {
|
||||
const { success, enabled, error } = await protoFileManagement.toggleImportPath(index);
|
||||
const { success, enabled, error } = await protoFileManagement.toggleImportPathFromRequest(index);
|
||||
if (!success) {
|
||||
if (error) {
|
||||
toast.error(`Failed to toggle import path: ${error.message}`);
|
||||
|
||||
@@ -389,19 +389,21 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
|
||||
{isConnectionActive && isStreamingMethod && (
|
||||
<div className="connection-controls relative flex items-center h-full gap-3">
|
||||
<div className="infotip" onClick={handleCancelConnection}>
|
||||
<div className="infotip" onClick={handleCancelConnection} data-testid="grpc-cancel-connection-button">
|
||||
<IconX color={theme.requestTabs.icon.color} strokeWidth={1.5} size={22} className="cursor-pointer" />
|
||||
<span className="infotip-text text-xs">Cancel</span>
|
||||
</div>
|
||||
|
||||
{isClientStreamingMethod && <div onClick={handleEndConnection}>
|
||||
<IconCheck
|
||||
color={theme.colors.text.green}
|
||||
strokeWidth={2}
|
||||
size={22}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>}
|
||||
{isClientStreamingMethod && (
|
||||
<div onClick={handleEndConnection} data-testid="grpc-end-connection-button">
|
||||
<IconCheck
|
||||
color={theme.colors.text.green}
|
||||
strokeWidth={2}
|
||||
size={22}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ const GrpcAuth = ({ item, collection }) => {
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { cancelRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconDeviceFloppy, IconArrowRight, IconCode } from '@tabler/icons';
|
||||
import { IconDeviceFloppy, IconArrowRight, IconCode, IconSquareRoundedX } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
import { hasRequestChanges } from 'utils/collections';
|
||||
@@ -22,6 +22,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';
|
||||
const editorRef = useRef(null);
|
||||
const isGrpc = item.type === 'grpc-request';
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||
@@ -40,9 +41,9 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
if (!editorRef.current?.editor) return;
|
||||
const editor = editorRef.current.editor;
|
||||
const cursor = editor.getCursor();
|
||||
|
||||
|
||||
const finalUrl = value?.trim() ?? value;
|
||||
|
||||
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
itemUid: item.uid,
|
||||
@@ -50,7 +51,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
url: finalUrl
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
// Restore cursor position only if URL was trimmed
|
||||
if (finalUrl !== value) {
|
||||
setTimeout(() => {
|
||||
@@ -80,6 +81,12 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelRequest = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<div className="flex flex-1 items-center h-full method-selector-container">
|
||||
@@ -87,7 +94,6 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<div className="flex items-center justify-center h-full w-16">
|
||||
<span className="text-xs text-indigo-500 font-bold">gRPC</span>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
)}
|
||||
@@ -120,15 +126,8 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
handleGenerateCode(e);
|
||||
}}
|
||||
>
|
||||
<IconCode
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={'cursor-pointer'}
|
||||
/>
|
||||
<span className="infotiptext text-xs">
|
||||
Generate Code
|
||||
</span>
|
||||
<IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={22} className="cursor-pointer" />
|
||||
<span className="infotiptext text-xs">Generate Code</span>
|
||||
</div>
|
||||
<div
|
||||
title="Save Request"
|
||||
@@ -149,11 +148,30 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} data-testid="send-arrow-icon" />
|
||||
{isLoading || item.response?.stream?.running ? (
|
||||
<IconSquareRoundedX
|
||||
color={theme.requestTabPanel.url.iconDanger}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
data-testid="cancel-request-icon"
|
||||
onClick={handleCancelRequest}
|
||||
/>
|
||||
) : (
|
||||
<IconArrowRight
|
||||
color={theme.requestTabPanel.url.icon}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
data-testid="send-arrow-icon"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collectionUid={collection.uid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
<GenerateCodeItem
|
||||
collectionUid={collection.uid}
|
||||
item={item}
|
||||
onClose={() => setGenerateCodeItemModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
|
||||
const Script = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [activeTab, setActiveTab] = useState('pre-request');
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
|
||||
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
// Refresh CodeMirror when tab becomes visible
|
||||
useEffect(() => {
|
||||
// Small delay to ensure DOM is updated
|
||||
const timer = setTimeout(() => {
|
||||
if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {
|
||||
preRequestEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
|
||||
postResponseEditorRef.current.editor.refresh();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeTab]);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
@@ -39,38 +56,46 @@ const Script = ({ item, collection }) => {
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
<div className="w-full h-full flex flex-col pt-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pre-request">Pre Request</TabsTrigger>
|
||||
<TabsTrigger value="post-response">Post Response</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2" dataTestId="pre-request-script-editor">
|
||||
<CodeEditor
|
||||
ref={preRequestEditorRef}
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="post-response" className="mt-2" dataTestId="post-response-script-editor">
|
||||
<CodeEditor
|
||||
ref={postResponseEditorRef}
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ const WSAuth = ({ item, collection }) => {
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
|
||||
@@ -10,7 +10,7 @@ import GrpcResponsePane from 'components/ResponsePane/GrpcResponsePane';
|
||||
import Welcome from 'components/Welcome';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { cancelRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl/index';
|
||||
import GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index';
|
||||
@@ -263,11 +263,17 @@ const RequestTabPanel = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
})
|
||||
);
|
||||
if (item.response?.stream?.running) {
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
}));
|
||||
} else if (item.requestState !== 'sending' && item.requestState !== 'queued') {
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: reaper, improve selection of panes
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ConfirmCollectionClose = ({ collection, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
handleCancel={onCancel}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
You have unsaved changes in <span className="font-semibold">{collection.name}</span> collection settings.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmCollectionClose;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ConfirmFolderClose = ({ folder, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
handleCancel={onCancel}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
You have unsaved changes in <span className="font-semibold">{folder.name}</span> folder settings.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmFolderClose;
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import DraftTabIcon from './DraftTabIcon';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => {
|
||||
const getTabInfo = (type, tabName) => {
|
||||
switch (type) {
|
||||
case 'collection-settings': {
|
||||
@@ -60,7 +61,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type, tabName)}</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<CloseTabIcon />
|
||||
{hasDraft ? <DraftTabIcon /> : <CloseTabIcon />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useCallback, useState, useRef, Fragment, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, saveCollectionRoot, saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import darkTheme from 'themes/dark';
|
||||
import lightTheme from 'themes/light';
|
||||
import { findItemInCollection, hasRequestChanges } from 'utils/collections';
|
||||
import ConfirmRequestClose from './ConfirmRequestClose';
|
||||
import ConfirmCollectionClose from './ConfirmCollectionClose';
|
||||
import ConfirmFolderClose from './ConfirmFolderClose';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -26,6 +28,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
const { storedTheme } = useTheme();
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);
|
||||
const [showConfirmFolderClose, setShowConfirmFolderClose] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
@@ -77,17 +81,97 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
return colorMap[method.toLocaleLowerCase()];
|
||||
};
|
||||
|
||||
const handleCloseCollectionSettings = (event) => {
|
||||
if (!collection.draft) {
|
||||
return handleCloseClick(event);
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setShowConfirmCollectionClose(true);
|
||||
};
|
||||
|
||||
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
|
||||
|
||||
const handleCloseFolderSettings = (event) => {
|
||||
if (!folder?.draft) {
|
||||
return handleCloseClick(event);
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setShowConfirmFolderClose(true);
|
||||
};
|
||||
|
||||
const hasDraft = tab.type === 'collection-settings' && collection?.draft;
|
||||
const hasFolderDraft = tab.type === 'folder-settings' && folder?.draft;
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
|
||||
onMouseUp={handleMouseUp} // Add middle-click behavior here
|
||||
>
|
||||
{showConfirmCollectionClose && tab.type === 'collection-settings' && (
|
||||
<ConfirmCollectionClose
|
||||
collection={collection}
|
||||
onCancel={() => setShowConfirmCollectionClose(false)}
|
||||
onCloseWithoutSave={() => {
|
||||
dispatch(deleteCollectionDraft({
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
dispatch(closeTabs({
|
||||
tabUids: [tab.uid]
|
||||
}));
|
||||
setShowConfirmCollectionClose(false);
|
||||
}}
|
||||
onSaveAndClose={() => {
|
||||
dispatch(saveCollectionRoot(collection.uid))
|
||||
.then(() => {
|
||||
dispatch(closeTabs({
|
||||
tabUids: [tab.uid]
|
||||
}));
|
||||
setShowConfirmCollectionClose(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showConfirmFolderClose && tab.type === 'folder-settings' && (
|
||||
<ConfirmFolderClose
|
||||
folder={folder}
|
||||
onCancel={() => setShowConfirmFolderClose(false)}
|
||||
onCloseWithoutSave={() => {
|
||||
dispatch(deleteFolderDraft({
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
}));
|
||||
dispatch(closeTabs({
|
||||
tabUids: [tab.uid]
|
||||
}));
|
||||
setShowConfirmFolderClose(false);
|
||||
}}
|
||||
onSaveAndClose={() => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid))
|
||||
.then(() => {
|
||||
dispatch(closeTabs({
|
||||
tabUids: [tab.uid]
|
||||
}));
|
||||
setShowConfirmFolderClose(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{tab.type === 'folder-settings' && !folder ? (
|
||||
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||
) : tab.type === 'folder-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
|
||||
<SpecialTab handleCloseClick={handleCloseFolderSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} hasDraft={hasFolderDraft} />
|
||||
) : tab.type === 'collection-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseCollectionSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={collection?.name} hasDraft={hasDraft} />
|
||||
) : (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
|
||||
)}
|
||||
|
||||
@@ -61,13 +61,13 @@ const GrpcQueryResult = ({ item, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full relative flex flex-col mt-2">
|
||||
<StyledWrapper className="w-full h-full relative flex flex-col mt-2" data-testid="grpc-response-content">
|
||||
{hasError && showErrorMessage && <GrpcError error={errorMessage} onClose={() => setShowErrorMessage(false)} />}
|
||||
{hasResponses && (
|
||||
<div className={`overflow-y-auto ${responsesList.length === 1 ? 'flex-1' : ''}`}>
|
||||
<div className={`overflow-y-auto ${responsesList.length === 1 ? 'flex-1' : ''}`} data-testid="grpc-responses-container">
|
||||
{responsesList.length === 1 ? (
|
||||
// Single message - render directly without accordion
|
||||
<div className="h-full">
|
||||
<div className="h-full" data-testid="grpc-single-response">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
@@ -80,13 +80,13 @@ const GrpcQueryResult = ({ item, collection }) => {
|
||||
</div>
|
||||
) : (
|
||||
// Multiple messages - use accordion
|
||||
<Accordion defaultIndex={0}>
|
||||
<Accordion defaultIndex={0} dataTestId="grpc-responses-accordion">
|
||||
{reversedResponsesList.map((response, index) => {
|
||||
// Calculate the original response number (for display purposes)
|
||||
const originalIndex = responsesList.length - index - 1;
|
||||
|
||||
return (
|
||||
<Accordion.Item key={originalIndex} index={index}>
|
||||
<Accordion.Item key={originalIndex} index={index} data-testid={`grpc-response-item-${originalIndex}`}>
|
||||
<Accordion.Header index={index} style={{ padding: '8px 12px', minHeight: '40px' }}>
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="font-medium">
|
||||
|
||||
@@ -108,7 +108,7 @@ const GrpcResponsePane = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
|
||||
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist" data-testid="grpc-response-tabs">
|
||||
{tabConfig.map((tab) => (
|
||||
<Tab
|
||||
key={tab.name}
|
||||
|
||||
@@ -12,12 +12,25 @@ import { getInitialExampleName } from 'utils/collections/index';
|
||||
import classnames from 'classnames';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getTitleText = ({ isResponseTooLarge, isStreamingResponse }) => {
|
||||
if (isStreamingResponse) {
|
||||
return 'Response Examples aren\'t supported in streaming responses yet.';
|
||||
}
|
||||
|
||||
if (isResponseTooLarge) {
|
||||
return 'Response size exceeds 5MB limit. Cannot save as example.';
|
||||
}
|
||||
|
||||
return 'Save current response as example';
|
||||
};
|
||||
|
||||
const ResponseBookmark = ({ item, collection, responseSize }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showSaveResponseExampleModal, setShowSaveResponseExampleModal] = useState(false);
|
||||
const response = item.response || {};
|
||||
|
||||
const isResponseTooLarge = responseSize >= 5 * 1024 * 1024; // 5 MB
|
||||
const isStreamingResponse = response.stream;
|
||||
|
||||
// Only show for HTTP requests
|
||||
if (item.type !== 'http-request') {
|
||||
@@ -96,19 +109,22 @@ const ResponseBookmark = ({ item, collection, responseSize }) => {
|
||||
toast.success(`Example "${name}" created successfully`);
|
||||
};
|
||||
|
||||
const disabledMessage = getTitleText({
|
||||
isResponseTooLarge,
|
||||
isStreamingResponse
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button
|
||||
onClick={handleSaveClick}
|
||||
disabled={isResponseTooLarge}
|
||||
disabled={isResponseTooLarge || isStreamingResponse}
|
||||
title={
|
||||
isResponseTooLarge
|
||||
? 'Response size exceeds 5MB limit. Cannot save as example.'
|
||||
: 'Save current response as example'
|
||||
disabledMessage
|
||||
}
|
||||
className={classnames('p-1', {
|
||||
'opacity-50 cursor-not-allowed': isResponseTooLarge
|
||||
'opacity-50 cursor-not-allowed': isResponseTooLarge || isStreamingResponse
|
||||
})}
|
||||
data-testid="response-bookmark-btn"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponseStopWatch = ({ startMillis }) => {
|
||||
const [milliseconds, setMilliseconds] = useState(startMillis);
|
||||
|
||||
const tickInterval = 100;
|
||||
const tick = () => {
|
||||
setMilliseconds((_milliseconds) => _milliseconds + tickInterval);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timerID = setInterval(() => {
|
||||
tick();
|
||||
}, tickInterval);
|
||||
return () => {
|
||||
clearInterval(timerID);
|
||||
};
|
||||
}, []);
|
||||
|
||||
let seconds = milliseconds / 1000;
|
||||
let secondsFormatted = `${seconds.toFixed(1)}s`;
|
||||
let width = secondsFormatted.length * 0.4; // Calculate width manually to stop parent layout from "flickering" by changing width too fast
|
||||
return <StyledWrapper className="ml-4" style={{ width: `${width}rem` }}>{secondsFormatted}</StyledWrapper>;
|
||||
};
|
||||
|
||||
export default React.memo(ResponseStopWatch);
|
||||
@@ -4,7 +4,7 @@ import statusCodePhraseMap from './get-status-code-phrase';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
// Todo: text-error class is not getting pulled in for 500 errors
|
||||
const StatusCode = ({ status, statusText }) => {
|
||||
const StatusCode = ({ status, statusText, isStreaming }) => {
|
||||
const getTabClassname = (status) => {
|
||||
return classnames('ml-2', {
|
||||
'text-ok': status >= 100 && status < 200,
|
||||
@@ -17,7 +17,7 @@ const StatusCode = ({ status, statusText }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`response-status-code ${getTabClassname(status)}`} data-testid="response-status-code">
|
||||
{status} {statusText || statusCodePhraseMap[status]}
|
||||
{status} {statusText || statusCodePhraseMap[status]} {isStreaming ? ' - STREAMING' : null}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,8 @@ const getEffectiveAuthSource = (collection, item) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
uid: collection.uid,
|
||||
|
||||
@@ -195,7 +195,7 @@ const WSMessagesList = ({ order = -1, messages = [] }) => {
|
||||
<StyledWrapper className="ws-messages-list mt-1 flex flex-col">
|
||||
{ordered.map((msg, idx, src) => {
|
||||
const inFocus = order === -1 ? src.length - 1 === idx : idx === 0;
|
||||
return <WSMessageItem inFocus={inFocus} id={idx} message={msg} />;
|
||||
return <WSMessageItem key={msg.timestamp} inFocus={inFocus} id={idx} message={msg} />;
|
||||
})}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
import ResponseLayoutToggle from './ResponseLayoutToggle';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch';
|
||||
import WSMessagesList from './WsResponsePane/WSMessagesList';
|
||||
|
||||
const ResponsePane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -71,6 +73,10 @@ const ResponsePane = ({ item, collection }) => {
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
const isStream = item.response?.stream ?? false;
|
||||
if (isStream) {
|
||||
return <WSMessagesList order={-1} messages={item.response.data} />;
|
||||
}
|
||||
return (
|
||||
<QueryResult
|
||||
item={item}
|
||||
@@ -184,8 +190,10 @@ const ResponsePane = ({ item, collection }) => {
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<ResponseBookmark item={item} collection={collection} responseSize={responseSize} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
<StatusCode status={response.status} isStreaming={item.response?.stream?.running} />
|
||||
{item.response?.stream?.running
|
||||
? <ResponseStopWatch startMillis={response.duration} />
|
||||
: <ResponseTime duration={response.duration} />}
|
||||
<ResponseSize size={responseSize} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -3,16 +3,13 @@ import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons';
|
||||
import { resetCollectionRunner, updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection, areItemsLoading, getRequestItemsForCollectionRun } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconExternalLink } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import RunnerTags from './RunnerTags/index';
|
||||
import RunConfigurationPanel from './RunConfigurationPanel';
|
||||
import { getRequestItemsForCollectionRun } from 'utils/collections/index';
|
||||
import { updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
@@ -42,49 +39,61 @@ const anyTestFailed = (item) => {
|
||||
item.postResponseTestStatus === 'fail';
|
||||
};
|
||||
|
||||
// === Centralized filters definition ===
|
||||
const FILTERS = {
|
||||
all: {
|
||||
label: 'All',
|
||||
predicate: () => true,
|
||||
resultFilter: (results) => results
|
||||
},
|
||||
passed: {
|
||||
label: 'Passed',
|
||||
predicate: (item) => allTestsPassed(item),
|
||||
resultFilter: (results) => results?.filter((r) => r.status === 'pass')
|
||||
},
|
||||
failed: {
|
||||
label: 'Failed',
|
||||
predicate: (item) => anyTestFailed(item),
|
||||
resultFilter: (results) => results?.filter((r) => ['fail', 'error'].includes(r.status))
|
||||
},
|
||||
skipped: {
|
||||
label: 'Skipped',
|
||||
predicate: (item) => item.status === 'skipped',
|
||||
resultFilter: (results) => results
|
||||
}
|
||||
};
|
||||
|
||||
// === Reusable filter button ===
|
||||
const FilterButton = ({ label, count, active, onClick }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`font-medium transition-colors cursor-pointer flex items-center gap-1.5 border-b-2 pb-2 ${
|
||||
active
|
||||
? 'text-[#343434] dark:text-[#CCCCCC] border-[#F59E0B]'
|
||||
: 'text-[#989898] dark:text-[#CCCCCC80] border-transparent'
|
||||
}`}
|
||||
style={{ fontFamily: 'Inter', fontSize: '14px', fontWeight: 500, lineHeight: '100%', letterSpacing: '0%' }}
|
||||
>
|
||||
{label}
|
||||
<span
|
||||
className="px-[4.5px] py-[2px] rounded-[2px] bg-[#F7F7F7] dark:bg-[#242424] border border-[#EFEFEF] dark:border-[#92929233] text-[#989898] dark:text-inherit"
|
||||
style={{ borderWidth: '1px', fontFamily: 'Inter', fontSize: '10px', fontWeight: 500, lineHeight: '100%', letterSpacing: '0%' }}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
const [activeFilter, setActiveFilter] = useState('all');
|
||||
const [selectedRequestItems, setSelectedRequestItems] = useState([]);
|
||||
const [configureMode, setConfigureMode] = useState(false);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
|
||||
const autoScrollRunnerBody = () => {
|
||||
if (runnerBodyRef?.current) {
|
||||
// mimics the native terminal scroll style
|
||||
runnerBodyRef.current.scrollTo(0, 100000);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!collection.runnerResult) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
autoScrollRunnerBody();
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
if (runnerInfo.status === 'running') {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [collection.runnerResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration) {
|
||||
if (savedConfiguration.selectedRequestItems && configureMode) {
|
||||
setSelectedRequestItems(savedConfiguration.selectedRequestItems);
|
||||
}
|
||||
if (savedConfiguration.delay !== undefined && delay === null) {
|
||||
setDelay(savedConfiguration.delay);
|
||||
}
|
||||
}
|
||||
}, [collection.runnerConfiguration, configureMode, delay]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
|
||||
@@ -126,6 +135,63 @@ export default function RunnerResults({ collection }) {
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const activeFilterConfig = FILTERS[activeFilter];
|
||||
const filteredItems = items.filter(activeFilterConfig.predicate);
|
||||
|
||||
const filterTestResults = (results) => {
|
||||
if (!results || !Array.isArray(results)) return [];
|
||||
return activeFilterConfig.resultFilter(results);
|
||||
};
|
||||
|
||||
const autoScrollRunnerBody = () => {
|
||||
if (runnerBodyRef?.current) {
|
||||
const element = runnerBodyRef.current;
|
||||
const scrollThreshold = 100; // pixels from bottom to consider "at bottom"
|
||||
const isNearBottom
|
||||
= element.scrollHeight - element.scrollTop - element.clientHeight < scrollThreshold;
|
||||
|
||||
// Only auto-scroll if user is already near the bottom
|
||||
if (isNearBottom) {
|
||||
// mimics the native terminal scroll style
|
||||
element.scrollTo(0, 100000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!collection.runnerResult) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
autoScrollRunnerBody();
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-scroll when items are added or updated during execution
|
||||
// Only scrolls if user is already at/near the bottom
|
||||
if (filteredItems.length > 0) {
|
||||
autoScrollRunnerBody();
|
||||
}
|
||||
}, [filteredItems]);
|
||||
|
||||
useEffect(() => {
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
if (runnerInfo.status === 'running') {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [collection.runnerResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration) {
|
||||
if (savedConfiguration.selectedRequestItems && configureMode) {
|
||||
setSelectedRequestItems(savedConfiguration.selectedRequestItems);
|
||||
}
|
||||
if (savedConfiguration.delay !== undefined && delay === null) {
|
||||
setDelay(savedConfiguration.delay);
|
||||
}
|
||||
}
|
||||
}, [collection.runnerConfiguration, configureMode, delay]);
|
||||
|
||||
const ensureCollectionIsMounted = () => {
|
||||
if(collection.mountStatus === 'mounted'){
|
||||
return;
|
||||
@@ -192,14 +258,14 @@ export default function RunnerResults({ collection }) {
|
||||
}, [tagsEnabled]);
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter(allTestsPassed);
|
||||
const failedRequests = items.filter(anyTestFailed);
|
||||
const filterCounts = {
|
||||
all: items.length,
|
||||
passed: items.filter(allTestsPassed).length,
|
||||
failed: items.filter(anyTestFailed).length,
|
||||
skipped: items.filter((i) => i.status === 'skipped').length
|
||||
};
|
||||
|
||||
const skippedRequests = items.filter((item) => {
|
||||
return item.status === 'skipped';
|
||||
});
|
||||
let isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
if (!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className="pl-4 overflow-hidden h-full">
|
||||
@@ -285,27 +351,57 @@ export default function RunnerResults({ collection }) {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative overflow-auto">
|
||||
<div className="flex items-center my-6 flex-row">
|
||||
<div className="font-medium title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
{/* Filter Bar and Actions */}
|
||||
<div className="flex items-center justify-between mb-4 pt-[14px] gap-4">
|
||||
<div className="flex items-stretch rounded-lg border border-[#EFEFEF] dark:border-[#92929233] max-h-[35px] flex-shrink-0" style={{ borderWidth: '1px' }}>
|
||||
<div className="flex items-center px-3 py-2 rounded-l-lg bg-[#F3F3F3] dark:bg-[#2B2D2F]">
|
||||
<span className="text-gray-600 dark:text-gray-400" style={{ fontFamily: 'Inter', fontSize: '14px', fontWeight: 400 }}>
|
||||
Filter by:
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-5 px-3 pt-2 pb-0 rounded-r-lg bg-transparent dark:bg-transparent">
|
||||
{Object.entries(FILTERS).map(([key, { label }]) => (
|
||||
<FilterButton
|
||||
key={key}
|
||||
label={label}
|
||||
count={filterCounts[key]}
|
||||
active={activeFilter === key}
|
||||
onClick={() => setActiveFilter(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && (
|
||||
<button className="btn btn-sm btn-danger" onClick={cancelExecution}>
|
||||
Cancel Execution
|
||||
</button>
|
||||
)}
|
||||
|
||||
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid ? (
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<button className="btn btn-sm btn-danger" onClick={cancelExecution}>Cancel Execution</button>
|
||||
</div>
|
||||
) : runnerInfo.status === 'ended' ? (
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1.5 rounded-md bg-transparent border border-[#989898] dark:border-[#444444] text-[#989898] hover:opacity-80 transition-colors"
|
||||
style={{ fontFamily: 'Inter', fontSize: '12px', fontWeight: 500 }}
|
||||
onClick={runAgain}
|
||||
>
|
||||
Run Again
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1.5 rounded-md bg-transparent border border-[#989898] dark:border-[#444444] text-[#989898] hover:opacity-80 transition-colors"
|
||||
style={{ fontFamily: 'Inter', fontSize: '12px', fontWeight: 500 }}
|
||||
onClick={resetRunner}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 h-[calc(100vh_-_10rem)] overflow-hidden">
|
||||
<div
|
||||
className={`flex flex-col overflow-y-auto ${selectedItem || (configureMode && !selectedItem && !runnerInfo.status === 'running') ? 'w-1/2' : 'w-full'}`}
|
||||
ref={runnerBodyRef}
|
||||
className="flex flex-col w-1/2"
|
||||
>
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '}
|
||||
{skippedRequests.length}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && (
|
||||
<div className="pb-2 text-xs flex flex-row gap-1">
|
||||
Tags:
|
||||
@@ -326,8 +422,8 @@ export default function RunnerResults({ collection }) {
|
||||
: null}
|
||||
|
||||
{/* Items list */}
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{items.map((item) => {
|
||||
<div className="overflow-y-auto flex-1 " ref={runnerBodyRef}>
|
||||
{filteredItems.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
@@ -371,7 +467,7 @@ export default function RunnerResults({ collection }) {
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
? filterTestResults(item.preRequestTestResults).map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -391,7 +487,7 @@ export default function RunnerResults({ collection }) {
|
||||
))
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
? filterTestResults(item.postResponseTestResults).map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -411,7 +507,7 @@ export default function RunnerResults({ collection }) {
|
||||
))
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
? filterTestResults(item.testResults).map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -430,7 +526,7 @@ export default function RunnerResults({ collection }) {
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
{filterTestResults(item.assertionResults).map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -454,42 +550,50 @@ export default function RunnerResults({ collection }) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
Run Again
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6 ml-3" disabled={shouldDisableCollectionRun} onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
<button className="btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{selectedItem ? (
|
||||
<div className="flex flex-1 w-[50%] overflow-y-auto">
|
||||
<div className="flex flex-col w-full overflow-hidden">
|
||||
<div className="flex items-center mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{allTestsPassed(selectedItem) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
<div className="flex items-center justify-between mb-4 font-medium">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{allTestsPassed(selectedItem)
|
||||
? <IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(selectedItem)
|
||||
? <IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped'
|
||||
? <IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors cursor-pointer flex items-center justify-center"
|
||||
title="Close"
|
||||
aria-label="Close response view"
|
||||
>
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="flex flex-1 w-[50%] overflow-y-auto">
|
||||
<div className="flex flex-col w-full h-full items-center justify-center text-center">
|
||||
<div className="mb-4 text-gray-400 dark:text-gray-500">
|
||||
<IconExternalLink size={64} strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Click on the status code to view the response
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -21,13 +21,14 @@ export const resolveInheritedAuth = (item, collection) => {
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
break;
|
||||
|
||||
@@ -2,13 +2,14 @@ import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { interpolateHeaders, interpolateBody } from './interpolation';
|
||||
import { get } from 'lodash';
|
||||
|
||||
// Merge headers from collection, folders, and request
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
|
||||
// Add collection headers first
|
||||
const collectionHeaders = collection?.root?.request?.headers || [];
|
||||
const collectionHeaders = collection?.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
@@ -19,7 +20,7 @@ const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderHeaders = i?.root?.request?.headers || [];
|
||||
const folderHeaders = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'root.request.headers', []);
|
||||
folderHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
@@ -56,7 +57,7 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
|
||||
// Add auth headers if needed
|
||||
if (request.auth && request.auth.mode !== 'none') {
|
||||
const collectionAuth = collection?.root?.request?.auth || null;
|
||||
const collectionAuth = collection?.draft?.root ? get(collection, 'draft.root.request.auth', null) : get(collection, 'root.request.auth', null);
|
||||
const authHeaders = getAuthHeaders(collectionAuth, request.auth);
|
||||
headers = [...headers, ...authHeaders];
|
||||
}
|
||||
|
||||
@@ -3,13 +3,11 @@ import { IconFileImport } from '@tabler/icons';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import jsyaml from 'js-yaml';
|
||||
import { postmanToBruno, isPostmanCollection } from 'utils/importers/postman-collection';
|
||||
import { convertInsomniaToBruno, isInsomniaCollection } from 'utils/importers/insomnia-collection';
|
||||
import { convertOpenapiToBruno, isOpenApiSpec } from 'utils/importers/openapi-collection';
|
||||
import { isPostmanCollection } from 'utils/importers/postman-collection';
|
||||
import { isInsomniaCollection } from 'utils/importers/insomnia-collection';
|
||||
import { isOpenApiSpec } from 'utils/importers/openapi-collection';
|
||||
import { isWSDLCollection } from 'utils/importers/wsdl-collection';
|
||||
import { processBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import { wsdlToBruno } from '@usebruno/converters';
|
||||
import ImportSettings from 'components/Sidebar/ImportSettings';
|
||||
import { isBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import FullscreenLoader from './FullscreenLoader/index';
|
||||
|
||||
const convertFileToObject = async (file) => {
|
||||
@@ -38,9 +36,6 @@ const convertFileToObject = async (file) => {
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [showImportSettings, setShowImportSettings] = useState(false);
|
||||
const [openApiData, setOpenApiData] = useState(null);
|
||||
const [groupingType, setGroupingType] = useState('tags');
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleDrag = (e) => {
|
||||
@@ -58,16 +53,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSettings = () => {
|
||||
try {
|
||||
const collection = convertOpenapiToBruno(openApiData, { groupBy: groupingType });
|
||||
handleSubmit({ collection });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toastError(err, 'Failed to process OpenAPI specification');
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = async (file) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -77,26 +62,23 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
throw new Error('Failed to parse file content');
|
||||
}
|
||||
|
||||
// Check if it's an OpenAPI spec and show settings
|
||||
let type = null;
|
||||
|
||||
if (isOpenApiSpec(data)) {
|
||||
setOpenApiData(data);
|
||||
setIsLoading(false);
|
||||
setShowImportSettings(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let collection;
|
||||
if (isWSDLCollection(data)) {
|
||||
collection = await wsdlToBruno(data);
|
||||
type = 'openapi';
|
||||
} else if (isWSDLCollection(data)) {
|
||||
type = 'wsdl';
|
||||
} else if (isPostmanCollection(data)) {
|
||||
collection = await postmanToBruno(data);
|
||||
type = 'postman';
|
||||
} else if (isInsomniaCollection(data)) {
|
||||
collection = convertInsomniaToBruno(data);
|
||||
type = 'insomnia';
|
||||
} else if (isBrunoCollection(data)) {
|
||||
type = 'bruno';
|
||||
} else {
|
||||
collection = await processBrunoCollection(data);
|
||||
throw new Error('Unsupported collection format');
|
||||
}
|
||||
|
||||
handleSubmit({ collection });
|
||||
handleSubmit({ rawData: data, type });
|
||||
} catch (err) {
|
||||
toastError(err, 'Import collection failed');
|
||||
} finally {
|
||||
@@ -140,17 +122,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
'application/xml'
|
||||
];
|
||||
|
||||
if (showImportSettings) {
|
||||
return (
|
||||
<ImportSettings
|
||||
groupingType={groupingType}
|
||||
setGroupingType={setGroupingType}
|
||||
onClose={onClose}
|
||||
onConfirm={handleImportSettings}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleCancel={onClose} dataTestId="import-collection-modal">
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -1,15 +1,94 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useRef, useEffect, useState, forwardRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { postmanToBruno } from 'utils/importers/postman-collection';
|
||||
import { convertInsomniaToBruno } from 'utils/importers/insomnia-collection';
|
||||
import { convertOpenapiToBruno } from 'utils/importers/openapi-collection';
|
||||
import { processBrunoCollection } from 'utils/importers/bruno-collection';
|
||||
import { wsdlToBruno } from '@usebruno/converters';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import Help from 'components/Help';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
// Extract collection name from raw data
|
||||
const getCollectionName = (format, rawData) => {
|
||||
if (!rawData) return 'Collection';
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
||||
switch (format) {
|
||||
case 'openapi':
|
||||
return rawData.info?.title || 'OpenAPI Collection';
|
||||
case 'postman':
|
||||
return rawData.info?.name || rawData.collection?.info?.name || 'Postman Collection';
|
||||
case 'insomnia':
|
||||
// For Insomnia v4 format, name is in the workspace resource
|
||||
if (rawData.resources && Array.isArray(rawData.resources)) {
|
||||
const workspace = rawData.resources.find((r) => r._type === 'workspace');
|
||||
if (workspace?.name) {
|
||||
return workspace.name;
|
||||
}
|
||||
}
|
||||
// Fallback to root name property
|
||||
return rawData.name || 'Insomnia Collection';
|
||||
case 'bruno':
|
||||
return rawData.name || 'Bruno Collection';
|
||||
case 'wsdl':
|
||||
return 'WSDL Collection';
|
||||
default:
|
||||
return 'Collection';
|
||||
}
|
||||
};
|
||||
|
||||
// Convert raw data to Bruno collection format
|
||||
const convertCollection = async (format, rawData, groupingType) => {
|
||||
try {
|
||||
let collection;
|
||||
|
||||
switch (format) {
|
||||
case 'openapi':
|
||||
collection = convertOpenapiToBruno(rawData, { groupBy: groupingType });
|
||||
break;
|
||||
case 'wsdl':
|
||||
collection = await wsdlToBruno(rawData);
|
||||
break;
|
||||
case 'postman':
|
||||
collection = await postmanToBruno(rawData);
|
||||
break;
|
||||
case 'insomnia':
|
||||
collection = convertInsomniaToBruno(rawData);
|
||||
break;
|
||||
case 'bruno':
|
||||
collection = await processBrunoCollection(rawData);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown collection format');
|
||||
}
|
||||
|
||||
return collection;
|
||||
} catch (err) {
|
||||
console.error('Conversion error:', err);
|
||||
toastError(err, 'Failed to convert collection');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const groupingOptions = [
|
||||
{ value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' },
|
||||
{ value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' }
|
||||
];
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const [groupingType, setGroupingType] = useState('tags');
|
||||
const dropdownTippyRef = useRef();
|
||||
const isOpenApi = format === 'openapi';
|
||||
|
||||
const collectionName = getCollectionName(format, rawData);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -22,10 +101,27 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
.max(500, 'must be 500 characters or less')
|
||||
.required('Location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
handleSubmit(values.collectionLocation);
|
||||
onSubmit: async (values) => {
|
||||
const convertedCollection = await convertCollection(format, rawData, groupingType);
|
||||
handleSubmit(convertedCollection, values.collectionLocation);
|
||||
}
|
||||
});
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
};
|
||||
|
||||
const GroupingDropdownIcon = forwardRef((props, ref) => {
|
||||
const selectedOption = groupingOptions.find((option) => option.value === groupingType);
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-between w-full current-group" data-testid="grouping-dropdown">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{selectedOption.label}</div>
|
||||
</div>
|
||||
<IconCaretDown size={16} className="text-gray-400 ml-[0.25rem]" fill="currentColor" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
@@ -48,53 +144,89 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose} dataTestId="import-collection-location-modal">
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<div className="mt-2">{collectionName}</div>
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3 flex items-center">
|
||||
Location
|
||||
<Help>
|
||||
<p>
|
||||
Bruno stores your collections on your computer's filesystem.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Choose the location where you want to store this collection.
|
||||
</p>
|
||||
</Help>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Import Collection"
|
||||
confirmText="Import"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
dataTestId="import-collection-location-modal"
|
||||
>
|
||||
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
onChange={e => {
|
||||
formik.setFieldValue('collectionLocation', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-2">{collectionName}</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="font-semibold mt-4 flex items-center">
|
||||
Location
|
||||
<Help>
|
||||
<p>Bruno stores your collections on your computer's filesystem.</p>
|
||||
<p className="mt-2">Choose the location where you want to store this collection.</p>
|
||||
</Help>
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('collectionLocation', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{isOpenApi && (
|
||||
<div className="mt-4 flex gap-4 items-center">
|
||||
<div>
|
||||
<label htmlFor="groupingType" className="block font-semibold mt-4">
|
||||
Folder arrangement
|
||||
</label>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1 mb-2">
|
||||
Select whether to create folders according to the spec's paths or tags.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<GroupingDropdownIcon />} placement="bottom-start">
|
||||
{groupingOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="dropdown-item"
|
||||
data-testid={option.testId}
|
||||
onClick={() => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
setGroupingType(option.value);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Modal from 'components/Modal';
|
||||
import Portal from 'components/Portal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const groupingOptions = [
|
||||
{ value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' },
|
||||
{ value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' }
|
||||
];
|
||||
|
||||
const ImportSettings = ({
|
||||
groupingType,
|
||||
setGroupingType,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
};
|
||||
|
||||
const GroupingDropdownIcon = forwardRef((props, ref) => {
|
||||
const selectedOption = groupingOptions.find((option) => option.value === groupingType);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex items-center justify-between w-full current-group"
|
||||
data-testid="grouping-dropdown"
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">{selectedOption.label}</div>
|
||||
</div>
|
||||
<IconCaretDown size={16} className="text-gray-400 ml-[0.25rem]" fill="currentColor" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="OpenAPI Import Settings"
|
||||
handleCancel={onClose}
|
||||
handleConfirm={onConfirm}
|
||||
confirmText="Import"
|
||||
dataTestId="import-settings-modal"
|
||||
>
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Folder arrangement</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Select whether to create folders according to the spec's paths or tags.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<GroupingDropdownIcon />} placement="bottom-start">
|
||||
{groupingOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className="dropdown-item"
|
||||
data-testid={option.testId}
|
||||
onClick={() => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
setGroupingType(option.value);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportSettings;
|
||||
@@ -15,24 +15,24 @@ import { multiLineMsg } from "utils/common";
|
||||
import { formatIpcError } from "utils/common/error";
|
||||
|
||||
const TitleBar = () => {
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
const [importData, setImportData] = useState(null);
|
||||
const dispatch = useDispatch();
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
setImportedCollection(collection);
|
||||
const handleImportCollection = ({ rawData, type }) => {
|
||||
setImportData({ rawData, type });
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation))
|
||||
const handleImportCollectionLocation = (convertedCollection, collectionLocation) => {
|
||||
dispatch(importCollection(convertedCollection, collectionLocation))
|
||||
.then(() => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
setImportData(null);
|
||||
toast.success('Collection imported successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -72,9 +72,10 @@ const TitleBar = () => {
|
||||
{importCollectionModalOpen ? (
|
||||
<ImportCollection onClose={() => setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} />
|
||||
) : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
{importCollectionLocationModalOpen && importData ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
rawData={importData.rawData}
|
||||
format={importData.type}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
|
||||
@@ -11,10 +11,11 @@ const Tab = ({ name, label, isActive, onClick, count = 0, className = '', ...pro
|
||||
className={tabClassName}
|
||||
role="tab"
|
||||
onClick={() => onClick(name)}
|
||||
data-testid={`tab-${name}`}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
{count > 0 && <sup className="ml-1 font-medium">{count}</sup>}
|
||||
{count > 0 && <sup className="ml-1 font-medium" data-testid={`tab-${name}-count`}>{count}</sup>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
59
packages/bruno-app/src/components/Tabs/index.js
Normal file
59
packages/bruno-app/src/components/Tabs/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const TabsContext = createContext();
|
||||
|
||||
export const Tabs = ({ value, onValueChange, children, className = '' }) => {
|
||||
return (
|
||||
<TabsContext.Provider value={{ value, onValueChange }}>
|
||||
<div className={`flex flex-col h-full flex-1 ${className}`}>{children}</div>
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const TabsList = ({ children, className = '' }) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inline-flex h-8 w-fit justify-center rounded-md p-0.5 gap-[2px] ${className}`}
|
||||
style={{ background: theme.tabs.secondary.inactive.bg }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TabsTrigger = ({ value: triggerValue, children, className = '' }) => {
|
||||
const { value, onValueChange } = useContext(TabsContext);
|
||||
const { theme } = useTheme();
|
||||
const isActive = value === triggerValue;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onValueChange(triggerValue)}
|
||||
className={`inline-flex items-center justify-center rounded-[4px] p-[8px] text-sm whitespace-nowrap transition-all cursor-pointer border border-transparent hover:opacity-90 ${className}`}
|
||||
style={{
|
||||
background: isActive ? theme.tabs.secondary.active.bg : 'transparent',
|
||||
color: isActive ? theme.tabs.secondary.active.color : theme.tabs.secondary.inactive.color
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const TabsContent = ({ value: contentValue, children, className = '', dataTestId = '' }) => {
|
||||
const { value } = useContext(TabsContext);
|
||||
const isActive = value === contentValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`outline-none flex flex-col h-full flex-1 ${className}`}
|
||||
data-testid={dataTestId}
|
||||
style={{ display: isActive ? 'flex' : 'none' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ const Welcome = () => {
|
||||
const { t } = useTranslation();
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [importData, setImportData] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
@@ -30,17 +30,17 @@ const Welcome = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleImportCollection = ({ collection }) => {
|
||||
setImportedCollection(collection);
|
||||
const handleImportCollection = ({ rawData, type }) => {
|
||||
setImportData({ rawData, type });
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation))
|
||||
const handleImportCollectionLocation = (convertedCollection, collectionLocation) => {
|
||||
dispatch(importCollection(convertedCollection, collectionLocation))
|
||||
.then(() => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
setImportData(null);
|
||||
toast.success(t('WELCOME.COLLECTION_IMPORT_SUCCESS'));
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -56,9 +56,10 @@ const Welcome = () => {
|
||||
{importCollectionModalOpen ? (
|
||||
<ImportCollection onClose={() => setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} />
|
||||
) : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
{importCollectionLocationModalOpen && importData ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
rawData={importData.rawData}
|
||||
format={importData.type}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useState, useRef, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { browseFiles, updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { updateCollectionProtobuf } from 'providers/ReduxStore/slices/collections';
|
||||
import { getRelativePath, getAbsoluteFilePath } from 'utils/common/path';
|
||||
import { browseDirectory } from 'utils/filesystem';
|
||||
import { loadGrpcMethodsFromProtoFile } from 'utils/network/index';
|
||||
import useLocalStorage from 'hooks/useLocalStorage/index';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
|
||||
/**
|
||||
* Custom hook for managing protofile data and collection configuration
|
||||
@@ -18,8 +20,13 @@ export default function useProtoFileManagement(collection) {
|
||||
const [protofileCache, setProtofileCache] = useLocalStorage('bruno.grpc.protofileCache', {});
|
||||
const [isLoadingMethods, setIsLoadingMethods] = useState(false);
|
||||
|
||||
const collectionProtoFiles = useMemo(() => collection?.brunoConfig?.protobuf?.protoFiles || [], [collection?.brunoConfig?.protobuf?.protoFiles]);
|
||||
const collectionImportPaths = useMemo(() => collection?.brunoConfig?.protobuf?.importPaths || [], [collection?.brunoConfig?.protobuf?.importPaths]);
|
||||
// Get protobuf config from draft if exists, otherwise from brunoConfig
|
||||
const protobufConfig = collection?.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.protobuf', {})
|
||||
: get(collection, 'brunoConfig.protobuf', {});
|
||||
|
||||
const collectionProtoFiles = useMemo(() => protobufConfig?.protoFiles || [], [protobufConfig?.protoFiles]);
|
||||
const collectionImportPaths = useMemo(() => protobufConfig?.importPaths || [], [protobufConfig?.importPaths]);
|
||||
|
||||
const protoFilesWithExistence = useMemo(() =>
|
||||
collectionProtoFiles.map((protoFile) => ({
|
||||
@@ -78,6 +85,39 @@ export default function useProtoFileManagement(collection) {
|
||||
return { success: true, relativePath, alreadyExists: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const protoFileObj = {
|
||||
path: relativePath,
|
||||
type: 'file',
|
||||
exists: true
|
||||
};
|
||||
|
||||
const updatedProtobuf = {
|
||||
...protobufConfig,
|
||||
protoFiles: [...collectionProtoFiles, protoFileObj]
|
||||
};
|
||||
|
||||
dispatch(updateCollectionProtobuf({
|
||||
collectionUid: collection.uid,
|
||||
protobuf: updatedProtobuf
|
||||
}));
|
||||
|
||||
return { success: true, relativePath };
|
||||
} catch (error) {
|
||||
console.error('Error adding proto file to collection:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
const addProtoFileFromRequest = async (filePath) => {
|
||||
const relativePath = getRelativePath(collection.pathname, filePath, true);
|
||||
|
||||
const exists = collectionProtoFiles.some((pf) => pf.path === relativePath);
|
||||
|
||||
if (exists) {
|
||||
return { success: true, relativePath, alreadyExists: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const protoFileObj = {
|
||||
path: relativePath,
|
||||
@@ -104,6 +144,38 @@ export default function useProtoFileManagement(collection) {
|
||||
};
|
||||
|
||||
const addImportPathToCollection = async (directoryPath) => {
|
||||
const relativePath = getRelativePath(collection.pathname, directoryPath, true);
|
||||
const importPathObj = {
|
||||
path: relativePath,
|
||||
enabled: true,
|
||||
exists: true
|
||||
};
|
||||
|
||||
const exists = collectionImportPaths.some((ip) => ip.path === importPathObj.path);
|
||||
|
||||
if (exists) {
|
||||
return { success: false, error: new Error('Import path already exists') };
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedProtobuf = {
|
||||
...protobufConfig,
|
||||
importPaths: [...collectionImportPaths, importPathObj]
|
||||
};
|
||||
|
||||
dispatch(updateCollectionProtobuf({
|
||||
collectionUid: collection.uid,
|
||||
protobuf: updatedProtobuf
|
||||
}));
|
||||
|
||||
return { success: true, relativePath };
|
||||
} catch (error) {
|
||||
console.error('Error adding import path:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
const addImportPathFromRequest = async (directoryPath) => {
|
||||
const relativePath = getRelativePath(collection.pathname, directoryPath, true);
|
||||
const importPathObj = {
|
||||
path: relativePath,
|
||||
@@ -137,6 +209,34 @@ export default function useProtoFileManagement(collection) {
|
||||
};
|
||||
|
||||
const toggleImportPath = async (index) => {
|
||||
try {
|
||||
const updatedImportPaths = [...collectionImportPaths];
|
||||
updatedImportPaths[index] = {
|
||||
...updatedImportPaths[index],
|
||||
enabled: !updatedImportPaths[index].enabled
|
||||
};
|
||||
|
||||
const updatedProtobuf = {
|
||||
...protobufConfig,
|
||||
importPaths: updatedImportPaths
|
||||
};
|
||||
|
||||
dispatch(updateCollectionProtobuf({
|
||||
collectionUid: collection.uid,
|
||||
protobuf: updatedProtobuf
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
enabled: updatedImportPaths[index].enabled
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error toggling import path:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
const toggleImportPathFromRequest = async (index) => {
|
||||
try {
|
||||
const updatedImportPaths = [...collectionImportPaths];
|
||||
updatedImportPaths[index] = {
|
||||
@@ -195,13 +295,15 @@ export default function useProtoFileManagement(collection) {
|
||||
const updatedProtoFiles = [...collectionProtoFiles];
|
||||
updatedProtoFiles.splice(index, 1);
|
||||
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
if (!brunoConfig.protobuf) {
|
||||
brunoConfig.protobuf = {};
|
||||
}
|
||||
brunoConfig.protobuf.protoFiles = updatedProtoFiles;
|
||||
const updatedProtobuf = {
|
||||
...protobufConfig,
|
||||
protoFiles: updatedProtoFiles
|
||||
};
|
||||
|
||||
await dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
dispatch(updateCollectionProtobuf({
|
||||
collectionUid: collection.uid,
|
||||
protobuf: updatedProtobuf
|
||||
}));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -215,13 +317,15 @@ export default function useProtoFileManagement(collection) {
|
||||
const updatedImportPaths = [...collectionImportPaths];
|
||||
updatedImportPaths.splice(index, 1);
|
||||
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
if (!brunoConfig.protobuf) {
|
||||
brunoConfig.protobuf = {};
|
||||
}
|
||||
brunoConfig.protobuf.importPaths = updatedImportPaths;
|
||||
const updatedProtobuf = {
|
||||
...protobufConfig,
|
||||
importPaths: updatedImportPaths
|
||||
};
|
||||
|
||||
await dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
dispatch(updateCollectionProtobuf({
|
||||
collectionUid: collection.uid,
|
||||
protobuf: updatedProtobuf
|
||||
}));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -236,16 +340,19 @@ export default function useProtoFileManagement(collection) {
|
||||
const updatedImportPaths = [...collectionImportPaths];
|
||||
updatedImportPaths[index] = {
|
||||
...updatedImportPaths[index],
|
||||
path: relativePath
|
||||
path: relativePath,
|
||||
exists: true
|
||||
};
|
||||
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
if (!brunoConfig.protobuf) {
|
||||
brunoConfig.protobuf = {};
|
||||
}
|
||||
brunoConfig.protobuf.importPaths = updatedImportPaths;
|
||||
const updatedProtobuf = {
|
||||
...protobufConfig,
|
||||
importPaths: updatedImportPaths
|
||||
};
|
||||
|
||||
await dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
dispatch(updateCollectionProtobuf({
|
||||
collectionUid: collection.uid,
|
||||
protobuf: updatedProtobuf
|
||||
}));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -261,16 +368,19 @@ export default function useProtoFileManagement(collection) {
|
||||
updatedProtoFiles[index] = {
|
||||
...updatedProtoFiles[index],
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
type: 'file',
|
||||
exists: true
|
||||
};
|
||||
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
if (!brunoConfig.protobuf) {
|
||||
brunoConfig.protobuf = {};
|
||||
}
|
||||
brunoConfig.protobuf.protoFiles = updatedProtoFiles;
|
||||
const updatedProtobuf = {
|
||||
...protobufConfig,
|
||||
protoFiles: updatedProtoFiles
|
||||
};
|
||||
|
||||
await dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
dispatch(updateCollectionProtobuf({
|
||||
collectionUid: collection.uid,
|
||||
protobuf: updatedProtobuf
|
||||
}));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -286,12 +396,15 @@ export default function useProtoFileManagement(collection) {
|
||||
loadMethodsFromProtoFile,
|
||||
addProtoFileToCollection,
|
||||
addImportPathToCollection,
|
||||
addImportPathFromRequest,
|
||||
toggleImportPath,
|
||||
toggleImportPathFromRequest,
|
||||
browseForProtoFile,
|
||||
browseForImportDirectory,
|
||||
removeProtoFileFromCollection,
|
||||
removeImportPathFromCollection,
|
||||
replaceImportPathInCollection,
|
||||
replaceProtoFileInCollection
|
||||
replaceProtoFileInCollection,
|
||||
addProtoFileFromRequest
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,55 +7,106 @@ import { useDispatch } from 'react-redux';
|
||||
import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges } from 'utils/collections';
|
||||
import { pluralizeWord } from 'utils/common';
|
||||
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
|
||||
import { saveMultipleRequests } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const SaveRequestsModal = ({ onClose }) => {
|
||||
const MAX_UNSAVED_REQUESTS_TO_SHOW = 5;
|
||||
const MAX_UNSAVED_ITEMS_TO_SHOW = 5;
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const currentDrafts = useMemo(() => {
|
||||
const drafts = [];
|
||||
const allDrafts = useMemo(() => {
|
||||
const requestDrafts = [];
|
||||
const collectionDrafts = [];
|
||||
const folderDrafts = [];
|
||||
const tabsByCollection = groupBy(tabs, (t) => t.collectionUid);
|
||||
|
||||
Object.keys(tabsByCollection).forEach((collectionUid) => {
|
||||
const collection = findCollectionByUid(collections, collectionUid);
|
||||
if (collection) {
|
||||
// Check for collection draft
|
||||
if (collection.draft) {
|
||||
collectionDrafts.push({
|
||||
type: 'collection',
|
||||
name: collection.name,
|
||||
collectionUid: collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// Check for request and folder drafts
|
||||
const items = flattenItems(collection.items);
|
||||
const collectionDrafts = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item));
|
||||
each(collectionDrafts, (draft) => {
|
||||
drafts.push({
|
||||
|
||||
// Request drafts
|
||||
const requests = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item));
|
||||
each(requests, (draft) => {
|
||||
requestDrafts.push({
|
||||
type: 'request',
|
||||
...draft,
|
||||
collectionUid: collectionUid
|
||||
});
|
||||
});
|
||||
|
||||
// Folder drafts
|
||||
const folders = filter(items, (item) => item.type === 'folder' && item.draft);
|
||||
each(folders, (folder) => {
|
||||
folderDrafts.push({
|
||||
type: 'folder',
|
||||
name: folder.name,
|
||||
folderUid: folder.uid,
|
||||
collectionUid: collectionUid
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return drafts;
|
||||
return [...collectionDrafts, ...folderDrafts, ...requestDrafts];
|
||||
}, [collections, tabs]);
|
||||
|
||||
const totalDraftsCount = allDrafts.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDrafts.length === 0) {
|
||||
if (totalDraftsCount === 0) {
|
||||
return dispatch(completeQuitFlow());
|
||||
}
|
||||
}, [currentDrafts, dispatch]);
|
||||
}, [totalDraftsCount, dispatch]);
|
||||
|
||||
const closeWithoutSave = () => {
|
||||
dispatch(completeQuitFlow());
|
||||
onClose();
|
||||
};
|
||||
|
||||
const closeWithSave = () => {
|
||||
dispatch(saveMultipleRequests(currentDrafts))
|
||||
.then(() => dispatch(completeQuitFlow()))
|
||||
.then(() => onClose());
|
||||
const closeWithSave = async () => {
|
||||
try {
|
||||
// Separate drafts by type
|
||||
const collectionDrafts = allDrafts.filter((d) => d.type === 'collection');
|
||||
const folderDrafts = allDrafts.filter((d) => d.type === 'folder');
|
||||
const requestDrafts = allDrafts.filter((d) => d.type === 'request');
|
||||
|
||||
// Save all collection drafts
|
||||
if (collectionDrafts.length > 0) {
|
||||
await dispatch(saveMultipleCollections(collectionDrafts));
|
||||
}
|
||||
|
||||
// Save all folder drafts
|
||||
if (folderDrafts.length > 0) {
|
||||
await dispatch(saveMultipleFolders(folderDrafts));
|
||||
}
|
||||
|
||||
// Save all request drafts
|
||||
if (requestDrafts.length > 0) {
|
||||
await dispatch(saveMultipleRequests(requestDrafts));
|
||||
}
|
||||
|
||||
dispatch(completeQuitFlow());
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error saving drafts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentDrafts.length) {
|
||||
if (totalDraftsCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -77,23 +128,30 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
Do you want to save the changes you made to the following{' '}
|
||||
<span className="font-medium">{currentDrafts.length}</span> {pluralizeWord('request', currentDrafts.length)}?
|
||||
<span className="font-medium">{totalDraftsCount}</span> {pluralizeWord('item', totalDraftsCount)}?
|
||||
</p>
|
||||
|
||||
<ul className="mt-4">
|
||||
{currentDrafts.slice(0, MAX_UNSAVED_REQUESTS_TO_SHOW).map((item) => {
|
||||
{allDrafts.slice(0, MAX_UNSAVED_ITEMS_TO_SHOW).map((item, index) => {
|
||||
const prefix
|
||||
= item.type === 'collection'
|
||||
? 'Collection: '
|
||||
: item.type === 'folder'
|
||||
? 'Folder: '
|
||||
: 'Request: ';
|
||||
return (
|
||||
<li key={item.uid} className="mt-1 text-xs">
|
||||
{item.filename}
|
||||
<li key={`${item.type}-${item.collectionUid || item.uid}-${index}`} className="mt-1 text-xs">
|
||||
{prefix}
|
||||
{item.name || item.filename}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{currentDrafts.length > MAX_UNSAVED_REQUESTS_TO_SHOW && (
|
||||
{totalDraftsCount > MAX_UNSAVED_ITEMS_TO_SHOW && (
|
||||
<p className="mt-1 text-xs">
|
||||
...{currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW} additional{' '}
|
||||
{pluralizeWord('request', currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW)} not shown
|
||||
...{totalDraftsCount - MAX_UNSAVED_ITEMS_TO_SHOW} additional{' '}
|
||||
{pluralizeWord('item', totalDraftsCount - MAX_UNSAVED_ITEMS_TO_SHOW)} not shown
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -108,7 +166,7 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={closeWithSave}>
|
||||
{currentDrafts.length > 1 ? 'Save All' : 'Save'}
|
||||
{totalDraftsCount > 1 ? 'Save All' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
collectionUnlinkEnvFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
processEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
runFolderEvent,
|
||||
runRequestEvent,
|
||||
scriptEnvironmentUpdateEvent
|
||||
scriptEnvironmentUpdateEvent,
|
||||
streamDataReceived
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -137,8 +139,8 @@ const useIpcEvents = () => {
|
||||
dispatch(processEnvUpdateEvent(val));
|
||||
});
|
||||
|
||||
const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {
|
||||
console[val.type](...val.args);
|
||||
const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {
|
||||
console[val.type](...val.args);
|
||||
dispatch(addLog({
|
||||
type: val.type,
|
||||
args: val.args,
|
||||
@@ -188,6 +190,14 @@ const useIpcEvents = () => {
|
||||
dispatch(collectionAddOauth2CredentialsByUrl(payload));
|
||||
});
|
||||
|
||||
const removeHttpStreamNewDataListener = ipcRenderer.on('main:http-stream-new-data', (val) => {
|
||||
dispatch(streamDataReceived(val));
|
||||
});
|
||||
|
||||
const removeHttpStreamEndListener = ipcRenderer.on('main:http-stream-end', (val) => {
|
||||
dispatch(requestCancelled(val));
|
||||
});
|
||||
|
||||
const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => {
|
||||
dispatch(updateCollectionLoadingState(val));
|
||||
});
|
||||
@@ -212,6 +222,8 @@ const useIpcEvents = () => {
|
||||
removeGlobalEnvironmentsUpdatesListener();
|
||||
removeSnapshotHydrationListener();
|
||||
removeCollectionOauth2CredentialsUpdatesListener();
|
||||
removeHttpStreamNewDataListener();
|
||||
removeHttpStreamEndListener();
|
||||
removeCollectionLoadingStateListener();
|
||||
removePersistentEnvVariablesUpdateListener();
|
||||
removeSystemResourcesListener();
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
sendRequest,
|
||||
saveRequest,
|
||||
saveCollectionRoot,
|
||||
saveFolderRoot
|
||||
saveFolderRoot,
|
||||
saveCollectionSettings
|
||||
} from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs, reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||
@@ -57,7 +58,7 @@ export const HotkeysProvider = (props) => {
|
||||
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
|
||||
}
|
||||
} else if (activeTab.type === 'collection-settings') {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { handleMakeTabParmanent } from "./utils";
|
||||
|
||||
const actionsToIntercept = [
|
||||
// Request-level actions
|
||||
'collections/requestUrlChanged',
|
||||
'collections/updateAuth',
|
||||
'collections/addQueryParam',
|
||||
@@ -37,14 +38,39 @@ const actionsToIntercept = [
|
||||
'collections/updateVar',
|
||||
'collections/deleteVar',
|
||||
'collections/moveVar',
|
||||
'collections/updateRequestDocs',
|
||||
'collections/runRequestEvent', // TODO: This doesn't necessarily related to a draft state, need to rethink.
|
||||
|
||||
// Folder-level actions
|
||||
'collections/addFolderHeader',
|
||||
'collections/updateFolderHeader',
|
||||
'collections/deleteFolderHeader',
|
||||
'collections/addFolderVar',
|
||||
'collections/updateFolderVar',
|
||||
'collections/deleteFolderVar',
|
||||
'collections/updateRequestDocs',
|
||||
'collections/runRequestEvent', // TODO: This doesn't necessarily related to a draft state, need to rethink.
|
||||
'collections/updateFolderRequestScript',
|
||||
'collections/updateFolderResponseScript',
|
||||
'collections/updateFolderTests',
|
||||
'collections/updateFolderAuth',
|
||||
'collections/updateFolderAuthMode',
|
||||
'collections/updateFolderDocs',
|
||||
|
||||
// Collection-level actions
|
||||
'collections/addCollectionHeader',
|
||||
'collections/updateCollectionHeader',
|
||||
'collections/deleteCollectionHeader',
|
||||
'collections/addCollectionVar',
|
||||
'collections/updateCollectionVar',
|
||||
'collections/deleteCollectionVar',
|
||||
'collections/updateCollectionAuth',
|
||||
'collections/updateCollectionAuthMode',
|
||||
'collections/updateCollectionRequestScript',
|
||||
'collections/updateCollectionResponseScript',
|
||||
'collections/updateCollectionTests',
|
||||
'collections/updateCollectionDocs',
|
||||
'collections/updateCollectionClientCertificates',
|
||||
'collections/updateCollectionProtobuf',
|
||||
'collections/updateCollectionProxy'
|
||||
];
|
||||
|
||||
export const draftDetectMiddleware = ({ dispatch, getState }) => (next) => (action) => {
|
||||
|
||||
@@ -6,14 +6,35 @@ function handleMakeTabParmanent(state, action, dispatch) {
|
||||
const tabs = state.tabs.tabs;
|
||||
const activeTabUid = state.tabs.activeTabUid;
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const itemUid = action.payload.itemUid || action.payload.folderUid
|
||||
const collection = findCollectionByUid(state.collections.collections, action.payload.collectionUid);
|
||||
if (collection) {
|
||||
|
||||
if (!focusedTab || focusedTab.preview !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { itemUid, folderUid, collectionUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle request-level changes
|
||||
if (itemUid) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item && focusedTab.preview == true) {
|
||||
if (item) {
|
||||
dispatch(makeTabPermanent({ uid: itemUid }));
|
||||
}
|
||||
}
|
||||
// Handle folder-level changes (folder settings tab)
|
||||
else if (folderUid) {
|
||||
const folder = findItemInCollection(collection, folderUid);
|
||||
if (folder) {
|
||||
dispatch(makeTabPermanent({ uid: folderUid }));
|
||||
}
|
||||
} else if (collectionUid) {
|
||||
// Handle collection-level changes (collection settings tab)
|
||||
dispatch(makeTabPermanent({ uid: collectionUid }));
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
isItemARequest,
|
||||
transformRequestToSaveToFilesystem
|
||||
transformRequestToSaveToFilesystem,
|
||||
transformCollectionRootToSave
|
||||
} from 'utils/collections';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
import { cancelNetworkRequest, connectWS, sendGrpcRequest, sendNetworkRequest, sendWsRequest } from 'utils/network/index';
|
||||
@@ -43,7 +44,9 @@ import {
|
||||
updateRunnerConfiguration as _updateRunnerConfiguration,
|
||||
updateActiveConnections,
|
||||
saveRequest as _saveRequest,
|
||||
saveEnvironment as _saveEnvironment
|
||||
saveEnvironment as _saveEnvironment,
|
||||
saveCollectionDraft,
|
||||
saveFolderDraft
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -58,7 +61,8 @@ import {
|
||||
getReorderedItemsInTargetDirectory,
|
||||
resetSequencesInFolder,
|
||||
getReorderedItemsInSourceDirectory,
|
||||
calculateDraggedItemNewPathname
|
||||
calculateDraggedItemNewPathname,
|
||||
transformFolderRootToSave
|
||||
} from 'utils/collections/index';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import { buildPersistedEnvVariables } from 'utils/environments';
|
||||
@@ -160,11 +164,18 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
|
||||
// Transform collection root (uses draft if exists)
|
||||
const collectionRootToSave = transformCollectionRootToSave(collectionCopy);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-collection-root', collection.pathname, collection.root)
|
||||
.then(() => toast.success('Collection Settings saved successfully'))
|
||||
.invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave)
|
||||
.then(() => {
|
||||
toast.success('Collection Settings saved successfully');
|
||||
dispatch(saveCollectionDraft({ collectionUid }));
|
||||
})
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save collection settings!');
|
||||
@@ -189,15 +200,107 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
// Use draft if it exists, otherwise use root
|
||||
const folderRootToSave = transformFolderRootToSave(folder);
|
||||
|
||||
const folderData = {
|
||||
name: folder.name,
|
||||
pathname: folder.pathname,
|
||||
root: folder.root
|
||||
root: folderRootToSave
|
||||
};
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
.then(() => toast.success('Folder Settings saved successfully'))
|
||||
.then(() => {
|
||||
toast.success('Folder Settings saved successfully');
|
||||
// If there was a draft, save it to root and clear the draft
|
||||
if (folder.draft) {
|
||||
dispatch(saveFolderDraft({ collectionUid, folderUid }));
|
||||
}
|
||||
})
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save folder settings!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const saveMultipleCollections = (collectionDrafts) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { collections } = state.collections;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const savePromises = [];
|
||||
|
||||
each(collectionDrafts, (collectionDraft) => {
|
||||
const collection = findCollectionByUid(collections, collectionDraft.collectionUid);
|
||||
if (collection) {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const collectionRootToSave = transformCollectionRootToSave(collectionCopy);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
let savePromises = [];
|
||||
|
||||
savePromises.push(ipcRenderer.invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave));
|
||||
|
||||
if (collectionCopy.draft?.brunoConfig) {
|
||||
savePromises.push(ipcRenderer.invoke('renderer:update-bruno-config', collectionCopy.draft.brunoConfig, collectionCopy.pathname, collectionDraft.collectionUid));
|
||||
}
|
||||
|
||||
Promise.all(savePromises)
|
||||
.then(() => {
|
||||
dispatch(saveCollectionDraft({ collectionUid: collectionDraft.collectionUid }));
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save collection settings!');
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all(savePromises)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save collection settings!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const saveMultipleFolders = (folderDrafts) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { collections } = state.collections;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const savePromises = [];
|
||||
|
||||
each(folderDrafts, (folderDraft) => {
|
||||
const collection = findCollectionByUid(collections, folderDraft.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, folderDraft.folderUid) : null;
|
||||
|
||||
if (collection && folder) {
|
||||
const folderRootToSave = transformFolderRootToSave(folder);
|
||||
const folderData = {
|
||||
name: folder.name,
|
||||
pathname: folder.pathname,
|
||||
root: folderRootToSave
|
||||
};
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
const savePromise = ipcRenderer
|
||||
.invoke('renderer:save-folder-root', folderData)
|
||||
.then(() => {
|
||||
if (folder.draft) {
|
||||
dispatch(saveFolderDraft({ collectionUid: folderDraft.collectionUid, folderUid: folderDraft.folderUid }));
|
||||
}
|
||||
});
|
||||
|
||||
savePromises.push(savePromise);
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all(savePromises)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save folder settings!');
|
||||
@@ -1642,6 +1745,45 @@ export const browseFiles = (filters, properties) => (_dispatch, _getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const saveCollectionSettings = (collectionUid, brunoConfig = null) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
|
||||
// Transform collection root (uses draft if exists)
|
||||
const collectionRootToSave = transformCollectionRootToSave(collectionCopy);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const savePromises = [];
|
||||
|
||||
// Save collection.bru file
|
||||
savePromises.push(ipcRenderer.invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave));
|
||||
|
||||
// Save bruno.json if brunoConfig is provided or if there's a brunoConfig draft
|
||||
const brunoConfigToSave = brunoConfig || (collectionCopy.draft && collectionCopy.draft.brunoConfig);
|
||||
if (brunoConfigToSave) {
|
||||
savePromises.push(ipcRenderer.invoke('renderer:update-bruno-config', brunoConfigToSave, collectionCopy.pathname, collectionUid));
|
||||
}
|
||||
|
||||
Promise.all(savePromises)
|
||||
.then(() => {
|
||||
toast.success('Collection Settings saved successfully');
|
||||
dispatch(saveCollectionDraft({ collectionUid }));
|
||||
})
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save collection settings!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { parseQueryParams, buildQueryString as stringifyQueryParams } from '@use
|
||||
import { uuid } from 'utils/common';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { hexy as hexdump } from 'hexy';
|
||||
import {
|
||||
addDepth,
|
||||
areItemsTheSameExceptSeqUpdate,
|
||||
@@ -380,9 +381,17 @@ export const collectionsSlice = createSlice({
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
item.response = null;
|
||||
if (item.response?.stream?.running) {
|
||||
item.response.stream.running = null;
|
||||
|
||||
const startTimestamp = item.requestSent.timestamp;
|
||||
item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration;
|
||||
item.response.data = [{ type: 'info', timestamp: Date.now(), message: 'Connection Closed' }].concat(item.response.data);
|
||||
} else {
|
||||
item.response = null;
|
||||
item.requestUid = null;
|
||||
}
|
||||
item.cancelTokenUid = null;
|
||||
item.requestUid = null;
|
||||
item.requestStartTime = null;
|
||||
}
|
||||
}
|
||||
@@ -395,7 +404,7 @@ export const collectionsSlice = createSlice({
|
||||
if (item) {
|
||||
item.requestState = 'received';
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = null;
|
||||
item.cancelTokenUid = item.response.stream?.running ? item.cancelTokenUid : null;
|
||||
item.requestStartTime = null;
|
||||
|
||||
if (!collection.timeline) {
|
||||
@@ -590,6 +599,11 @@ export const collectionsSlice = createSlice({
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
if (item.response && item.response.stream?.running) {
|
||||
item.response.data = '';
|
||||
item.response.size = 0;
|
||||
return;
|
||||
}
|
||||
item.response = null;
|
||||
}
|
||||
}
|
||||
@@ -637,6 +651,43 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
saveCollectionDraft: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection && collection.draft) {
|
||||
if (collection.draft.root) {
|
||||
collection.root = collection.draft.root;
|
||||
}
|
||||
if (collection.draft.brunoConfig) {
|
||||
collection.brunoConfig = collection.draft.brunoConfig;
|
||||
}
|
||||
collection.draft = null;
|
||||
}
|
||||
},
|
||||
saveFolderDraft: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
|
||||
if (folder && folder.draft) {
|
||||
folder.root = folder.draft;
|
||||
folder.draft = null;
|
||||
}
|
||||
},
|
||||
deleteCollectionDraft: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection && collection.draft) {
|
||||
collection.draft = null;
|
||||
}
|
||||
},
|
||||
deleteFolderDraft: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
|
||||
if (folder && folder.draft) {
|
||||
folder.draft = null;
|
||||
}
|
||||
},
|
||||
newEphemeralHttpRequest: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -1142,7 +1193,13 @@ export const collectionsSlice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
collection.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
|
||||
collection.draft.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
@@ -1797,40 +1854,50 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
set(collection, 'root.request.auth', {});
|
||||
set(collection, 'root.request.auth.mode', action.payload.mode);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
set(collection, 'draft.root.request.auth', {});
|
||||
set(collection, 'draft.root.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
},
|
||||
updateCollectionAuth: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
set(collection, 'root.request.auth', {});
|
||||
set(collection, 'root.request.auth.mode', action.payload.mode);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
set(collection, 'draft.root.request.auth', {});
|
||||
set(collection, 'draft.root.request.auth.mode', action.payload.mode);
|
||||
switch (action.payload.mode) {
|
||||
case 'awsv4':
|
||||
set(collection, 'root.request.auth.awsv4', action.payload.content);
|
||||
set(collection, 'draft.root.request.auth.awsv4', action.payload.content);
|
||||
break;
|
||||
case 'bearer':
|
||||
set(collection, 'root.request.auth.bearer', action.payload.content);
|
||||
set(collection, 'draft.root.request.auth.bearer', action.payload.content);
|
||||
break;
|
||||
case 'basic':
|
||||
set(collection, 'root.request.auth.basic', action.payload.content);
|
||||
set(collection, 'draft.root.request.auth.basic', action.payload.content);
|
||||
break;
|
||||
case 'digest':
|
||||
set(collection, 'root.request.auth.digest', action.payload.content);
|
||||
set(collection, 'draft.root.request.auth.digest', action.payload.content);
|
||||
break;
|
||||
case 'ntlm':
|
||||
set(collection, 'root.request.auth.ntlm', action.payload.content);
|
||||
set(collection, 'draft.root.request.auth.ntlm', action.payload.content);
|
||||
break;
|
||||
case 'oauth2':
|
||||
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
||||
set(collection, 'draft.root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
case 'wsse':
|
||||
set(collection, 'root.request.auth.wsse', action.payload.content);
|
||||
set(collection, 'draft.root.request.auth.wsse', action.payload.content);
|
||||
break;
|
||||
case 'apikey':
|
||||
set(collection, 'root.request.auth.apikey', action.payload.content);
|
||||
set(collection, 'draft.root.request.auth.apikey', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1839,35 +1906,122 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
set(collection, 'root.request.script.req', action.payload.script);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
set(collection, 'draft.root.request.script.req', action.payload.script);
|
||||
}
|
||||
},
|
||||
updateCollectionResponseScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
set(collection, 'root.request.script.res', action.payload.script);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
set(collection, 'draft.root.request.script.res', action.payload.script);
|
||||
}
|
||||
},
|
||||
updateCollectionTests: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
set(collection, 'root.request.tests', action.payload.tests);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
set(collection, 'draft.root.request.tests', action.payload.tests);
|
||||
}
|
||||
},
|
||||
updateCollectionDocs: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
set(collection, 'root.docs', action.payload.docs);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
set(collection, 'draft.root.docs', action.payload.docs);
|
||||
}
|
||||
},
|
||||
updateCollectionProxy: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root),
|
||||
brunoConfig: cloneDeep(collection.brunoConfig)
|
||||
};
|
||||
}
|
||||
if (!collection.draft.brunoConfig) {
|
||||
collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
}
|
||||
set(collection, 'draft.brunoConfig.proxy', action.payload.proxy);
|
||||
}
|
||||
},
|
||||
updateCollectionClientCertificates: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root),
|
||||
brunoConfig: cloneDeep(collection.brunoConfig)
|
||||
};
|
||||
}
|
||||
if (!collection.draft.brunoConfig) {
|
||||
collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
}
|
||||
set(collection, 'draft.brunoConfig.clientCertificates', action.payload.clientCertificates);
|
||||
}
|
||||
},
|
||||
updateCollectionPresets: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root),
|
||||
brunoConfig: cloneDeep(collection.brunoConfig)
|
||||
};
|
||||
}
|
||||
if (!collection.draft.brunoConfig) {
|
||||
collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
}
|
||||
set(collection, 'draft.brunoConfig.presets', action.payload.presets);
|
||||
}
|
||||
},
|
||||
updateCollectionProtobuf: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root),
|
||||
brunoConfig: cloneDeep(collection.brunoConfig)
|
||||
};
|
||||
}
|
||||
if (!collection.draft.brunoConfig) {
|
||||
collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
}
|
||||
set(collection, 'draft.brunoConfig.protobuf', action.payload.protobuf);
|
||||
}
|
||||
},
|
||||
addFolderHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
const headers = get(folder, 'draft.request.headers', []);
|
||||
headers.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
@@ -1875,14 +2029,17 @@ export const collectionsSlice = createSlice({
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.headers', headers);
|
||||
set(folder, 'draft.request.headers', headers);
|
||||
}
|
||||
},
|
||||
updateFolderHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
const headers = get(folder, 'draft.request.headers', []);
|
||||
const header = find(headers, (h) => h.uid === action.payload.header.uid);
|
||||
if (header) {
|
||||
header.name = action.payload.header.name;
|
||||
@@ -1896,9 +2053,12 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
let headers = get(folder, 'root.request.headers', []);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
let headers = get(folder, 'draft.request.headers', []);
|
||||
headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
|
||||
set(folder, 'root.request.headers', headers);
|
||||
set(folder, 'draft.request.headers', headers);
|
||||
}
|
||||
},
|
||||
addFolderVar: (state, action) => {
|
||||
@@ -1906,24 +2066,27 @@ export const collectionsSlice = createSlice({
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
if (type === 'request') {
|
||||
const vars = get(folder, 'root.request.vars.req', []);
|
||||
const vars = get(folder, 'draft.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.vars.req', vars);
|
||||
set(folder, 'draft.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
const vars = get(folder, 'root.request.vars.res', []);
|
||||
const vars = get(folder, 'draft.request.vars.res', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.vars.res', vars);
|
||||
set(folder, 'draft.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1932,8 +2095,11 @@ export const collectionsSlice = createSlice({
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
if (type === 'request') {
|
||||
let vars = get(folder, 'root.request.vars.req', []);
|
||||
let vars = get(folder, 'draft.request.vars.req', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
@@ -1941,9 +2107,9 @@ export const collectionsSlice = createSlice({
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(folder, 'root.request.vars.req', vars);
|
||||
set(folder, 'draft.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(folder, 'root.request.vars.res', []);
|
||||
let vars = get(folder, 'draft.request.vars.res', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
@@ -1951,7 +2117,7 @@ export const collectionsSlice = createSlice({
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(folder, 'root.request.vars.res', vars);
|
||||
set(folder, 'draft.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1960,14 +2126,17 @@ export const collectionsSlice = createSlice({
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
const type = action.payload.type;
|
||||
if (folder) {
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
if (type === 'request') {
|
||||
let vars = get(folder, 'root.request.vars.req', []);
|
||||
let vars = get(folder, 'draft.request.vars.req', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(folder, 'root.request.vars.req', vars);
|
||||
set(folder, 'draft.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(folder, 'root.request.vars.res', []);
|
||||
let vars = get(folder, 'draft.request.vars.res', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(folder, 'root.request.vars.res', vars);
|
||||
set(folder, 'draft.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1975,21 +2144,30 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
set(folder, 'root.request.script.req', action.payload.script);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
set(folder, 'draft.request.script.req', action.payload.script);
|
||||
}
|
||||
},
|
||||
updateFolderResponseScript: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
set(folder, 'root.request.script.res', action.payload.script);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
set(folder, 'draft.request.script.res', action.payload.script);
|
||||
}
|
||||
},
|
||||
updateFolderTests: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
set(folder, 'root.request.tests', action.payload.tests);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
set(folder, 'draft.request.tests', action.payload.tests);
|
||||
}
|
||||
},
|
||||
updateFolderAuth: (state, action) => {
|
||||
@@ -2000,35 +2178,38 @@ export const collectionsSlice = createSlice({
|
||||
if (!folder) return;
|
||||
|
||||
if (folder) {
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
set(folder, 'draft.request.auth', {});
|
||||
set(folder, 'draft.request.auth.mode', action.payload.mode);
|
||||
switch (action.payload.mode) {
|
||||
case 'oauth2':
|
||||
set(folder, 'root.request.auth.oauth2', action.payload.content);
|
||||
set(folder, 'draft.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
case 'basic':
|
||||
set(folder, 'root.request.auth.basic', action.payload.content);
|
||||
set(folder, 'draft.request.auth.basic', action.payload.content);
|
||||
break;
|
||||
case 'bearer':
|
||||
set(folder, 'root.request.auth.bearer', action.payload.content);
|
||||
set(folder, 'draft.request.auth.bearer', action.payload.content);
|
||||
break;
|
||||
case 'digest':
|
||||
set(folder, 'root.request.auth.digest', action.payload.content);
|
||||
set(folder, 'draft.request.auth.digest', action.payload.content);
|
||||
break;
|
||||
case 'ntlm':
|
||||
set(folder, 'root.request.auth.ntlm', action.payload.content);
|
||||
set(folder, 'draft.request.auth.ntlm', action.payload.content);
|
||||
break;
|
||||
case 'apikey':
|
||||
set(folder, 'root.request.auth.apikey', action.payload.content);
|
||||
set(folder, 'draft.request.auth.apikey', action.payload.content);
|
||||
break;
|
||||
case 'awsv4':
|
||||
set(folder, 'root.request.auth.awsv4', action.payload.content);
|
||||
set(folder, 'draft.request.auth.awsv4', action.payload.content);
|
||||
break;
|
||||
case 'wsse':
|
||||
set(folder, 'root.request.auth.wsse', action.payload.content);
|
||||
set(folder, 'draft.request.auth.wsse', action.payload.content);
|
||||
break;
|
||||
case 'ws':
|
||||
set(folder, 'root.request.auth.ws', action.payload.content);
|
||||
set(folder, 'draft.request.auth.ws', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2037,7 +2218,12 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
const headers = get(collection, 'draft.root.request.headers', []);
|
||||
headers.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
@@ -2045,14 +2231,19 @@ export const collectionsSlice = createSlice({
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.headers', headers);
|
||||
set(collection, 'draft.root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
updateCollectionHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
const headers = get(collection, 'draft.root.request.headers', []);
|
||||
const header = find(headers, (h) => h.uid === action.payload.header.uid);
|
||||
if (header) {
|
||||
header.name = action.payload.header.name;
|
||||
@@ -2066,73 +2257,95 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
let headers = get(collection, 'root.request.headers', []);
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
let headers = get(collection, 'draft.root.request.headers', []);
|
||||
headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
|
||||
set(collection, 'root.request.headers', headers);
|
||||
set(collection, 'draft.root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
addCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
if (type === 'request') {
|
||||
const vars = get(collection, 'root.request.vars.req', []);
|
||||
const vars = get(collection, 'draft.root.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
set(collection, 'draft.root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
const vars = get(collection, 'root.request.vars.res', []);
|
||||
const vars = get(collection, 'draft.root.request.vars.res', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
set(collection, 'draft.root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'root.request.vars.req', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'root.request.vars.res', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'draft.root.request.vars.req', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(collection, 'draft.root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'draft.root.request.vars.res', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(collection, 'draft.root.request.vars.res', vars);
|
||||
}
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
},
|
||||
deleteCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (collection) {
|
||||
if (!collection.draft) {
|
||||
collection.draft = {
|
||||
root: cloneDeep(collection.root)
|
||||
};
|
||||
}
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'root.request.vars.req', []);
|
||||
let vars = get(collection, 'draft.root.request.vars.req', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
set(collection, 'draft.root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'root.request.vars.res', []);
|
||||
let vars = get(collection, 'draft.root.request.vars.res', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
set(collection, 'draft.root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2636,7 +2849,10 @@ export const collectionsSlice = createSlice({
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (folder) {
|
||||
if (isItemAFolder(folder)) {
|
||||
set(folder, 'root.docs', action.payload.docs);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
set(folder, 'draft.docs', action.payload.docs);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2727,11 +2943,32 @@ export const collectionsSlice = createSlice({
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
|
||||
if (folder) {
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
if (!folder.draft) {
|
||||
folder.draft = cloneDeep(folder.root);
|
||||
}
|
||||
set(folder, 'draft.request.auth', {});
|
||||
set(folder, 'draft.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
},
|
||||
streamDataReceived: (state, action) => {
|
||||
const { itemUid, collectionUid, data } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (data.data) {
|
||||
item.response.data ||= [];
|
||||
item.response.data = [{
|
||||
type: 'incoming',
|
||||
message: data.data,
|
||||
messageHexdump: hexdump(data.data),
|
||||
timestamp: Date.now()
|
||||
}].concat(item.response.data);
|
||||
}
|
||||
item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]);
|
||||
item.response.size = data.data?.length + (item.response.size || 0);
|
||||
}
|
||||
},
|
||||
addRequestTag: (state, action) => {
|
||||
const { tag, collectionUid, itemUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -2998,6 +3235,10 @@ export const {
|
||||
clearRequestTimeline,
|
||||
saveRequest,
|
||||
deleteRequestDraft,
|
||||
saveCollectionDraft,
|
||||
saveFolderDraft,
|
||||
deleteCollectionDraft,
|
||||
deleteFolderDraft,
|
||||
newEphemeralHttpRequest,
|
||||
collapseFullCollection,
|
||||
toggleCollection,
|
||||
@@ -3068,6 +3309,10 @@ export const {
|
||||
updateCollectionResponseScript,
|
||||
updateCollectionTests,
|
||||
updateCollectionDocs,
|
||||
updateCollectionProxy,
|
||||
updateCollectionClientCertificates,
|
||||
updateCollectionPresets,
|
||||
updateCollectionProtobuf,
|
||||
collectionAddFileEvent,
|
||||
collectionAddDirectoryEvent,
|
||||
collectionChangeFileEvent,
|
||||
@@ -3085,6 +3330,7 @@ export const {
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
moveCollection,
|
||||
streamDataReceived,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
|
||||
@@ -115,6 +115,7 @@ const darkTheme = {
|
||||
url: {
|
||||
bg: '#3D3D3D',
|
||||
icon: 'rgb(204, 204, 204)',
|
||||
iconDanger: '#fa5343',
|
||||
errorHoverBg: '#4a2a2a'
|
||||
},
|
||||
dragbar: {
|
||||
@@ -241,6 +242,16 @@ const darkTheme = {
|
||||
active: {
|
||||
color: '#CCCCCC',
|
||||
border: '#F59E0B'
|
||||
},
|
||||
secondary: {
|
||||
active: {
|
||||
bg: '#2D2D2D',
|
||||
color: '#CCCCCC'
|
||||
},
|
||||
inactive: {
|
||||
bg: '#3F3F3F',
|
||||
color: '#CCCCCC'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ const lightTheme = {
|
||||
url: {
|
||||
bg: '#f3f3f3',
|
||||
icon: '#515151',
|
||||
iconDanger: '#d91f11',
|
||||
errorHoverBg: '#fef2f2'
|
||||
},
|
||||
dragbar: {
|
||||
@@ -242,6 +243,16 @@ const lightTheme = {
|
||||
active: {
|
||||
color: '#343434',
|
||||
border: '#D97706'
|
||||
},
|
||||
secondary: {
|
||||
active: {
|
||||
bg: '#FFFFFF',
|
||||
color: '#343434'
|
||||
},
|
||||
inactive: {
|
||||
bg: '#ECECEE',
|
||||
color: '#989898'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -748,6 +748,60 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
return itemToSave;
|
||||
};
|
||||
|
||||
export const transformCollectionRootToSave = (collection) => {
|
||||
const _collection = collection.draft?.root ? collection.draft.root : collection.root;
|
||||
|
||||
const collectionRootToSave = {
|
||||
docs: _collection?.docs,
|
||||
meta: _collection?.meta,
|
||||
request: {
|
||||
auth: _collection?.request?.auth,
|
||||
headers: [],
|
||||
script: _collection?.request?.script,
|
||||
vars: _collection?.request?.vars,
|
||||
tests: _collection?.request?.tests
|
||||
}
|
||||
};
|
||||
|
||||
each(_collection?.request?.headers, (header) => {
|
||||
collectionRootToSave.request.headers.push({
|
||||
uid: header.uid,
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: header.enabled
|
||||
});
|
||||
});
|
||||
|
||||
return collectionRootToSave;
|
||||
};
|
||||
|
||||
export const transformFolderRootToSave = (folder) => {
|
||||
const _folder = folder.draft ? folder.draft : folder.root;
|
||||
const folderRootToSave = {
|
||||
docs: _folder.docs,
|
||||
request: {
|
||||
auth: _folder?.request?.auth,
|
||||
headers: [],
|
||||
script: _folder?.request?.script,
|
||||
vars: _folder?.request?.vars,
|
||||
tests: _folder?.request?.tests
|
||||
}
|
||||
};
|
||||
|
||||
each(_folder.request.headers, (header) => {
|
||||
folderRootToSave.request.headers.push({
|
||||
uid: header.uid,
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
description: header.description,
|
||||
enabled: header.enabled
|
||||
});
|
||||
});
|
||||
|
||||
return folderRootToSave;
|
||||
};
|
||||
|
||||
// todo: optimize this
|
||||
export const deleteItemInCollection = (itemUid, collection) => {
|
||||
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
|
||||
@@ -1177,7 +1231,8 @@ const mergeVars = (collection, requestTreePath = []) => {
|
||||
let collectionVariables = {};
|
||||
let folderVariables = {};
|
||||
let requestVariables = {};
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
collectionVariables[_var.name] = _var.value;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { customAlphabet } from 'nanoid';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import fastJsonFormat from 'fast-json-format';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import { patternHasher } from '@usebruno/common/utils';
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
@@ -294,7 +295,7 @@ export const formatResponse = (data, dataBufferString, mode, filter, bufferThres
|
||||
}
|
||||
|
||||
try {
|
||||
return prettifyJsonString(rawData);
|
||||
return fastJsonFormat(rawData);
|
||||
} catch (error) {}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
@@ -326,9 +327,11 @@ export const formatResponse = (data, dataBufferString, mode, filter, bufferThres
|
||||
|
||||
export const prettifyJsonString = (jsonDataString) => {
|
||||
if (typeof jsonDataString !== 'string') return jsonDataString;
|
||||
|
||||
try {
|
||||
const { hashed, restore } = patternHasher(jsonDataString);
|
||||
const formattedJsonDataStringHashed = fastJsonFormat(hashed);
|
||||
const edits = format(hashed, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const formattedJsonDataStringHashed = applyEdits(hashed, edits);
|
||||
const formattedJsonDataString = restore(formattedJsonDataStringHashed);
|
||||
return formattedJsonDataString;
|
||||
} catch (error) {
|
||||
|
||||
@@ -218,16 +218,16 @@ describe('common utils', () => {
|
||||
});
|
||||
|
||||
test('should format complex json string', () => {
|
||||
const input = `{"id": 123456789123456789123456789,"name": "Test 'JSON' Data with "quotes" — Pretty Print ","active": true,"price": 199.9999999,"decimals": 1.00,"nullValue": null,"unicodeText": "こんにちは世界 ","escapedCharacters": "Line1\nLine2\tTabbed\"Quoted\" and 'single quoted' with 'code' style","nestedObject": { "level1": { "level2": { "emptyArray": [], "specialChars": "@#$%^&*()_+-=[]{}|;':,./<>?~", "booleanValues": [ true, false, true ], "numbers": [ 0, -1, 1.23e10, 3.1415926535 ] } }},"mixedArray": [ "string with 'apostrophe'", 42, false, null, { "innerObj": { "keyWithQuotes": "value containing \`backticks\` and 'single quotes'", "nestedArray": [ { "a": "O'Reilly" }{ "b": "'inline code'" }, [ "deep", "array", { "c": "contains 'quotes'" } ] ] } }],"nonStringVariable": {{nonStringVar}},"withBrunoVariable": "{{string}} '{{with}}' "{{variety}}" of '{{variables}}'","dateExample": "2025-11-07T12:34:56Z","regexExample": "^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$","urls": { "website": "https://example.com?param='value'&flag='true'", "escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"},"multiLineString": "This is a long text\nthat spans multiple\nlines with \`backticks\` 'quotes' and 'code' snippets "}`;
|
||||
const input = `{"id": 123456789123456789123456789,"name": "Test 'JSON' Data with \"quotes\" — Pretty Print ","active": true,"price": 199.9999999,"decimals": 1.00,"nullValue": null,"unicodeText": "こんにちは世界 ","escapedCharacters": "Line1\\nLine2\\tTabbed\"Quoted\" and 'single quoted' with 'code' style","nestedObject": { "level1": { "level2": { "emptyArray": [], "specialChars": "@#$%^&*()_+-=[]{}|;':,./<>?~", "booleanValues": [ true, false, true ], "numbers": [ 0, -1, 1.23e10, 3.1415926535 ] } }},"mixedArray": [ "string with 'apostrophe'", 42, false, null, { "innerObj": { "keyWithQuotes": "value containing \`backticks\` and 'single quotes'", "nestedArray": [ { "a": "O'Reilly" }{ "b": "'inline code'" }, [ "deep", "array", { "c": "contains 'quotes'" } ] ] } }],"nonStringVariable": {{nonStringVar}},"withBrunoVariable": "{{string}} '{{with}}' "{{variety}}" of '{{variables}}'","dateExample": "2025-11-07T12:34:56Z","regexExample": "^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$","urls": { "website": "https://example.com?param='value'&flag='true'", "escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"},"multiLineString": "This is a long text\\nthat spans multiple\\nlines with \`backticks\` 'quotes' and 'code' snippets "}`;
|
||||
const expectedOutput = `{
|
||||
"id": 123456789123456789123456789,
|
||||
"name": "Test 'JSON' Data with "quotes" — Pretty Print ",
|
||||
"name": "Test 'JSON' Data with \"quotes\" — Pretty Print ",
|
||||
"active": true,
|
||||
"price": 199.9999999,
|
||||
"decimals": 1.00,
|
||||
"nullValue": null,
|
||||
"unicodeText": "こんにちは世界 ",
|
||||
"escapedCharacters": "Line1\nLine2\tTabbed\"Quoted\" and 'single quoted' with 'code' style",
|
||||
"escapedCharacters": "Line1\\nLine2\\tTabbed\"Quoted\" and 'single quoted' with 'code' style",
|
||||
"nestedObject": {
|
||||
"level1": {
|
||||
"level2": {
|
||||
@@ -280,7 +280,7 @@ describe('common utils', () => {
|
||||
"website": "https://example.com?param='value'&flag='true'",
|
||||
"escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"
|
||||
},
|
||||
"multiLineString": "This is a long text\nthat spans multiple\nlines with \`backticks\` 'quotes' and 'code' snippets "
|
||||
"multiLineString": "This is a long text\\nthat spans multiple\\nlines with \`backticks\` 'quotes' and 'code' snippets "
|
||||
}`;
|
||||
expect(prettifyJsonString(input)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
29
packages/bruno-app/src/utils/curl/content-type.js
Normal file
29
packages/bruno-app/src/utils/curl/content-type.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const normalizeContentType = (contentType) => {
|
||||
if (!contentType || typeof contentType !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return contentType.toLowerCase();
|
||||
};
|
||||
|
||||
export const isJsonLikeContentType = (contentType) => {
|
||||
const normalized = normalizeContentType(contentType);
|
||||
|
||||
return normalized.includes('application/json') || normalized.includes('+json');
|
||||
};
|
||||
|
||||
export const isXmlLikeContentType = (contentType) => {
|
||||
const normalized = normalizeContentType(contentType);
|
||||
|
||||
return normalized.includes('application/xml') || normalized.includes('+xml') || normalized.includes('text/xml');
|
||||
};
|
||||
|
||||
export const isPlainTextContentType = (contentType) => {
|
||||
const normalized = normalizeContentType(contentType);
|
||||
|
||||
return normalized.includes('text/plain');
|
||||
};
|
||||
|
||||
export const isStructuredContentType = (contentType) => {
|
||||
return isJsonLikeContentType(contentType) || isXmlLikeContentType(contentType) || isPlainTextContentType(contentType);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import parseCurlCommand from './parse-curl';
|
||||
import * as querystring from 'query-string';
|
||||
import * as jsesc from 'jsesc';
|
||||
import { buildQueryString } from '@usebruno/common/utils';
|
||||
import { isStructuredContentType } from './content-type';
|
||||
|
||||
function getContentType(headers = {}) {
|
||||
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
|
||||
@@ -34,7 +35,7 @@ function getDataString(request) {
|
||||
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
if (contentType && (contentType.includes('application/json') || contentType.includes('application/xml') || contentType.includes('text/plain'))) {
|
||||
if (isStructuredContentType(contentType)) {
|
||||
return { data: request.data };
|
||||
}
|
||||
|
||||
|
||||
@@ -120,4 +120,37 @@ describe('curlToJson', () => {
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse custom json content-types', () => {
|
||||
const curlCommand = `curl 'https://api.example.com/test'
|
||||
-H 'content-type: application/x.custom+json;version=1'
|
||||
--data-raw '{"test":"data"}'
|
||||
`;
|
||||
|
||||
const result = curlToJson(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com/test',
|
||||
raw_url: 'https://api.example.com/test',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'content-type': 'application/x.custom+json;version=1'
|
||||
},
|
||||
data: '{"test":"data"}'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse vendor tree json content-types', () => {
|
||||
const curlCommand = `curl --request POST \\
|
||||
--url https://api.example.com/orders/42/preferences \\
|
||||
--header 'accept: */*' \\
|
||||
--header 'content-type: application/vnd.vendor+json' \\
|
||||
--data '{\\n "data": {\\n "type": "order-preferences",\\n "attributes": {\\n "notes": "Leave at door",\\n "priority": true\\n }\\n }\\n}'`;
|
||||
|
||||
const result = curlToJson(curlCommand);
|
||||
expect(result.data).toContain('"type": "order-preferences"');
|
||||
expect(result.data).toContain('"notes": "Leave at door"');
|
||||
expect(result.data).toContain('"priority": true');
|
||||
expect(result.headers['content-type']).toBe('application/vnd.vendor+json');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { forOwn } from 'lodash';
|
||||
import curlToJson from './curl-to-json';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
import { isJsonLikeContentType, isPlainTextContentType, isXmlLikeContentType } from './content-type';
|
||||
|
||||
export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => {
|
||||
const parseFormData = (parsedBody) => {
|
||||
@@ -59,25 +60,27 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
|
||||
};
|
||||
|
||||
if (parsedBody && contentType && typeof contentType === 'string') {
|
||||
if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
|
||||
const normalizedContentType = contentType.toLowerCase();
|
||||
|
||||
if (requestType === 'graphql-request' && (isJsonLikeContentType(contentType) || normalizedContentType.includes('application/graphql'))) {
|
||||
body.mode = 'graphql';
|
||||
body.graphql = parseGraphQL(parsedBody);
|
||||
} else if (requestType === 'http-request' && request.isDataBinary) {
|
||||
body.mode = 'file';
|
||||
body.file = parsedBody;
|
||||
}else if (contentType.includes('application/json')) {
|
||||
} else if (isJsonLikeContentType(contentType)) {
|
||||
body.mode = 'json';
|
||||
body.json = prettifyJsonString(parsedBody);
|
||||
} else if (contentType.includes('xml')) {
|
||||
} else if (isXmlLikeContentType(contentType) || normalizedContentType.includes('xml')) {
|
||||
body.mode = 'xml';
|
||||
body.xml = parsedBody;
|
||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
} else if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
|
||||
body.mode = 'formUrlEncoded';
|
||||
body.formUrlEncoded = parseFormData(parsedBody);
|
||||
} else if (contentType.includes('multipart/form-data')) {
|
||||
} else if (normalizedContentType.includes('multipart/form-data')) {
|
||||
body.mode = 'multipartForm';
|
||||
body.multipartForm = parsedBody;
|
||||
} else if (contentType.includes('text/plain')) {
|
||||
} else if (isPlainTextContentType(contentType)) {
|
||||
body.mode = 'text';
|
||||
body.text = parsedBody;
|
||||
}
|
||||
|
||||
@@ -14,3 +14,27 @@ export const processBrunoCollection = async (jsonData) => {
|
||||
throw new BrunoError('Import collection failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const isBrunoCollection = (data) => {
|
||||
// Check for Bruno collection format
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have a version field that is a non-empty string
|
||||
if (typeof data.version !== 'string' || !data.version.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have a name field that is a non-empty string
|
||||
if (typeof data.name !== 'string' || !data.name.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have an items array
|
||||
if (!Array.isArray(data.items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -29,13 +29,19 @@ const isPostmanCollection = (data) => {
|
||||
}
|
||||
|
||||
const schema = info.schema;
|
||||
// Accept schemas hosted at schema.getpostman.com or schema.postman.com
|
||||
const schemaRegex = /^https:\/\/schema\.(?:getpostman|postman)\.com\//;
|
||||
if (typeof schema === 'string' && schemaRegex.test(schema)) {
|
||||
return true;
|
||||
if (typeof schema !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Only accept supported Postman v2.0 and v2.1 schemas
|
||||
const supportedSchemas = [
|
||||
'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
'https://schema.postman.com/json/collection/v2.0.0/collection.json',
|
||||
'https://schema.postman.com/json/collection/v2.1.0/collection.json'
|
||||
];
|
||||
|
||||
return supportedSchemas.includes(schema);
|
||||
};
|
||||
|
||||
export { postmanToBruno, readFile, isPostmanCollection };
|
||||
|
||||
@@ -10,6 +10,7 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
if (response?.error) {
|
||||
resolve(response)
|
||||
}
|
||||
|
||||
resolve({
|
||||
state: 'success',
|
||||
data: response.data,
|
||||
@@ -20,7 +21,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
duration: response.duration,
|
||||
timeline: response.timeline
|
||||
timeline: response.timeline,
|
||||
stream: response.stream
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
|
||||
@@ -14,7 +14,7 @@ const STREAMING_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20MB
|
||||
|
||||
const prepareRequest = async (item = {}, collection = {}) => {
|
||||
const request = item?.request;
|
||||
const brunoConfig = get(collection, 'brunoConfig', {});
|
||||
const brunoConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig', {}) : get(collection, 'brunoConfig', {});
|
||||
const collectionPath = collection?.pathname;
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
@@ -48,7 +48,8 @@ const prepareRequest = async (item = {}, collection = {}) => {
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
if (collectionAuth && request.auth?.mode === 'inherit') {
|
||||
if (collectionAuth.mode === 'basic') {
|
||||
axiosRequest.basicAuth = {
|
||||
|
||||
@@ -123,7 +123,8 @@ const getFolderRoot = (dir) => {
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
|
||||
let collectionHeaders = get(collection, 'root.request.headers', []);
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
let collectionHeaders = get(collectionRoot, 'request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
@@ -132,7 +133,8 @@ const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let _headers = get(i, 'root.request.headers', []);
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
let _headers = get(folderRoot, 'request.headers', []);
|
||||
_headers.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header.value);
|
||||
@@ -153,7 +155,8 @@ const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
|
||||
const mergeVars = (collection, request, requestTreePath) => {
|
||||
let reqVars = new Map();
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);
|
||||
let collectionVariables = {};
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
@@ -165,7 +168,8 @@ const mergeVars = (collection, request, requestTreePath) => {
|
||||
let requestVariables = {};
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
let vars = get(folderRoot, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
reqVars.set(_var.name, _var.value);
|
||||
@@ -197,7 +201,7 @@ const mergeVars = (collection, request, requestTreePath) => {
|
||||
}
|
||||
|
||||
let resVars = new Map();
|
||||
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
|
||||
let collectionResponseVars = get(collectionRoot, 'request.vars.res', []);
|
||||
collectionResponseVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
@@ -205,7 +209,8 @@ const mergeVars = (collection, request, requestTreePath) => {
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.res', []);
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
let vars = get(folderRoot, 'request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
resVars.set(_var.name, _var.value);
|
||||
@@ -232,26 +237,28 @@ const mergeVars = (collection, request, requestTreePath) => {
|
||||
};
|
||||
|
||||
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
|
||||
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
|
||||
let collectionPostResScript = get(collection, 'root.request.script.res', '');
|
||||
let collectionTests = get(collection, 'root.request.tests', '');
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');
|
||||
let collectionPostResScript = get(collectionRoot, 'request.script.res', '');
|
||||
let collectionTests = get(collectionRoot, 'request.tests', '');
|
||||
|
||||
let combinedPreReqScript = [];
|
||||
let combinedPostResScript = [];
|
||||
let combinedTests = [];
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let preReqScript = get(i, 'root.request.script.req', '');
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
let preReqScript = get(folderRoot, 'request.script.req', '');
|
||||
if (preReqScript && preReqScript.trim() !== '') {
|
||||
combinedPreReqScript.push(preReqScript);
|
||||
}
|
||||
|
||||
let postResScript = get(i, 'root.request.script.res', '');
|
||||
let postResScript = get(folderRoot, 'request.script.res', '');
|
||||
if (postResScript && postResScript.trim() !== '') {
|
||||
combinedPostResScript.push(postResScript);
|
||||
}
|
||||
|
||||
let tests = get(i, 'root.request.tests', '');
|
||||
let tests = get(folderRoot, 'request.tests', '');
|
||||
if (tests && tests?.trim?.() !== '') {
|
||||
combinedTests.push(tests);
|
||||
}
|
||||
@@ -320,12 +327,14 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
};
|
||||
|
||||
const mergeAuth = (collection, request, requestTreePath) => {
|
||||
let collectionAuth = collection?.root?.request?.auth || { mode: 'none' };
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
let collectionAuth = collectionRoot?.request?.auth || { mode: 'none' };
|
||||
let effectiveAuth = collectionAuth;
|
||||
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = i?.root?.request?.auth;
|
||||
const folderRoot = i?.draft || i?.root;
|
||||
const folderAuth = get(folderRoot, 'request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
}
|
||||
|
||||
@@ -29,4 +29,10 @@ describe('patternHasher', () => {
|
||||
expect(hashed).toMatchInlineSnapshot(`"$name.example.com"`);
|
||||
expect(restore(hashed)).toEqual(originalUrl);
|
||||
});
|
||||
|
||||
it('verify restoring duplicate hashes', () => {
|
||||
const originalJSON = `{"name":"{{name}}","x":"{{name}}", "y":"{{name}}"}`;
|
||||
const { hashed, restore } = patternHasher(originalJSON);
|
||||
expect(restore(hashed)).toEqual(originalJSON);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ export function patternHasher(input: string, pattern: string | RegExp = VARIABLE
|
||||
let clone = current;
|
||||
for (const hash in hashToOriginal) {
|
||||
const value = hashToOriginal[hash];
|
||||
clone = clone.replace(hash, value);
|
||||
clone = clone.replaceAll(hash, value);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"target": "ES6",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"lib": ["es2021"],
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react",
|
||||
"module": "ESNext",
|
||||
|
||||
@@ -149,9 +149,11 @@ export const brunoToPostman = (collection) => {
|
||||
const generateEventSection = (item) => {
|
||||
const eventArray = [];
|
||||
// Request: item.script, Folder: item.root.request.script, Collection: item.request.script
|
||||
const scriptBlock = item?.script || item?.root?.request?.script || item?.request?.script;
|
||||
// Tests: item.tests, Folder: item.root.request.tests, Collection: item.request.tests
|
||||
const scriptBlock = item?.script || item?.root?.request?.script || item?.request?.script || {};
|
||||
const testsBlock = item?.tests || item?.root?.request?.tests || item?.request?.tests;
|
||||
|
||||
if (scriptBlock?.req) {
|
||||
if (scriptBlock.req && typeof scriptBlock.req === 'string') {
|
||||
eventArray.push({
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
@@ -162,14 +164,27 @@ export const brunoToPostman = (collection) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (scriptBlock?.res) {
|
||||
// testsBlock is added in the post response script since postman only supports tests in the post response script
|
||||
if (scriptBlock.res || testsBlock) {
|
||||
const exec = [];
|
||||
if (scriptBlock.res && typeof scriptBlock.res === 'string') {
|
||||
exec.push(...scriptBlock.res.split('\n'));
|
||||
}
|
||||
if (testsBlock && typeof testsBlock === 'string') {
|
||||
if (exec.length > 0) {
|
||||
exec.push('');
|
||||
}
|
||||
exec.push('// Tests');
|
||||
exec.push(...testsBlock.split('\n'));
|
||||
}
|
||||
|
||||
eventArray.push({
|
||||
listen: 'test',
|
||||
script: {
|
||||
type: 'text/javascript',
|
||||
packages: {},
|
||||
requests: {},
|
||||
exec: scriptBlock.res.split('\n')
|
||||
exec: exec
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
import { brunoToPostman } from '../../src/postman/bruno-to-postman';
|
||||
|
||||
describe('Bruno to Postman Converter with Tests and Scripts', () => {
|
||||
const brunoCollection = {
|
||||
name: 'Script and Tests Collection',
|
||||
version: '1',
|
||||
items: [
|
||||
{
|
||||
name: 'Request With Scripts and Tests',
|
||||
type: 'http',
|
||||
filename: 'request-with-scripts.bru',
|
||||
seq: 1,
|
||||
settings: {
|
||||
encodeUrl: true,
|
||||
timeout: 0
|
||||
},
|
||||
tags: [],
|
||||
examples: [],
|
||||
request: {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{\n "location": "root-request"\n}',
|
||||
formUrlEncoded: [],
|
||||
multipartForm: [],
|
||||
file: []
|
||||
},
|
||||
script: {
|
||||
req: 'console.log("root-request script line 1");\nconsole.log("root-request script line 2")',
|
||||
res: 'console.log("root-request script line 1");\nconsole.log("root-request script line 2")'
|
||||
},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: 'test("Status code is 200", () => {\n expect(res.status).to.eql(200);\n});\ntest("Body is not empty", () => {\n expect(res.text).not.to.eql("");\n});',
|
||||
docs: '',
|
||||
auth: {
|
||||
mode: 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
name: 'Scripts Folder',
|
||||
filename: 'scripts-folder',
|
||||
seq: 2,
|
||||
examples: [],
|
||||
root: {
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'none'
|
||||
},
|
||||
script: {
|
||||
req: 'console.log("scripts-folder script line 1");\nconsole.log("scripts-folder script line 2")',
|
||||
res: 'console.log("scripts-folder script line 1");\nconsole.log("scripts-folder script line 2")'
|
||||
},
|
||||
tests: 'test("Status code is 200", () => {\n expect(res.status).to.eql(200);\n});\ntest("Body is not empty", () => {\n expect(res.text).not.to.eql("");\n});'
|
||||
},
|
||||
meta: {
|
||||
name: 'Scripts Folder',
|
||||
seq: 2
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'http',
|
||||
name: 'Request In Scripts Folder',
|
||||
filename: 'scripts-folder-echo.bru',
|
||||
seq: 1,
|
||||
settings: {
|
||||
encodeUrl: true,
|
||||
timeout: 0
|
||||
},
|
||||
tags: [],
|
||||
examples: [],
|
||||
request: {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{\n "location": "folder-request"\n}',
|
||||
formUrlEncoded: [],
|
||||
multipartForm: [],
|
||||
file: []
|
||||
},
|
||||
script: {
|
||||
req: 'console.log("scripts-folder-request script line 1");\nconsole.log("scripts-folder-request script line 2")',
|
||||
res: 'console.log("scripts-folder-request script line 1");\nconsole.log("scripts-folder-request script line 2")'
|
||||
},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: 'test("Status code is 200", () => {\n expect(res.status).to.eql(200);\n});\ntest("Body is not empty", () => {\n expect(res.text).not.to.eql("");\n});',
|
||||
docs: '',
|
||||
auth: {
|
||||
mode: 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
name: 'Scripts Inner Folder',
|
||||
filename: 'scripts-inner-folder',
|
||||
seq: 2,
|
||||
examples: [],
|
||||
root: {
|
||||
request: {
|
||||
auth: {
|
||||
mode: 'none'
|
||||
},
|
||||
script: {
|
||||
req: 'console.log("scripts-inner-folder script line 1");\nconsole.log("scripts-inner-folder script line 2")',
|
||||
res: 'console.log("scripts-inner-folder script line 1");\nconsole.log("scripts-inner-folder script line 2")'
|
||||
},
|
||||
tests: 'test("Status code is 200", () => {\n expect(res.status).to.eql(200);\n});\ntest("Body is not empty", () => {\n expect(res.text).not.to.eql("");\n});'
|
||||
},
|
||||
meta: {
|
||||
name: 'Scripts Inner Folder',
|
||||
seq: 2
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'http',
|
||||
name: 'Request In Scripts Inner Folder',
|
||||
filename: 'scripts-inner-folder-echo.bru',
|
||||
seq: 2,
|
||||
settings: {
|
||||
encodeUrl: true,
|
||||
timeout: 0
|
||||
},
|
||||
tags: [],
|
||||
examples: [],
|
||||
request: {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: [],
|
||||
params: [],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{\n "location": "inner-folder-request"\n}',
|
||||
formUrlEncoded: [],
|
||||
multipartForm: [],
|
||||
file: []
|
||||
},
|
||||
script: {
|
||||
req: 'console.log("scripts-inner-folder-request script line 1");\nconsole.log("scripts-inner-folder-request script line 2")',
|
||||
res: 'console.log("scripts-inner-folder-request script line 1");\nconsole.log("scripts-inner-folder-request script line 2")'
|
||||
},
|
||||
vars: {},
|
||||
assertions: [],
|
||||
tests: 'test("Status code is 200", () => {\n expect(res.status).to.eql(200);\n});\ntest("Body is not empty", () => {\n expect(res.text).not.to.eql("");\n});',
|
||||
docs: '',
|
||||
auth: {
|
||||
mode: 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
environments: [],
|
||||
root: {
|
||||
request: {
|
||||
script: {
|
||||
req: 'console.log("root-request script line 1");\nconsole.log("root-request script line 2")',
|
||||
res: 'console.log("root-request script line 1");\nconsole.log("root-request script line 2")'
|
||||
},
|
||||
tests: 'test("Status code is 200", () => {\n expect(res.status).to.eql(200);\n});\ntest("Body is not empty", () => {\n expect(res.text).not.to.eql("");\n});'
|
||||
}
|
||||
},
|
||||
brunoConfig: {
|
||||
version: '1',
|
||||
name: 'Script and Tests Collection',
|
||||
type: 'collection',
|
||||
ignore: [
|
||||
'node_modules',
|
||||
'.git'
|
||||
],
|
||||
size: 0.0020351409912109375,
|
||||
filesCount: 6
|
||||
}
|
||||
};
|
||||
|
||||
it('should convert Bruno request scripts and tests to Postman event scripts', () => {
|
||||
const postmanCollection = brunoToPostman(brunoCollection);
|
||||
// Root request events
|
||||
const rootRequest = postmanCollection.item.find((i) => i.name === 'Request With Scripts and Tests');
|
||||
const rootPre = rootRequest.event.find((e) => e.listen === 'prerequest');
|
||||
const rootTest = rootRequest.event.find((e) => e.listen === 'test');
|
||||
expect(rootPre).toBeDefined();
|
||||
expect(rootTest).toBeDefined();
|
||||
expect(rootPre.script.exec).toEqual([
|
||||
'console.log("root-request script line 1");',
|
||||
'console.log("root-request script line 2")'
|
||||
]);
|
||||
expect(rootTest.script.exec).toEqual([
|
||||
'console.log("root-request script line 1");',
|
||||
'console.log("root-request script line 2")',
|
||||
'',
|
||||
'// Tests',
|
||||
'test("Status code is 200", () => {',
|
||||
' expect(res.status).to.eql(200);',
|
||||
'});',
|
||||
'test("Body is not empty", () => {',
|
||||
' expect(res.text).not.to.eql("");',
|
||||
'});'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert Bruno folder scripts and tests to Postman event scripts', () => {
|
||||
const postmanCollection = brunoToPostman(brunoCollection);
|
||||
// Folder events
|
||||
const folder = postmanCollection.item.find((i) => i.name === 'Scripts Folder');
|
||||
const folderPre = folder.event.find((e) => e.listen === 'prerequest');
|
||||
const folderTest = folder.event.find((e) => e.listen === 'test');
|
||||
expect(folderPre).toBeDefined();
|
||||
expect(folderTest).toBeDefined();
|
||||
expect(folderPre.script.exec).toEqual([
|
||||
'console.log("scripts-folder script line 1");',
|
||||
'console.log("scripts-folder script line 2")'
|
||||
]);
|
||||
expect(folderTest.script.exec).toEqual([
|
||||
'console.log("scripts-folder script line 1");',
|
||||
'console.log("scripts-folder script line 2")',
|
||||
'',
|
||||
'// Tests',
|
||||
'test("Status code is 200", () => {',
|
||||
' expect(res.status).to.eql(200);',
|
||||
'});',
|
||||
'test("Body is not empty", () => {',
|
||||
' expect(res.text).not.to.eql("");',
|
||||
'});'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert Bruno inner folder scripts and tests to Postman event scripts', () => {
|
||||
const postmanCollection = brunoToPostman(brunoCollection);
|
||||
const folder = postmanCollection.item.find((i) => i.name === 'Scripts Folder');
|
||||
// Inner folder events
|
||||
const innerFolder = folder.item.find((i) => i.name === 'Scripts Inner Folder');
|
||||
const innerFolderPre = innerFolder.event.find((e) => e.listen === 'prerequest');
|
||||
const innerFolderTest = innerFolder.event.find((e) => e.listen === 'test');
|
||||
expect(innerFolderPre).toBeDefined();
|
||||
expect(innerFolderTest).toBeDefined();
|
||||
expect(innerFolderPre.script.exec).toEqual([
|
||||
'console.log("scripts-inner-folder script line 1");',
|
||||
'console.log("scripts-inner-folder script line 2")'
|
||||
]);
|
||||
expect(innerFolderTest.script.exec).toEqual([
|
||||
'console.log("scripts-inner-folder script line 1");',
|
||||
'console.log("scripts-inner-folder script line 2")',
|
||||
'',
|
||||
'// Tests',
|
||||
'test("Status code is 200", () => {',
|
||||
' expect(res.status).to.eql(200);',
|
||||
'});',
|
||||
'test("Body is not empty", () => {',
|
||||
' expect(res.text).not.to.eql("");',
|
||||
'});'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert Bruno collection scripts and tests to Postman event scripts', () => {
|
||||
const postmanCollection = brunoToPostman(brunoCollection);
|
||||
// Collection events
|
||||
const collectionPre = postmanCollection.event.find((e) => e.listen === 'prerequest');
|
||||
const collectionTest = postmanCollection.event.find((e) => e.listen === 'test');
|
||||
expect(collectionPre).toBeDefined();
|
||||
expect(collectionTest).toBeDefined();
|
||||
expect(collectionPre.script.exec).toEqual([
|
||||
'console.log("root-request script line 1");',
|
||||
'console.log("root-request script line 2")'
|
||||
]);
|
||||
expect(collectionTest.script.exec).toEqual([
|
||||
'console.log("root-request script line 1");',
|
||||
'console.log("root-request script line 2")',
|
||||
'',
|
||||
'// Tests',
|
||||
'test("Status code is 200", () => {',
|
||||
' expect(res.status).to.eql(200);',
|
||||
'});',
|
||||
'test("Body is not empty", () => {',
|
||||
' expect(res.text).not.to.eql("");',
|
||||
'});'
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1062,6 +1062,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
const certsAndProxyConfig = await getCertsAndProxyConfig({
|
||||
collectionUid,
|
||||
collection,
|
||||
request: requestCopy,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
@@ -1195,6 +1196,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
|
||||
const certsAndProxyConfig = await getCertsAndProxyConfig({
|
||||
collectionUid,
|
||||
collection,
|
||||
request: requestCopy,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
|
||||
@@ -11,6 +11,7 @@ const { interpolateString } = require('./interpolate-string');
|
||||
*/
|
||||
const getCertsAndProxyConfig = async ({
|
||||
collectionUid,
|
||||
collection,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
@@ -40,7 +41,7 @@ const getCertsAndProxyConfig = async ({
|
||||
httpsAgentRequestFields['caCertificatesCount'] = caCertificatesCount;
|
||||
httpsAgentRequestFields['ca'] = caCertificates || [];
|
||||
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collectionUid, collection);
|
||||
const interpolationOptions = {
|
||||
globalEnvironmentVariables,
|
||||
envVars,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user