Merge branch 'main' into feature/1602-multipart-content-type

# Conflicts:
#	packages/bruno-lang/v2/src/bruToJson.js
#	packages/bruno-lang/v2/src/jsonToBru.js
This commit is contained in:
busy-panda 2024-07-01 15:17:53 +02:00
commit a703b84681
154 changed files with 4817 additions and 819 deletions

View File

@ -1,5 +1,20 @@
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md)
| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
**English**
| [Українська](docs/contributing/contributing_ua.md)
| [Русский](docs/contributing/contributing_ru.md)
| [Türkçe](docs/contributing/contributing_tr.md)
| [Deutsch](docs/contributing/contributing_de.md)
| [Français](docs/contributing/contributing_fr.md)
| [Português (BR)](docs/contributing/contributing_pt_br.md)
| [한국어](docs/contributing/contributing_kr.md)
| [বাংলা](docs/contributing/contributing_bn.md)
| [Español](docs/contributing/contributing_es.md)
| [Italiano](docs/contributing/contributing_it.md)
| [Română](docs/contributing/contributing_ro.md)
| [Polski](docs/contributing/contributing_pl.md)
| [简体中文](docs/contributing/contributing_cn.md)
| [正體中文](docs/contributing/contributing_zhtw.md)
| [日本語](docs/contributing/contributing_ja.md)
| [हिंदी](docs/contributing/contributing_hi.md)
## Let's make Bruno better, together!!

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](/contributing_fr.md) | **বাংলা** | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| **বাংলা**
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## আসুন ব্রুনোকে আরও ভালো করি, একসাথে!!

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | [Українська](./contributing_ua.md) | [Русский](./contributing_ru.md) | [Türkçe](./contributing_tr.md) | [Deutsch](./contributing_de.md) | [Français](./contributing_fr.md) | [Português (BR)](./contributing_pt_br.md) | [বাংলা](./contributing_bn.md) | [Español](./contributing_es.md) | [Română](./contributing_ro.md) | [Polski](./contributing_pl.md) | **简体中文** | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| **简体中文**
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## 让我们一起改进 Bruno
@ -23,7 +39,6 @@ Bruno 基于 NextJs 和 React 构建。我们使用 Electron 来封装桌面版
您需要 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区_npm workspaces_
## 开发
Bruno 是作为一个 _client lourd重客户端_ 应用程序开发的。您需要在一个终端中启动 nextjs 来加载应用程序,然后在另一个终端中启动 Electron 应用程序。
@ -68,7 +83,6 @@ done
find . -type f -name "package-lock.json" -delete
```
### 测试
```bash
@ -79,7 +93,6 @@ npm test --workspace=packages/bruno-schema
npm test --workspace=packages/bruno-lang
```
### 提交 Pull Request
- 请保持 PR 精简并专注于单一目标

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | **Deutsch** | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| **Deutsch**
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Lass uns Bruno noch besser machen, gemeinsam!!

View File

@ -1,3 +1,21 @@
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| **Español**
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## ¡Juntos, hagamos a Bruno mejor!
Estamos encantados de que quieras ayudar a mejorar Bruno. A continuación encontrarás las instrucciones para empezar a trabajar con Bruno en tu computadora.

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | **Français** | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| **Français**
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Ensemble, améliorons Bruno !
@ -27,7 +43,6 @@ Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org
Bruno est développé comme une application _client lourd_. Vous devrez charger l'application en démarrant nextjs dans un premier terminal, puis démarre l'application Electron dans un second.
### Dépendances
- NodeJS v18
@ -68,7 +83,6 @@ done
find . -type f -name "package-lock.json" -delete
```
### Tests
```bash
@ -79,7 +93,6 @@ npm test --workspace=packages/bruno-schema
npm test --workspace=packages/bruno-lang
```
### Ouvrir une Pull Request
- Merci de conserver les PR minimes et focalisées sur un seul objectif

View File

@ -0,0 +1,98 @@
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| **हिंदी**
## आइए मिलकर Bruno को बेहतर बनाएं !!
हमें खुशी है कि आप Bruno को बेहतर बनाना चाहते हैं। Bruno को अपने कंप्यूटर पर लाना शुरू करने के लिए दिशानिर्देश नीचे दिए गए हैं।
### टेक्नोलॉजी स्टैक
Bruno को Next.js और React का उपयोग करके बनाया गया है। हम डेस्कटॉप संस्करण को शिप करने के लिए इलेक्ट्रॉन का भी उपयोग करते हैं (जो स्थानीय संग्रह का समर्थन करता है)
Libraries जिनका हम उपयोग करते हैं
- CSS - Tailwind
- कोड संपादक - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Forms - formik
- Schema Validation - Yup
- Request Client - axios
- Filesystem Watcher - chokidar
### निर्भरताएँ
आपको [Node v18.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं
## डेवलपमेंट
Bruno को एक डेस्कटॉप ऐप के रूप में बनाया किया जा रहा है। आपको Next.js ऐप को एक टर्मिनल में चलाकर ऐप को लोड करना होगा और फिर इलेक्ट्रॉन ऐप को दूसरे टर्मिनल में चलाना होगा।
### लोकल डेवलपमेंट
```bash
# nodejs 18 संस्करण का उपयोग करें
nvm use
# डिपेंडेंसी इनस्टॉल करे
npm i --legacy-peer-deps
# पैकेज बिल्ड करें
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
# Next.js ऐप चलाएँ (टर्मिनल 1 पर)
npm run dev:web
# इलेक्ट्रॉन ऐप चलाएँ (टर्मिनल 2 पर)
npm run dev:electron
```
### समस्या निवारण
जब आप `npm इंस्टॉल` चलाते हैं तो आपको `असमर्थित प्लेटफ़ॉर्म` त्रुटि का सामना करना पड़ सकता है। इसे ठीक करने के लिए, आपको `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 डिलीट करे
find . -type f -name "package-lock.json" -delete
```
### परिक्षण
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### पुल अनुरोध प्रक्रिया
- कृपया PR को छोटा रखें और एक चीज़ पर केंद्रित रखें
- कृपया शाखाएँ बनाने के प्रारूप का पालन करें
- feature/[feature name]: इस शाखा में किसी विशिष्ट सुविधा के लिए परिवर्तन होने चाहिए
- उदाहरण: feature/dark-mode
- bugfix/[bug name]: इस शाखा में केवल विशिष्ट बग के लिए बग फिक्स शामिल होने चाहिए
- उदाहरण bugfix/bug-1

View File

@ -1,3 +1,21 @@
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| **Italiano**
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Insieme, miglioriamo Bruno!
Sono felice di vedere che hai intenzione di migliorare Bruno. Di seguito, troverai le regole e le guide per ripristinare Bruno sul tuo computer.

View File

@ -0,0 +1,98 @@
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| **日本語**
| [हिंदी](./contributing_hi.md)
## 一緒に Bruno をよりよいものにしていきましょう!!
Bruno を改善していただけるのは歓迎です。以下はあなたの環境で Bruno を起動するためのガイドラインです。
### 技術スタック
Bruno は Next.js と React で作られています。デスクトップアプリ(ローカルのコレクションに対応しています)には electron も使用しています。
使用ライブラリ
- CSS - Tailwind
- Code Editors - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Forms - formik
- Schema Validation - Yup
- Request Client - axios
- Filesystem Watcher - chokidar
### 依存関係
[Node v18.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。
## 開発
Bruno はデスクトップアプリとして開発されています。一つのターミナルで Next.js アプリを立ち上げ、もう一つのターミナルで electron アプリを立ち上げてアプリを読み込む必要があります。
### ローカル環境での開発
```bash
# use nodejs 18 version
nvm use
# install deps
npm i --legacy-peer-deps
# build packages
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
### トラブルシューティング
`npm install`を実行すると、`Unsupported platform`エラーに遭遇することがあります。これを直すためには、`node_modules`と`package-lock.json`を削除し、`npm install`を実行しなおす必要があります。これにより、アプリを動かすのに必要なパッケージがすべてインストールされます。
```shell
# Delete node_modules in sub-directories
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Delete package-lock in sub-directories
find . -type f -name "package-lock.json" -delete
```
### テストを動かすには
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### プルリクエストの手順
- プルリクエストは小規模で、一つのことにフォーカスしたものにしてください。
- 以下のフォーマットに従ってブランチを作ってください。
- feature/[feature name]: このブランチには特定の機能に対する変更を含んでください。
- 例: feature/dark-mode
- bugfix/[bug name]: このブランチには特定のバグに対する修正のみを含むようにしてください。
- 例: bugfix/bug-1

View File

@ -1,5 +1,20 @@
[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md)
| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | **한국어**
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| **한국어**
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## 함께 Bruno를 더 좋게 만들어요!!

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | **Polski** | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| **Polski**
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Wspólnie uczynijmy Bruno lepszym !!
@ -51,7 +67,7 @@ npm run dev:web
# uruchom aplikację electron (terminal 2)
npm run dev:electron
```
### Rozwiązywanie Problemów

View File

@ -1,3 +1,21 @@
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| **Português (BR)**
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Vamos tornar o Bruno melhor, juntos!!
Estamos felizes que você queira ajudar a melhorar o Bruno. Abaixo estão as diretrizes e orientações para começar a executar o Bruno no seu computador.

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | [Українська](/docs/contributing/contributing_ua.md) | [Русский](/docs/contributing/contributing_ru.md) | [Türkçe](/docs/contributing/contributing_tr.md) | [Deutsch](/docs/contributing/contributing_de.md) | [Français](/docs/contributing/contributing_fr.md) | [Português (BR)](/docs/contributing/contributing_pt_br.md) | [বাংলা](/docs/contributing/contributing_bn.md) | [Español](/docs/contributing/contributing_es.md) | [Italiano](/docs/contributing/contributing_it.md) | **Română** | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| **Română**
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Haideţi să îmbunătățim Bruno, împreună!!

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | **Русский** | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| **Русский**
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Давайте вместе сделаем Бруно лучше!!!

View File

@ -1,4 +1,20 @@
[English](../../contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | **Türkçe** | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| **Türkçe**
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Bruno'yu birlikte daha iyi hale getirelim!!!
@ -57,7 +73,6 @@ npm run dev:electron
`npm install`'ı çalıştırdığınızda `Unsupported platform` hatası ile karşılaşabilirsiniz. Bunu düzeltmek için `node_modules` ve `package-lock.json` dosyalarını silmeniz ve `npm install` dosyasını çalıştırmanız gerekecektir. Bu, uygulamayı çalıştırmak için gereken tüm gerekli paketleri yüklemelidir.
```shell
# Alt dizinlerdeki node_modules öğelerini silme
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | **Українська** | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md)
[English](../../contributing.md)
| **Українська**
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| [正體中文](./contributing_zhtw.md)
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## Давайте зробимо Bruno краще, разом !!

View File

@ -1,4 +1,20 @@
[English](/contributing.md) | [Українська](./contributing_ua.md) | [Русский](./contributing_ru.md) | [Türkçe](./contributing_tr.md) | [Deutsch](./contributing_de.md) | [Français](./contributing_fr.md) | [Português (BR)](./contributing_pt_br.md) | [বাংলা](./contributing_bn.md) | [Español](./contributing_es.md) | [Română](./contributing_ro.md) | [Polski](./contributing_pl.md) | [简体中文](./contributing_cn.md) | **正體中文**
[English](../../contributing.md)
| [Українська](./contributing_ua.md)
| [Русский](./contributing_ru.md)
| [Türkçe](./contributing_tr.md)
| [Deutsch](./contributing_de.md)
| [Français](./contributing_fr.md)
| [Português (BR)](./contributing_pt_br.md)
| [한국어](./contributing_kr.md)
| [বাংলা](./contributing_bn.md)
| [Español](./contributing_es.md)
| [Italiano](./contributing_it.md)
| [Română](./contributing_ro.md)
| [Polski](./contributing_pl.md)
| [简体中文](./contributing_cn.md)
| **正體中文**
| [日本語](./contributing_ja.md)
| [हिंदी](./contributing_hi.md)
## 讓我們一起來讓 Bruno 變得更好!
@ -23,12 +39,10 @@ Bruno 使用 Next.js 和 React 構建。我們使用 Electron 來封裝及發佈
您需要使用 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區_npm workspaces_
## 開發
Bruno 正以桌面應用程式的形式開發。您需要在一個終端機中執行 Next.js 來載入應用程式,然後在另一個終端機中執行 electron 應用程式。
### 開發依賴
- NodeJS v18
@ -69,7 +83,6 @@ done
find . -type f -name "package-lock.json" -delete
```
### 測試
```bash
@ -80,7 +93,6 @@ npm test --workspace=packages/bruno-schema
npm test --workspace=packages/bruno-lang
```
### 發送 Pull Request
- 請保持 PR 精簡並專注於一個目標

View File

@ -1,4 +1,14 @@
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | **বাংলা** | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md)
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| [Deutsch](./publishing_de.md)
| [Français](./publishing_fr.md)
| [Português (BR)](./publishing_pt_br.md)
| **বাংলা**
| [Română](./publishing_ro.md)
| [Polski](./publishing_pl.md)
| [简体中文](./publishing_cn.md)
| [正體中文](./publishing_zhtw.md)
| [日本語](./publishing_ja.md)
### ব্রুনোকে নতুন প্যাকেজ ম্যানেজারে প্রকাশ করা

View File

@ -1,4 +1,14 @@
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | **简体中文** | [正體中文](docs/publishing/publishing_zhtw.md)
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| [Deutsch](./publishing_de.md)
| [Français](./publishing_fr.md)
| [Português (BR)](./publishing_pt_br.md)
| [বাংলা](./publishing_bn.md)
| [Română](./publishing_ro.md)
| [Polski](./publishing_pl.md)
| **简体中文**
| [正體中文](./publishing_zhtw.md)
| [日本語](./publishing_ja.md)
### 将 Bruno 发布到新的包管理器

View File

@ -0,0 +1,20 @@
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| **Deutsch**
| [Français](./publishing_fr.md)
| [Português (BR)](./publishing_pt_br.md)
| [বাংলা](./publishing_bn.md)
| [Română](./publishing_ro.md)
| [Polski](./publishing_pl.md)
| [简体中文](./publishing_cn.md)
| [正體中文](./publishing_zhtw.md)
| [日本語](./publishing_ja.md)
### Veröffentlichung von Bruno über neue Paket-Manager
Obwohl Bruno Open Source und für alle frei zugänglich ist, bitten wir dich Kontakt zu uns aufzunehmen, bevor du Bruno über weitere Paket-Manager veröffentlichst.
Als Schöpfer von Bruno liegen alle Marktrechte von `Bruno` bei mir und ich möchte die volle Kontrolle über alle Verbreitungswege behalten.
Falls Bruno über einen weiteren Paketmanager veröffentlicht werden soll, eröffne bitte ein GitHub-Issue.
Während ein Großteil der Features kostenlos und Open Source ist (beinhaltet REST und GraphQL APIs),
bemühen wir uns um ein harmonisches Gleichgewicht zwischen Open-Source-Prinzipien und Nachhaltigkeit - https://github.com/usebruno/bruno/discussions/269

View File

@ -1,4 +1,14 @@
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | **Français** | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md)
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| [Deutsch](./publishing_de.md)
| **Français**
| [Português (BR)](./publishing_pt_br.md)
| [বাংলা](./publishing_bn.md)
| [Română](./publishing_ro.md)
| [Polski](./publishing_pl.md)
| [简体中文](./publishing_cn.md)
| [正體中文](./publishing_zhtw.md)
| [日本語](./publishing_ja.md)
### Publier Bruno dans un nouveau gestionnaire de paquets

View File

@ -0,0 +1,18 @@
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| [Deutsch](./publishing_de.md)
| [Français](./publishing_fr.md)
| [Português (BR)](./publishing_pt_br.md)
| [বাংলা](./publishing_bn.md)
| [Română](./publishing_ro.md)
| [Polski](./publishing_pl.md)
| [简体中文](./publishing_cn.md)
| [正體中文](./publishing_zhtw.md)
| **日本語**
### Bruno を新しいパッケージマネージャに公開する場合の注意
私たちのソースコードはオープンソースで誰でも使用できますが、新しいパッケージマネージャで公開を検討する前に、私たちにご連絡ください。私は Bruno の製作者として、このプロジェクト「Bruno」の商標を保有しており、その配布を管理したいと考えています。もし新しいパッケージマネージャで Bruno を使いたい場合は、GitHub の issue を立ててください。
私たちの機能の大部分が無料でオープンソース(REST や GraphQL の API も含む)ですが、
私たちはオープンソースの原則と長期的な維持の間でよいバランスをとれるように努力しています- https://github.com/usebruno/bruno/discussions/269

View File

@ -1,4 +1,14 @@
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | **Polski** | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md)
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| [Deutsch](./publishing_de.md)
| [Français](./publishing_fr.md)
| [Português (BR)](./publishing_pt_br.md)
| [বাংলা](./publishing_bn.md)
| [Română](./publishing_ro.md)
| **Polski**
| [简体中文](./publishing_cn.md)
| [正體中文](./publishing_zhtw.md)
| [日本語](./publishing_ja.md)
### Publikowanie Bruno w nowym menedżerze pakietów

View File

@ -1,4 +1,14 @@
[English](/publishing.md) | **Português (BR)** | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md)
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| [Deutsch](./publishing_de.md)
| [Français](./publishing_fr.md)
| **Português (BR)**
| [বাংলা](./publishing_bn.md)
| [Română](./publishing_ro.md)
| [Polski](./publishing_pl.md)
| [简体中文](./publishing_cn.md)
| [正體中文](./publishing_zhtw.md)
| [日本語](./publishing_ja.md)
### Publicando Bruno em um novo gerenciador de pacotes

View File

@ -1,4 +1,14 @@
[English](/publishing.md) | [Português (BR)](/docs/publishing/publishing_pt_br.md) | **Română** | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](/docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md)
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| [Deutsch](./publishing_de.md)
| [Français](./publishing_fr.md)
| [Português (BR)](./publishing_pt_br.md)
| [বাংলা](./publishing_bn.md)
| **Română**
| [Polski](./publishing_pl.md)
| [简体中文](./publishing_cn.md)
| [正體中文](./publishing_zhtw.md)
| [日本語](./publishing_ja.md)
### Publicarea lui Bruno la un gestionar de pachete nou

View File

@ -1,4 +1,14 @@
[English](../../publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | **Türkçe** | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [简体中文](docs/publishing/publishing_cn.md) | [正體中文](docs/publishing/publishing_zhtw.md)
[English](../../publishing.md)
| **Türkçe**
| [Deutsch](./publishing_de.md)
| [Français](./publishing_fr.md)
| [Português (BR)](./publishing_pt_br.md)
| [বাংলা](./publishing_bn.md)
| [Română](./publishing_ro.md)
| [Polski](./publishing_pl.md)
| [简体中文](./publishing_cn.md)
| [正體中文](./publishing_zhtw.md)
| [日本語](./publishing_ja.md)
### Bruno'yu yeni bir paket yöneticisine yayınlama

View File

@ -1,4 +1,14 @@
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | **正體中文** | [简体中文](docs/publishing/publishing_cn.md)
[English](../../publishing.md)
| [Türkçe](./publishing_tr.md)
| [Deutsch](./publishing_de.md)
| [Français](./publishing_fr.md)
| [Português (BR)](./publishing_pt_br.md)
| [বাংলা](./publishing_bn.md)
| [Română](./publishing_ro.md)
| [Polski](./publishing_pl.md)
| [简体中文](./publishing_cn.md)
| **正體中文**
| [日本語](./publishing_ja.md)
### 將 Bruno 發佈到新的套件管理器

View File

@ -1,16 +1,32 @@
<br />
<img src="assets/images/logo-transparent.png" width="80"/>
<img src="../../assets/images/logo-transparent.png" width="80"/>
### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs).
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
**English** | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) | [العربية](docs/readme/readme_ar.md)
[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)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| **العربية**
| [日本語](./readme_ja.md)
برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك.
@ -22,7 +38,7 @@
📢 شاهد حديثنا الأخير في مؤتمر India FOSS 3.0 [هنا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](https://github.com/usebruno/bruno/blob/main/assets/images/landing-2.png) <br /><br />
![bruno](/assets/images/landing-2.png) <br /><br />
### الطبعة الذهبية ✨
@ -64,15 +80,16 @@ echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ br
sudo apt update
sudo apt
```
### التشغيل عبر منصات متعددة 🖥️
![bruno](https://github.com/usebruno/bruno/blob/main/assets/images/run-anywhere.png) <br /><br />
![bruno](/assets/images/run-anywhere.png) <br /><br />
### التعاون عبر Git 👩‍💻🧑‍💻
أو أي نظام تحكم في الإصدار الذي تفضله
![bruno](https://github.com/usebruno/bruno/blob/main/assets/images/version-control.png) <br /><br />
![bruno](/assets/images/version-control.png) <br /><br />
### الروابط المهمة 📌
@ -101,7 +118,7 @@ sudo apt
### نشر إلى مديري الحزم الجديدة
يرجى الرجوع [هنا](publishing.md) لمزيد من المعلومات.
يرجى الرجوع [هنا](../../publishing.md) لمزيد من المعلومات.
### تواصل معنا 🌐
@ -122,7 +139,7 @@ sudo apt
### المساهمة 👩‍💻🧑‍💻
يسعدني أنك تتطلع لتحسين برونو. يرجى الاطلاع على [دليل المساهمة](contributing.md)
يسعدني أنك تتطلع لتحسين برونو. يرجى الاطلاع على [دليل المساهمة](../../contributing.md)
حتى إذا لم تكن قادرًا على التساهم بشكل مباشر من خلال الشيفرة، فلا تتردد في الإبلاغ عن الأخطاء وطلب الميزات التي يجب تنفيذها لحل حالتك.
@ -136,4 +153,4 @@ sudo apt
### الرخصة 📄
[MIT](license.md)
[MIT](../../license.md)

View File

@ -4,13 +4,29 @@
### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE।
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | **বাংলা** | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md)
[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)
| **বাংলা**
| [Español](./readme_es.md)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।
@ -89,7 +105,7 @@ sudo apt install bruno
### অবদান 👩‍💻🧑‍💻
আমি খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। অনুগ্রহ করে [অবদানকারী নির্দেশিকা](contributing.md) দেখুন
আমি খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। অনুগ্রহ করে [অবদানকারী নির্দেশিকা](../contributing/contributing_bn.md) দেখুন
আপনি কোডের মাধ্যমে অবদান রাখতে না পারলেও, অনুগ্রহ করে বাগ এবং বৈশিষ্ট্যের অনুরোধ ফাইল করতে দ্বিধা করবেন না যা আপনার ব্যবহারের ক্ষেত্রে সমাধান করার জন্য প্রয়োগ করা প্রয়োজন।
@ -120,4 +136,4 @@ sudo apt install bruno
### লাইসেন্স 📄
[MIT](license.md)
[MIT](../../license.md)

View File

@ -4,14 +4,29 @@
### Bruno - 开源 IDE用于探索和测试 API。
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](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) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | **简体中文** | [正體中文](docs/readme/readme_zhtw.md)
[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)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| **简体中文**
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。
@ -21,7 +36,6 @@ Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯
Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)
📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](../../assets/images/landing-2.png) <br /><br />
@ -93,11 +107,11 @@ sudo apt install bruno
### 发布到新的包管理器
有关更多信息,请参见 [此处](../../publishing_cn.md) 。
有关更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
### 贡献 👩‍💻🧑‍💻
我很高兴您希望改进bruno。请查看 [贡献指南](../../contributing_cn.md)。
我很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。

View File

@ -4,13 +4,29 @@
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | **Deutsch** | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md)
[English](../../readme.md)
| [Українська](./readme_ua.md)
| [Русский](./readme_ru.md)
| [Türkçe](./readme_tr.md)
| **Deutsch**
| [Français](./readme_fr.md)
| [Português (BR)](./readme_pt_br.md)
| [한국어](./readme_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.
@ -114,7 +130,7 @@ Wenn Bruno dir und in deinen Teams bei der Arbeit geholfen hat, vergiss bitte ni
### Bereitstellung in neuen Paket-Managern
Mehr Informationen findest du [hier](/publishing.md).
Mehr Informationen findest du [hier](../publishing/publishing_de.md).
### Mitmachen 👩‍💻🧑‍💻
@ -149,4 +165,4 @@ Das Logo stammt von [OpenMoji](https://openmoji.org/library/emoji-1F436/). Lizen
### Lizenz 📄
[MIT](/license.md)
[MIT](../../license.md)

View File

@ -4,14 +4,29 @@
### Bruno - IDE de código abierto para explorar y probar APIs.
[![Versión en Github](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Actividad de Commits](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Sitio Web](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Descargas](https://img.shields.io/badge/Download-Latest-brightgreen)](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** | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](./readme_zhtw.md)
[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**
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares.
Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs.
@ -101,11 +116,11 @@ Si Bruno te ha ayudado en tu trabajo y con tus equipos, por favor, no olvides co
### Publicar en nuevos gestores de paquetes
Por favor, consulta [aquí](publishing.md) para más información.
Por favor, consulta [aquí](../../publishing.md) para más información.
### Contribuye 👩‍💻🧑‍💻
Estamos encantados de que quieras ayudar a mejorar Bruno. Por favor, consulta la [guía de contribución](contributing_es.md) para más información.
Estamos encantados de que quieras ayudar a mejorar Bruno. Por favor, consulta la [guía de contribución](../contributing/contributing_es.md) para más información.
Incluso si no puedes contribuir con código, no dudes en reportar errores y solicitar nuevas funcionalidades que necesites para resolver tu caso de uso.
@ -136,4 +151,4 @@ El logo fue obtenido de [OpenMoji](https://openmoji.org/library/emoji-1F436/). L
### Licencia 📄
[MIT](license.md)
[MIT](../../license.md)

View File

@ -4,16 +4,31 @@
### Bruno - IDE Opensource pour explorer et tester des APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](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**
| [Português (BR)](./readme_pt_br.md)
| [한국어](./readme_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
[English](/readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | **Français** | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md)
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représente Postman et les autres outils.
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils.
Bruno sauvegarde vos collections directement sur votre système de fichiers. Nous utilisons un langage de balise de type texte pour décrire les requêtes API.
@ -21,9 +36,7 @@ Vous pouvez utiliser git ou tout autre gestionnaire de version pour travailler d
Bruno ne fonctionne qu'en mode déconnecté. Il n'y a pas d'abonnement ou de synchronisation avec le cloud Bruno, il n'y en aura jamais. Nous sommes conscients de la confidentialité de vos données et nous sommes convaincus qu'elles doivent rester sur vos appareils. Vous pouvez lire notre vision à long terme [ici (en anglais)](https://github.com/usebruno/bruno/discussions/269).
📢 Regarder notre présentation récente lors de la conférence India FOSS 3.0 (en anglais) [ici](https://www.youtube.com/watch?v=7bSMFpbcPiY)
📢 Regardez notre présentation récente lors de la conférence India FOSS 3.0 (en anglais) [ici](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](/assets/images/landing-2.png) <br /><br />
@ -57,7 +70,7 @@ sudo apt update
sudo apt install bruno
```
### Fonctionne sur de multiples platformes 🖥️
### Fonctionne sur de multiples plateformes 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
@ -85,7 +98,7 @@ Ou n'importe quel système de gestion de sources
### Soutien ❤️
Ouaf! Si vous aimez le projet, cliquez sur le bouton ⭐ !!
Si vous aimez Bruno et que vous souhaitez soutenir le travail _opensource_, pensez à devenir un sponsor via la page [Github Sponsors](https://github.com/sponsors/helloanoop).
### Partage de témoignages 📣
@ -93,7 +106,7 @@ Si Bruno vous a aidé dans votre travail, au sein de votre équipe, merci de pen
### Publier Bruno sur un nouveau gestionnaire de paquets
Veuillez regarder [ici](/publishing.md) pour plus d'information.
Veuillez regarder [ici](../publishing/publishing_fr.md) pour plus d'information.
### Contribuer 👩‍💻🧑‍💻
@ -129,4 +142,4 @@ Licence : CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### Licence 📄
[MIT](/license.md)
[MIT](../../license.md)

View File

@ -4,12 +4,30 @@
### Bruno - Opensource IDE per esplorare e testare gli APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](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)
| **Italiano**
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili.
Bruno memorizza le tue raccolte direttamente in una cartella del tuo filesystem. Utilizziamo un linguaggio di markup in testo semplice chiamato Bru per salvare informazioni sulle richeste API.
@ -83,11 +101,11 @@ Se Bruno ti ha aiutato con il tuo lavoro ed il tuo team, per favore non dimentic
### Pubblica Bruno su un nuovo gestore di pacchetti
Per favore vedi [qui](publishing.md) per accedere a più informazioni.
Per favore vedi [qui](../../publishing.md) per accedere a più informazioni.
### Contribuire 👩‍💻🧑‍💻
Sono felice che vuoi migliorare Bruno. Per favore controlla la [guida per la partecipazione](contributing.md)
Sono felice che vuoi migliorare Bruno. Per favore controlla la [guida per la partecipazione](../contributing/contributing_it.md)
Anche se non sei in grado di contribuire tramite il codice, non esitare a segnalare bug e richieste di funzionalità che devono essere implementati per risolvere il tuo caso d'uso.
@ -118,4 +136,4 @@ Il logo è stato creato da [OpenMoji](https://openmoji.org/library/emoji-1F436/)
### Licenza 📄
[MIT](license.md)
[MIT](../../license.md)

176
docs/readme/readme_ja.md Normal file
View File

@ -0,0 +1,176 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - API の検証・動作テストのためのオープンソース IDE.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](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)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| **日本語**
Bruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。
Bruno はローカルフォルダに直接コレクションを保存します。API リクエストの情報を保存するために Bru というプレーンテキストのマークアップ言語を採用しています。
Git や任意のバージョン管理システムを使って API コレクションを共同開発することもできます。
Bruno はオフラインのみで利用できます。Bruno にクラウド同期機能を追加する予定はありません。私たちはデータプライバシーを尊重しており、データはローカルに保存されるべきだと考えています。私たちの長期的なビジョンは[こちら](https://github.com/usebruno/bruno/discussions/269)をご覧ください。
[Bruno をダウンロード](https://www.usebruno.com/downloads)
📢 India FOSS 3.0 Conference での発表の様子は[こちら](https://www.youtube.com/watch?v=7bSMFpbcPiY)から
![bruno](/assets/images/landing-2.png) <br /><br />
### ゴールデンエディション ✨
機能のほとんどが無料で使用でき、オープンソースとなっています。
私たちは[オープンソースの原則と長期的な維持](https://github.com/usebruno/bruno/discussions/269)の間でうまくバランスを取ろうと努力しています。
[ゴールデンエディション](https://www.usebruno.com/pricing)を **19 ドル** (買い切り)で購入できます!
### インストール方法
Bruno は[私たちのウェブサイト](https://www.usebruno.com/downloads)からバイナリをダウンロードできます。Mac, Windows, Linux に対応しています。
Homebrew, Chocolatey, Scoop, Snap, Flatpak, Apt などのパッケージマネージャからもインストール可能です。
```sh
# MacでHomebrewを使ってインストール
brew install bruno
# WindowsでChocolateyを使ってインストール
choco install bruno
# WindowsでScoopを使ってインストール
scoop bucket add extras
scoop install bruno
# Windowsでwingetを使ってインストール
winget install Bruno.Bruno
# LinuxでSnapを使ってインストール
snap install bruno
# LinuxでFlatpakを使ってインストール
flatpak install com.usebruno.Bruno
# LinuxでAptを使ってインストール
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [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
```
### マルチプラットフォームでの実行に対応 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Git との連携が可能 👩‍💻🧑‍💻
または任意のバージョン管理システムにも対応しています。
![bruno](/assets/images/version-control.png) <br /><br />
### スポンサー
#### ゴールドスポンサー
<img src="../../assets/images/sponsors/samagata.png" width="150"/>
#### シルバースポンサー
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
#### ブロンズスポンサー
<a href="https://zuplo.link/bruno">
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
</a>
### 主要リンク 📌
- [私たちの長期ビジョン](https://github.com/usebruno/bruno/discussions/269)
- [ロードマップ](https://github.com/usebruno/bruno/discussions/384)
- [ドキュメント](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [ウェブサイト](https://www.usebruno.com)
- [料金設定](https://www.usebruno.com/pricing)
- [ダウンロード](https://www.usebruno.com/downloads)
- [Github スポンサー](https://github.com/sponsors/helloanoop).
### Showcase 🎥
- [体験談](https://github.com/usebruno/bruno/discussions/343)
- [ナレッジベース](https://github.com/usebruno/bruno/discussions/386)
- [スクリプト集](https://github.com/usebruno/bruno/discussions/385)
### サポート ❤️
もし Bruno を気に入っていただいて、オープンソースの活動を支援していただけるなら、[Github Sponsors](https://github.com/sponsors/helloanoop)でスポンサーになることを考えてみてください。
### 体験談のシェア 📣
Bruno が職場やチームで役立っているのであれば、[GitHub discussion 上であなたの体験談](https://github.com/usebruno/bruno/discussions/343)をシェアしていただくようお願いします。
### 新しいパッケージマネージャへの公開
詳しくは[こちら](../publishing/publishing_ja.md)をご覧ください。
### 連絡先 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](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/)でライセンスされています。
### 貢献するには 👩‍💻🧑‍💻
Bruno を改善していただけるのは歓迎です。[コントリビュートガイド](../contributing/contributing_ja.md)をご覧ください。
もしコードによる貢献ができない場合でも、あなたのユースケースを解決するために遠慮なくバグ報告や機能リクエストを出してください。
### 開発者
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### ライセンス 📄
[MIT](../../license.md)

View File

@ -4,12 +4,30 @@
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](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_bn.md)
| [Español](./readme_es.md)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다.
Bruno는 사용자의 컬렉션을 파일 시스템의 폴더에 직접 저장합니다. 일반 텍스트 마크업 언어인 Bru를 사용해 API 요청에 대한 정보를 저장합니다.
@ -87,7 +105,7 @@ Bruno가 여러분과 여러분의 팀에 도움이 되었다면, 잊지 말고
### 컨트리뷰트 👩‍💻🧑‍💻
컨트리뷰트에 관심이 있으시면 링크를 참고해 주세요. [컨트리뷰트 가이드](/docs/contributing/contributing_kr.md)
컨트리뷰트에 관심이 있으시면 링크를 참고해 주세요. [컨트리뷰트 가이드](../contributing/contributing_kr.md)
코드를 통해 기여할 수 없더라도 사용 사례를 해결하기 위해 구현이 필요한 버그나 기능 요청을 주저하지 마시고 제출해 주세요.
@ -118,4 +136,4 @@ The logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/).
### License 📄
[MIT](license.md)
[MIT](../../license.md)

View File

@ -1,18 +1,34 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - Otwartoźródłowe IDE do exploracji i testów APIs.
### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md)) | [한국어](docs/readme/readme_kr.md) ) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | **Polski** | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md)
[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)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| **Polski**
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowy przez Postman i podobne narzędzia.
Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman.
Bruno przechowuje twoje kolekcje bezpośrednio w folderze na twoim systemie plików. Używamy prostego języka znaczników, Bru, do zapisywania informacji o żądaniach API.
@ -41,14 +57,20 @@ choco install bruno
scoop bucket add extras
scoop install bruno
# On Windows via winget
winget install Bruno.Bruno
# On Linux via Snap
snap install bruno
# On Linux via Flatpak
flatpak install com.usebruno.Bruno
# On Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
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
@ -87,15 +109,15 @@ Jeśli podoba Ci się Bruno i chcesz wspierać naszą pracę opensource, rozważ
### Udostępnij Opinie 📣
Jeśli Bruno pomógł Tobie w pracy i Twoim zespołom, nie zapomnij podzielić się swoimi [opiniami na naszej dyskusji GitHub](https://github.com/usebruno/bruno/discussions/343)
Jeśli Bruno pomógł w pracy Tobie i Twoim zespołom, nie zapomnij podzielić się swoimi [opiniami na naszej dyskusji GitHub](https://github.com/usebruno/bruno/discussions/343)
### Publikowanie w Nowych Menedżerach Pakietów
Więcej informacji znajdziesz [tutaj](publishing.md).
Więcej informacji znajdziesz [tutaj](../publishing/publishing_pl.md).
### Współpraca 👩‍💻🧑‍💻
Cieszę się, że chcesz udoskonalić bruno. Proszę sprawdź [przewodnik współpracy](contributing.md)
Cieszymy się, że chcesz udoskonalić bruno. Proszę sprawdź [przewodnik współpracy](../contributing/contributing_pl.md)
Nawet jeśli nie jesteś w stanie przyczynić się poprzez kod, nie wahaj się zgłaszać błędów i wniosków o funkcje, które muszą zostać zaimplementowane, aby rozwiązać Twój przypadek użycia.
@ -126,4 +148,4 @@ Logo pochodzi z [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licencja:
### Licencja 📄
[MIT](license.md)
[MIT](../../license.md)

View File

@ -4,12 +4,30 @@
### Bruno - IDE de código aberto para explorar e testar APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](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_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes.
Bruno armazena suas coleções diretamente em uma pasta no seu sistema de arquivos. Utilizamos uma linguagem de marcação de texto simples, chamada Bru, para salvar informações sobre requisições de API.
@ -147,4 +165,4 @@ Mesmo que você não possa contribuir codificando, não deixe de relatar problem
### Licença 📄
[MIT](license.md)
[MIT](../../license.md)

View File

@ -4,13 +4,29 @@
### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/docs/readme/readme_ua.md) | [Русский](/docs/readme/readme_ru.md) | [Türkçe](/docs/readme/readme_tr.md) | [Deutsch](/docs/readme/readme_de.md) | [Français](/docs/readme/readme_fr.md) | [Português (BR)](/docs/readme/readme_pt_br.md)) | [한국어](/docs/readme/readme_kr.md) | [বাংলা](/docs/readme/readme_bn.md) | [Español](/docs/readme/readme_es.md) | [Italiano](/docs/readme/readme_it.md) | **Română** | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md)
[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)
| [Italiano](./readme_it.md)
| **Română**
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare.
@ -87,11 +103,11 @@ Dacă Bruno va ajutat la locul de muncă și la echipele dvs., vă rugăm să nu
### Publicarea la gestionari de pachete noi
Vă rugăm să citiţi [aici](/docs/publishing/publishing_ro.md) pentru mai multă informaţie.
Vă rugăm să citiţi [aici](../publishing/publishing_ro.md) pentru mai multă informaţie.
### Contribuiți 👩‍💻🧑‍💻
Mă bucur că doriți să îmbunătățiți Bruno. Vă rugăm să consultați [ghidul pentru contribuire](/docs/contributing/contributing_ro.md)
Mă bucur că doriți să îmbunătățiți Bruno. Vă rugăm să consultați [ghidul pentru contribuire](../contributing/contributing_ro.md)
Chiar dacă nu puteți face contribuții prin cod, vă rugăm să nu ezitați să raportați erori și să solicitați funcții care trebuie implementate pentru a rezolva cazul dvs. de utilizare.
@ -122,4 +138,4 @@ Logo-ul provine de la [OpenMoji](https://openmoji.org/library/emoji-1F436/). Lic
### Licența 📄
[MIT](license.md)
[MIT](../../license.md)

View File

@ -4,14 +4,29 @@
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | **Русский** | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md)
[English](../../readme.md)
| [Українська](./readme_ua.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)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.
@ -77,4 +92,4 @@ Bruno работает только в автономном режиме. Доб
### Лицензия 📄
[MIT](/license.md)
[MIT](../../license.md)

View File

@ -4,13 +4,29 @@
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
[![GitHub sürümü](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Web Sitesi](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![İndir](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | **Türkçe** | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md)
[English](../../readme.md)
| [Українська](./readme_ua.md)
| [Русский](./readme_ru.md)
| **Türkçe**
| [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)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir.
@ -91,11 +107,11 @@ Bruno işinizde ve ekiplerinizde size yardımcı olduysa, lütfen [github tartı
### Yeni Paket Yöneticilerine Yayınlama
Daha fazla bilgi için lütfen [buraya](publishing.md) bakın.
Daha fazla bilgi için lütfen [buraya](../publishing/publishing_tr.md) bakın.
### Katkıda Bulunun 👩‍💻🧑‍💻
Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzuna](contributing.md) göz atın
Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzuna](../contributing/contributing_tr.md) göz atın
Kod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek için uygulanması gereken hataları ve özellik isteklerini bildirmekten çekinmeyin.
@ -126,4 +142,4 @@ Logo [OpenMoji](https://openmoji.org/library/emoji-1F436/) adresinden alınmış
### Lisans 📄
[MIT](license.md)
[MIT](../../license.md)

View File

@ -4,13 +4,29 @@
### Bruno - IDE із відкритим кодом для тестування та дослідження API
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | **Українська** | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md)
[English](../../readme.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)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
@ -77,4 +93,4 @@ Bruno є повністю автономним. Немає жодних план
### Ліцензія 📄
[MIT](/license.md)
[MIT](../../license.md)

View File

@ -4,13 +4,29 @@
### Bruno - 探索和測試 API 的開源 IDE 工具
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](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) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | **正體中文**
[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)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| **正體中文**
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。

93
package-lock.json generated
View File

@ -9996,32 +9996,6 @@
"graphql": ">=0.11 <=16"
}
},
"node_modules/handlebars": {
"version": "4.7.8",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/handlebars/node_modules/minimist": {
"version": "1.2.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/har-schema": {
"version": "2.0.0",
"license": "ISC",
@ -10433,7 +10407,6 @@
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@ -12985,6 +12958,7 @@
},
"node_modules/neo-async": {
"version": "2.6.2",
"dev": true,
"license": "MIT"
},
"node_modules/new-github-issue-url": {
@ -16443,6 +16417,7 @@
},
"node_modules/source-map": {
"version": "0.6.1",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@ -17737,17 +17712,6 @@
"version": "1.0.6",
"license": "MIT"
},
"node_modules/uglify-js": {
"version": "3.17.4",
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/underscore": {
"version": "1.6.0"
},
@ -18363,10 +18327,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/wordwrap": {
"version": "1.0.0",
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"license": "MIT",
@ -18685,12 +18645,12 @@
},
"packages/bruno-cli": {
"name": "@usebruno/cli",
"version": "1.14.0",
"version": "1.16.0",
"license": "MIT",
"dependencies": {
"@aws-sdk/credential-providers": "3.525.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.11.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"aws4-axios": "^3.3.0",
"axios": "^1.5.1",
@ -19729,11 +19689,11 @@
},
"packages/bruno-electron": {
"name": "bruno",
"version": "v1.14.0",
"version": "v1.18.0",
"dependencies": {
"@aws-sdk/credential-providers": "3.525.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.11.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"@usebruno/schema": "0.7.0",
"about-window": "^1.15.2",
@ -19753,6 +19713,7 @@
"graphql": "^16.6.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"iconv-lite": "^0.6.3",
"is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
"json-bigint": "^1.0.0",
@ -20803,9 +20764,10 @@
},
"packages/bruno-js": {
"name": "@usebruno/js",
"version": "0.11.0",
"version": "0.12.0",
"license": "MIT",
"dependencies": {
"@usebruno/common": "0.1.0",
"@usebruno/query": "0.1.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@ -20815,7 +20777,6 @@
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1",
"handlebars": "^4.7.8",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
@ -24326,7 +24287,7 @@
"requires": {
"@aws-sdk/credential-providers": "3.525.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.11.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"aws4-axios": "^3.3.0",
"axios": "^1.5.1",
@ -25207,6 +25168,7 @@
"@usebruno/js": {
"version": "file:packages/bruno-js",
"requires": {
"@usebruno/common": "0.1.0",
"@usebruno/query": "0.1.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@ -25216,7 +25178,6 @@
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1",
"handlebars": "^4.7.8",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
@ -26138,7 +26099,7 @@
"requires": {
"@aws-sdk/credential-providers": "3.525.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.11.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"@usebruno/schema": "0.7.0",
"about-window": "^1.15.2",
@ -26162,6 +26123,7 @@
"graphql": "^16.6.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"iconv-lite": "^0.6.3",
"is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
"json-bigint": "^1.0.0",
@ -29284,21 +29246,6 @@
"graphql-ws": {
"version": "5.12.1"
},
"handlebars": {
"version": "4.7.8",
"requires": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"uglify-js": "^3.1.4",
"wordwrap": "^1.0.0"
},
"dependencies": {
"minimist": {
"version": "1.2.8"
}
}
},
"har-schema": {
"version": "2.0.0"
},
@ -29543,7 +29490,6 @@
},
"iconv-lite": {
"version": "0.6.3",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
@ -31169,7 +31115,8 @@
"version": "0.6.3"
},
"neo-async": {
"version": "2.6.2"
"version": "2.6.2",
"dev": true
},
"new-github-issue-url": {
"version": "0.2.1"
@ -33261,7 +33208,8 @@
}
},
"source-map": {
"version": "0.6.1"
"version": "0.6.1",
"dev": true
},
"source-map-js": {
"version": "1.0.2"
@ -34060,10 +34008,6 @@
"uc.micro": {
"version": "1.0.6"
},
"uglify-js": {
"version": "3.17.4",
"optional": true
},
"underscore": {
"version": "1.6.0"
},
@ -34458,9 +34402,6 @@
"version": "2.0.1",
"dev": true
},
"wordwrap": {
"version": "1.0.0"
},
"wrap-ansi": {
"version": "6.2.0",
"requires": {

View File

@ -22,7 +22,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, pkce } = oAuth;
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
const handleChange = (key, value) => {
dispatch(
@ -37,6 +37,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
clientId,
clientSecret,
scope,
state,
pkce,
[key]: value
}
@ -57,6 +58,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
clientId,
clientSecret,
scope,
state,
pkce: !Boolean(oAuth?.['pkce'])
}
})

View File

@ -22,6 +22,10 @@ const inputsConfig = [
{
key: 'scope',
label: 'Scope'
},
{
key: 'state',
label: 'State'
}
];

View File

@ -7,32 +7,90 @@ import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'path';
import slash from 'utils/common/slash';
const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef();
const formik = useFormik({
initialValues: {
domain: '',
type: 'cert',
certFilePath: '',
keyFilePath: '',
pfxFilePath: '',
passphrase: ''
},
validationSchema: Yup.object({
domain: Yup.string().required(),
certFilePath: Yup.string().required(),
keyFilePath: Yup.string().required(),
type: Yup.string().required().oneOf(['cert', 'pfx']),
certFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
then: Yup.string().min(1, 'certFilePath is a required field').required()
}),
keyFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
then: Yup.string().min(1, 'keyFilePath is a required field').required()
}),
pfxFilePath: Yup.string().when('type', {
is: (type) => type == 'pfx',
then: Yup.string().min(1, 'pfxFilePath is a required field').required()
}),
passphrase: Yup.string()
}),
onSubmit: (values) => {
onUpdate(values);
let relevantValues = {};
if (values.type === 'cert') {
relevantValues = {
domain: values.domain,
type: values.type,
certFilePath: values.certFilePath,
keyFilePath: values.keyFilePath,
passphrase: values.passphrase
};
} else {
relevantValues = {
domain: values.domain,
type: values.type,
pfxFilePath: values.pfxFilePath,
passphrase: values.passphrase
};
}
onUpdate(relevantValues);
formik.resetForm();
resetFileInputFields();
}
});
const getFile = (e) => {
formik.values[e.name] = e.files[0].path;
e.files?.[0]?.path && formik.setFieldValue(e.name, e.files?.[0]?.path);
};
const resetFileInputFields = () => {
certFilePathInputRef.current.value = '';
keyFilePathInputRef.current.value = '';
pfxFilePathInputRef.current.value = '';
};
const [passwordVisible, setPasswordVisible] = useState(false);
const handleTypeChange = (e) => {
formik.setFieldValue('type', e.target.value);
if (e.target.value === 'cert') {
formik.setFieldValue('pfxFilePath', '');
pfxFilePathInputRef.current.value = '';
} else {
formik.setFieldValue('certFilePath', '');
certFilePathInputRef.current.value = '';
formik.setFieldValue('keyFilePath', '');
keyFilePathInputRef.current.value = '';
}
};
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>
@ -75,17 +133,75 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label id="type-label" className="settings-label">
Type
</label>
<div className="flex items-center" aria-labelledby="type-label">
<label className="flex items-center cursor-pointer" htmlFor="cert">
<input
id="cert"
type="radio"
name="type"
value="cert"
checked={formik.values.type === 'cert'}
onChange={handleTypeChange}
className="mr-1"
/>
Cert
</label>
<label className="flex items-center ml-4 cursor-pointer" htmlFor="pfx">
<input
id="pfx"
type="radio"
name="type"
value="pfx"
checked={formik.values.type === 'pfx'}
onChange={handleTypeChange}
className="mr-1"
/>
PFX
</label>
</div>
</div>
{formik.values.type === 'cert' ? (
<>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="certFilePath">
Cert file
</label>
<div className="flex flex-row gap-2 justify-start">
<input
key="certFilePath"
id="certFilePath"
type="file"
name="certFilePath"
className="block non-passphrase-input"
className={`non-passphrase-input ${formik.values.certFilePath?.length ? 'hidden' : 'block'}`}
onChange={(e) => getFile(e.target)}
ref={certFilePathInputRef}
/>
{formik.values.certFilePath ? (
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.certFilePath))}
>
{path.basename(slash(formik.values.certFilePath))}
</div>
<IconTrash
size={18}
strokeWidth={1.5}
className="ml-2 cursor-pointer"
onClick={() => {
formik.setFieldValue('certFilePath', '');
certFilePathInputRef.current.value = '';
}}
/>
</div>
) : (
<></>
)}
</div>
{formik.touched.certFilePath && formik.errors.certFilePath ? (
<div className="ml-1 text-red-500">{formik.errors.certFilePath}</div>
) : null}
@ -94,17 +210,87 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
<label className="settings-label" htmlFor="keyFilePath">
Key file
</label>
<div className="flex flex-row gap-2">
<input
key="keyFilePath"
id="keyFilePath"
type="file"
name="keyFilePath"
className="block non-passphrase-input"
className={`non-passphrase-input ${formik.values.keyFilePath?.length ? 'hidden' : 'block'}`}
onChange={(e) => getFile(e.target)}
ref={keyFilePathInputRef}
/>
{formik.values.keyFilePath ? (
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.keyFilePath))}
>
{path.basename(slash(formik.values.keyFilePath))}
</div>
<IconTrash
size={18}
strokeWidth={1.5}
className="ml-2 cursor-pointer"
onClick={() => {
formik.setFieldValue('keyFilePath', '');
keyFilePathInputRef.current.value = '';
}}
/>
</div>
) : (
<></>
)}
</div>
{formik.touched.keyFilePath && formik.errors.keyFilePath ? (
<div className="ml-1 text-red-500">{formik.errors.keyFilePath}</div>
) : null}
</div>
</>
) : (
<>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="pfxFilePath">
PFX file
</label>
<div className="flex flex-row gap-2">
<input
key="pfxFilePath"
id="pfxFilePath"
type="file"
name="pfxFilePath"
className={`non-passphrase-input ${formik.values.pfxFilePath?.length ? 'hidden' : 'block'}`}
onChange={(e) => getFile(e.target)}
ref={pfxFilePathInputRef}
/>
{formik.values.pfxFilePath ? (
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.pfxFilePath))}
>
{path.basename(slash(formik.values.pfxFilePath))}
</div>
<IconTrash
size={18}
strokeWidth={1.5}
className="ml-2 cursor-pointer"
onClick={() => {
formik.setFieldValue('pfxFilePath', '');
pfxFilePathInputRef.current.value = '';
}}
/>
</div>
) : (
<></>
)}
</div>
{formik.touched.pfxFilePath && formik.errors.pfxFilePath ? (
<div className="ml-1 text-red-500">{formik.errors.pfxFilePath}</div>
) : null}
</div>
</>
)}
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="passphrase">
Passphrase

View File

@ -1,26 +1,10 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
function countRequests(items) {
let count = 0;
function recurse(item) {
if (item && typeof item === 'object') {
if (item.type !== 'folder') {
count++;
}
if (Array.isArray(item.items)) {
item.items.forEach(recurse);
}
}
}
items.forEach(recurse);
return count;
}
import { getTotalRequestCountInCollection } from 'utils/collections/';
const Info = ({ collection }) => {
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
return (
<StyledWrapper className="w-full flex flex-col h-full">
<div className="text-xs mb-4 text-muted">General information about the collection.</div>
@ -44,7 +28,7 @@ const Info = ({ collection }) => {
</tr>
<tr className="">
<td className="py-2 px-2 text-right">Requests&nbsp;:</td>
<td className="py-2 px-2">{countRequests(collection.items)}</td>
<td className="py-2 px-2">{totalRequestsInCollection}</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,56 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-header {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@ -0,0 +1,153 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addFolderHeader, updateFolderHeader, deleteFolderHeader } from 'providers/ReduxStore/slices/collections';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
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 addHeader = () => {
dispatch(
addFolderHeader({
collectionUid: collection.uid,
folderUid: folder.uid
})
);
};
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
header.name = e.target.value;
break;
}
case 'value': {
header.value = e.target.value;
break;
}
case 'enabled': {
header.enabled = e.target.checked;
break;
}
}
dispatch(
updateFolderHeader({
header: header,
collectionUid: collection.uid,
folderUid: folder.uid
})
);
};
const handleRemoveHeader = (header) => {
dispatch(
deleteFolderHeader({
headerUid: header.uid,
collectionUid: collection.uid,
folderUid: folder.uid
})
);
};
return (
<StyledWrapper className="w-full">
<div className="text-xs mb-4 text-muted">
Request headers that will be sent with every request inside this folder.
</div>
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td></td>
</tr>
</thead>
<tbody>
{headers && headers.length
? headers.map((header) => {
return (
<tr key={header.uid}>
<td>
<SingleLineEditor
value={header.name}
theme={storedTheme}
onSave={handleSave}
onChange={(newValue) =>
handleHeaderValueChange(
{
target: {
value: newValue
}
},
header,
'name'
)
}
autocomplete={headerAutoCompleteList}
collection={collection}
/>
</td>
<td>
<SingleLineEditor
value={header.value}
theme={storedTheme}
onSave={handleSave}
onChange={(newValue) =>
handleHeaderValueChange(
{
target: {
value: newValue
}
},
header,
'value'
)
}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={header.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Headers;

View File

@ -0,0 +1,13 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
height: inherit;
}
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,81 @@
import React 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 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 { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onRequestScriptEdit = (value) => {
dispatch(
updateFolderRequestScript({
script: value,
collectionUid: collection.uid,
folderUid: folder.uid
})
);
};
const onResponseScriptEdit = (value) => {
dispatch(
updateFolderResponseScript({
script: value,
collectionUid: collection.uid,
folderUid: folder.uid
})
);
};
const handleSave = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
return (
<StyledWrapper className="w-full flex flex-col h-full">
<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-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')}
/>
</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')}
/>
</div>
<div className="mt-12">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Script;

View File

@ -0,0 +1,46 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
div.tabs {
div.tab {
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: 1.25rem;
color: var(--color-tab-inactive);
cursor: pointer;
&:focus,
&:active,
&:focus-within,
&:focus-visible,
&:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
}
}
}
table {
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
li {
background-color: ${(props) => props.theme.bg} !important;
}
}
}
.muted {
color: ${(props) => props.theme.colors.text.muted};
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,5 @@
import styled from 'styled-components';
const StyledWrapper = styled.div``;
export default StyledWrapper;

View File

@ -0,0 +1,51 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateFolderTests } from 'providers/ReduxStore/slices/collections';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ collection, folder }) => {
const dispatch = useDispatch();
const tests = get(folder, 'root.request.tests', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
dispatch(
updateFolderTests({
tests: value,
collectionUid: collection.uid,
folderUid: folder.uid
})
);
};
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
return (
<StyledWrapper className="w-full flex flex-col h-full">
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>
<CodeEditor
collection={collection}
value={tests || ''}
theme={displayedTheme}
onEdit={onEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
/>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Tests;

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.title {
color: var(--color-tab-inactive);
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,56 @@
import styled from 'styled-components';
const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
}
}
.btn-add-var {
font-size: 0.8125rem;
}
input[type='text'] {
width: 100%;
border: solid 1px transparent;
outline: none !important;
background-color: inherit;
&:focus {
outline: none !important;
border: solid 1px transparent;
}
}
input[type='checkbox'] {
cursor: pointer;
position: relative;
top: 1px;
}
`;
export default Wrapper;

View File

@ -0,0 +1,161 @@
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex';
import { addFolderVar, deleteFolderVar, updateFolderVar } from 'providers/ReduxStore/slices/collections/index';
const VarsTable = ({ folder, collection, vars, varType }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addVar = () => {
dispatch(
addFolderVar({
collectionUid: collection.uid,
folderUid: folder.uid,
type: varType
})
);
};
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
const handleVarChange = (e, v, type) => {
const _var = cloneDeep(v);
switch (type) {
case 'name': {
const value = e.target.value;
if (variableNameRegex.test(value) === false) {
toast.error(
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
);
return;
}
_var.name = value;
break;
}
case 'value': {
_var.value = e.target.value;
break;
}
case 'enabled': {
_var.enabled = e.target.checked;
break;
}
}
dispatch(
updateFolderVar({
type: varType,
var: _var,
folderUid: folder.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveVar = (_var) => {
dispatch(
deleteFolderVar({
type: varType,
varUid: _var.uid,
folderUid: folder.uid,
collectionUid: collection.uid
})
);
};
return (
<StyledWrapper className="w-full">
<table>
<thead>
<tr>
<td>Name</td>
{varType === 'request' ? (
<td>
<div className="flex items-center">
<span>Value</span>
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" />
</div>
</td>
) : (
<td>
<div className="flex items-center">
<span>Expr</span>
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" />
</div>
</td>
)}
<td></td>
</tr>
</thead>
<tbody>
{vars && vars.length
? vars.map((_var) => {
return (
<tr key={_var.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={_var.name}
className="mousetrap"
onChange={(e) => handleVarChange(e, _var, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={_var.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleVarChange(
{
target: {
value: newValue
}
},
_var,
'value'
)
}
collection={collection}
/>
</td>
<td>
<div className="flex items-center">
<input
type="checkbox"
checked={_var.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, _var, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
</td>
</tr>
);
})
: null}
</tbody>
</table>
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={addVar}>
+ Add
</button>
</StyledWrapper>
);
};
export default VarsTable;

View File

@ -0,0 +1,32 @@
import React from 'react';
import get from 'lodash/get';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
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 handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<VarsTable folder={folder} collection={collection} vars={requestVars} varType="request" />
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<VarsTable folder={folder} collection={collection} vars={responseVars} varType="response" />
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Vars;

View File

@ -0,0 +1,75 @@
import React from 'react';
import classnames from 'classnames';
import { updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import Headers from './Headers';
import Script from './Script';
import Tests from './Tests';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars';
const FolderSettings = ({ collection, folder }) => {
const dispatch = useDispatch();
let tab = 'headers';
const { folderLevelSettingsSelectedTab } = collection;
if (folderLevelSettingsSelectedTab?.[folder.uid]) {
tab = folderLevelSettingsSelectedTab[folder.uid];
}
const setTab = (tab) => {
dispatch(
updatedFolderSettingsSelectedTab({
collectionUid: collection.uid,
folderUid: folder.uid,
tab
})
);
};
const getTabPanel = (tab) => {
switch (tab) {
case 'headers': {
return <Headers collection={collection} folder={folder} />;
}
case 'script': {
return <Script collection={collection} folder={folder} />;
}
case 'test': {
return <Tests collection={collection} folder={folder} />;
}
case 'vars': {
return <Vars collection={collection} folder={folder} />;
}
}
};
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
active: tabName === tab
});
};
return (
<StyledWrapper>
<div className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
Headers
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
Script
</div>
<div className={getTabClassname('test')} role="tab" onClick={() => setTab('test')}>
Test
</div>
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
Vars
</div>
</div>
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
</div>
</StyledWrapper>
);
};
export default FolderSettings;

View File

@ -47,7 +47,7 @@ const General = ({ close }) => {
filePath: get(preferences, 'request.customCaCertificate.filePath', null)
},
keepDefaultCaCertificates: {
enabled: get(preferences, 'request.keepDefaultCaCertificates.enabled', false)
enabled: get(preferences, 'request.keepDefaultCaCertificates.enabled', true)
},
timeout: preferences.request.timeout,
storeCookies: get(preferences, 'request.storeCookies', true),

View File

@ -32,6 +32,7 @@ import lightTheme from 'themes/light';
* isNumber : is number
* isString : is string
* isBoolean : is boolean
* isArray : is array
*/
const AssertionOperator = ({ operator, onChange }) => {
@ -61,7 +62,8 @@ const AssertionOperator = ({ operator, onChange }) => {
'isJson',
'isNumber',
'isString',
'isBoolean'
'isBoolean',
'isArray'
];
const handleChange = (e) => {

View File

@ -33,6 +33,7 @@ import { useTheme } from 'providers/Theme';
* isNumber : is number
* isString : is string
* isBoolean : is boolean
* isArray : is array
*/
const parseAssertionOperator = (str = '') => {
if (!str || typeof str !== 'string' || !str.length) {
@ -68,7 +69,8 @@ const parseAssertionOperator = (str = '') => {
'isJson',
'isNumber',
'isString',
'isBoolean'
'isBoolean',
'isArray'
];
const unaryOperators = [
@ -81,7 +83,8 @@ const parseAssertionOperator = (str = '') => {
'isJson',
'isNumber',
'isString',
'isBoolean'
'isBoolean',
'isArray'
];
const [operator, ...rest] = str.trim().split(' ');
@ -118,7 +121,8 @@ const isUnaryOperator = (operator) => {
'isJson',
'isNumber',
'isString',
'isBoolean'
'isBoolean',
'isArray'
];
return unaryOperators.includes(operator);

View File

@ -22,7 +22,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, pkce } = oAuth;
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
const handleChange = (key, value) => {
dispatch(
@ -37,6 +37,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
accessTokenUrl,
clientId,
clientSecret,
state,
scope,
pkce,
[key]: value
@ -58,6 +59,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
accessTokenUrl,
clientId,
clientSecret,
state,
scope,
pkce: !Boolean(oAuth?.['pkce'])
}

View File

@ -22,6 +22,10 @@ const inputsConfig = [
{
key: 'scope',
label: 'Scope'
},
{
key: 'state',
label: 'State'
}
];

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { buildClientSchema } from 'graphql';
import { buildClientSchema, buildSchema } from 'graphql';
import { fetchGqlSchema } from 'utils/network';
import { simpleHash } from 'utils/common';
import { simpleHash, safeParseJSON } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
@ -18,7 +18,12 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => {
if (!saved) {
return null;
}
return buildClientSchema(JSON.parse(saved));
let parsedData = safeParseJSON(saved);
if (typeof parsedData === 'object') {
return buildClientSchema(parsedData);
} else {
return buildSchema(parsedData);
}
} catch {
localStorage.setItem(localStorageKey, null);
return null;
@ -48,7 +53,7 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => {
return;
}
setSchemaSource('file');
return schemaContent.data;
return schemaContent?.data || schemaContent;
};
const loadSchema = async (schemaSource) => {
@ -66,11 +71,18 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => {
// fallback to introspection if source is unknown
data = await loadSchemaFromIntrospection();
}
if (data) {
if (typeof data === 'object') {
setSchema(buildClientSchema(data));
} else {
setSchema(buildSchema(data));
}
localStorage.setItem(localStorageKey, JSON.stringify(data));
toast.success('GraphQL Schema loaded successfully');
}
} catch (err) {
setError(err);
console.error(err);
toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`);
}

View File

@ -1,5 +1,4 @@
import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
@ -14,7 +13,7 @@ import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script';
import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
import { get } from 'lodash';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
@ -81,6 +80,8 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
});
};
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
// get the length of active params, headers, asserts and vars
const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []);
const headers = item.draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []);
@ -99,7 +100,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
Query
Params
{activeParamsLength > 0 && <sup className="ml-1 font-medium">{activeParamsLength}</sup>}
</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
@ -136,9 +137,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
) : null}
</div>
<section
className={`flex w-full ${
['script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'
}`}
className={classnames('flex w-full', {
'mt-5': !isMultipleContentTab
})}
>
{getTabPanel(focusedTab.requestPaneTab)}
</section>

View File

@ -1,6 +1,9 @@
import styled from 'styled-components';
const Wrapper = styled.div`
div.title {
color: var(--color-tab-inactive);
}
table {
width: 100%;
border-collapse: collapse;

View File

@ -1,12 +1,18 @@
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import has from 'lodash/has';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
import {
addQueryParam,
deleteQueryParam,
updatePathParam,
updateQueryParam
} from 'providers/ReduxStore/slices/collections';
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
@ -14,8 +20,10 @@ const QueryParams = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
const queryParams = params.filter((param) => param.type === 'query');
const pathParams = params.filter((param) => param.type === 'path');
const handleAddParam = () => {
const handleAddQueryParam = () => {
dispatch(
addQueryParam({
itemUid: item.uid,
@ -26,34 +34,63 @@ const QueryParams = ({ item, collection }) => {
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleParamChange = (e, _param, type) => {
const param = cloneDeep(_param);
switch (type) {
const handleQueryParamChange = (e, data, key) => {
let value;
switch (key) {
case 'name': {
param.name = e.target.value;
value = e.target.value;
break;
}
case 'value': {
param.value = e.target.value;
value = e.target.value;
break;
}
case 'enabled': {
param.enabled = e.target.checked;
value = e.target.checked;
break;
}
}
let queryParam = cloneDeep(data);
if (queryParam[key] === value) {
return;
}
queryParam[key] = value;
dispatch(
updateQueryParam({
param,
queryParam,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveParam = (param) => {
const handlePathParamChange = (e, data) => {
let value = e.target.value;
let pathParam = cloneDeep(data);
if (pathParam['value'] === value) {
return;
}
pathParam['value'] = value;
dispatch(
updatePathParam({
pathParam,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const handleRemoveQueryParam = (param) => {
dispatch(
deleteQueryParam({
paramUid: param.uid,
@ -64,7 +101,9 @@ const QueryParams = ({ item, collection }) => {
};
return (
<StyledWrapper className="w-full">
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Query</div>
<table>
<thead>
<tr>
@ -74,8 +113,8 @@ const QueryParams = ({ item, collection }) => {
</tr>
</thead>
<tbody>
{params && params.length
? params.map((param, index) => {
{queryParams && queryParams.length
? queryParams.map((param, index) => {
return (
<tr key={param.uid}>
<td>
@ -87,7 +126,7 @@ const QueryParams = ({ item, collection }) => {
spellCheck="false"
value={param.name}
className="mousetrap"
onChange={(e) => handleParamChange(e, param, 'name')}
onChange={(e) => handleQueryParamChange(e, param, 'name')}
/>
</td>
<td>
@ -96,7 +135,7 @@ const QueryParams = ({ item, collection }) => {
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handleParamChange(
handleQueryParamChange(
{
target: {
value: newValue
@ -117,9 +156,9 @@ const QueryParams = ({ item, collection }) => {
checked={param.enabled}
tabIndex="-1"
className="mr-3 mousetrap"
onChange={(e) => handleParamChange(e, param, 'enabled')}
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
/>
<button tabIndex="-1" onClick={() => handleRemoveParam(param)}>
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</div>
@ -130,9 +169,60 @@ const QueryParams = ({ item, collection }) => {
: null}
</tbody>
</table>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddParam}>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
+&nbsp;<span>Add Param</span>
</button>
<div className="mb-1 title text-xs">Path</div>
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
</tr>
</thead>
<tbody>
{pathParams && pathParams.length
? pathParams.map((path, index) => {
return (
<tr key={path.uid}>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={path.name}
className="mousetrap"
readOnly={true}
/>
</td>
<td>
<SingleLineEditor
value={path.value}
theme={storedTheme}
onSave={onSave}
onChange={(newValue) =>
handlePathParamChange(
{
target: {
value: newValue
}
},
path
)
}
onRun={handleRun}
collection={collection}
/>
</td>
</tr>
);
})
: null}
</tbody>
</table>
</div>
</StyledWrapper>
);
};

View File

@ -69,6 +69,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
onChange={(newValue) => onUrlChange(newValue)}
onRun={handleRun}
collection={collection}
item={item}
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<div

View File

@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
import FolderSettings from 'components/FolderSettings';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
@ -131,6 +132,10 @@ const RequestTabPanel = () => {
if (focusedTab.type === 'collection-settings') {
return <CollectionSettings collection={collection} />;
}
if (focusedTab.type === 'folder-settings') {
const folder = findItemInCollection(collection, focusedTab.folderUid);
return <FolderSettings collection={collection} folder={folder} />;
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {

View File

@ -44,7 +44,7 @@ const CollectionToolBar = ({ collection }) => {
<div className="flex items-center p-2">
<div className="flex flex-1 items-center cursor-pointer hover:underline" onClick={viewCollectionSettings}>
<IconFiles size={18} strokeWidth={1.5} />
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
<span className="ml-2 mr-4 font-semibold">{collection?.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<span className="mr-2">

View File

@ -1,22 +1,30 @@
import React from 'react';
import { IconVariable, IconSettings, IconRun } from '@tabler/icons';
import { IconVariable, IconSettings, IconRun, IconFolder } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type }) => {
const getTabInfo = (type) => {
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
const getTabInfo = (type, tabName) => {
switch (type) {
case 'collection-settings': {
return (
<>
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Collection</span>
<span className="ml-1 leading-6">Collection</span>
</>
);
}
case 'folder-settings': {
return (
<div className="flex items-center flex-nowrap overflow-hidden">
<IconFolder size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
<span className="ml-1 leading-6 truncate">{tabName || 'Folder'}</span>
</div>
);
}
case 'variables': {
return (
<>
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Variables</span>
<span className="ml-1 leading-6">Variables</span>
</>
);
}
@ -24,7 +32,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
return (
<>
<IconRun size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Runner</span>
<span className="ml-1 leading-6">Runner</span>
</>
);
}
@ -33,7 +41,7 @@ const SpecialTab = ({ handleCloseClick, type }) => {
return (
<>
<div className="flex items-center tab-label pl-2">{getTabInfo(type)}</div>
<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)}>
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
<path

View File

@ -13,7 +13,7 @@ import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
const RequestTab = ({ tab, collection }) => {
const RequestTab = ({ tab, collection, folderUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
@ -80,11 +80,15 @@ const RequestTab = ({ tab, collection }) => {
return color;
};
if (['collection-settings', 'variables', 'collection-runner'].includes(tab.type)) {
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
{tab.type === 'folder-settings' ? (
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} tabName={folder?.name} />
) : (
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} />
)}
</StyledWrapper>
);
}

View File

@ -75,7 +75,6 @@ const RequestTabs = () => {
'has-chevrons': showChevrons
});
};
// Todo: Must support ephemeral requests
return (
<StyledWrapper className={getRootClassname()}>
@ -111,7 +110,7 @@ const RequestTabs = () => {
role="tab"
onClick={() => handleClick(tab)}
>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} folderUid={tab.folderUid} />
</li>
);
})

View File

@ -9,6 +9,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard';
import toast from 'react-hot-toast';
import { IconCopy } from '@tabler/icons';
import { findCollectionByItemUid } from '../../../../../../../utils/collections/index';
import { getAuthHeaders } from '../../../../../../../utils/codegenerator/auth';
const CodeView = ({ language, item }) => {
const { displayedTheme } = useTheme();
@ -20,10 +21,16 @@ const CodeView = ({ language, item }) => {
item.uid
);
const headers = [...(collection?.root?.request?.headers || []), ...(requestHeaders || [])];
const collectionRootAuth = collection?.root?.request?.auth;
const requestAuth = item.draft ? get(item, 'draft.request.auth') : get(item, 'request.auth');
const headers = [
...getAuthHeaders(collectionRootAuth, requestAuth),
...(collection?.root?.request?.headers || []),
...(requestHeaders || [])
];
let snippet = '';
try {
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client);
} catch (e) {
@ -43,6 +50,7 @@ const CodeView = ({ language, item }) => {
</CopyToClipboard>
<CodeEditor
readOnly
collection={collection}
value={snippet}
font={get(preferences, 'font.codeFont', 'default')}
theme={displayedTheme}

View File

@ -2,30 +2,10 @@ import Modal from 'components/Modal/index';
import { useState } from 'react';
import CodeView from './CodeView';
import StyledWrapper from './StyledWrapper';
import { isValidUrl } from 'utils/url/index';
import get from 'lodash/get';
import { isValidUrl } from 'utils/url';
import { find, get } from 'lodash';
import { findEnvironmentInCollection } from 'utils/collections';
// Todo: Fix this
// import { interpolate } from '@usebruno/common';
import brunoCommon from '@usebruno/common';
const { interpolate } = brunoCommon;
const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
if (!url || !url.length || typeof url !== 'string') {
return;
}
return interpolate(url, {
...envVars,
...collectionVariables,
process: {
env: {
...processEnvVars
}
}
});
};
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
const languages = [
{
@ -76,7 +56,6 @@ const languages = [
];
const GenerateCodeItem = ({ collection, item, onClose }) => {
const url = get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
let envVars = {};
if (environment) {
@ -87,12 +66,23 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
}, {});
}
const requestUrl =
get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
// interpolate the url
const interpolatedUrl = interpolateUrl({
url,
url: requestUrl,
envVars,
collectionVariables: collection.collectionVariables,
processEnvVars: collection.processEnvVariables
});
// interpolate the path params
const finalUrl = interpolateUrlPathParams(
interpolatedUrl,
get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params')
);
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
return (
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
@ -116,7 +106,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
</div>
</div>
<div className="flex-grow p-4">
{isValidUrl(interpolatedUrl) ? (
{isValidUrl(finalUrl) ? (
<CodeView
language={selectedLanguage}
item={{
@ -125,18 +115,18 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
item.request.url !== ''
? {
...item.request,
url: interpolatedUrl
url: finalUrl
}
: {
...item.draft.request,
url: interpolatedUrl
url: finalUrl
}
}}
/>
) : (
<div className="flex flex-col justify-center items-center w-full">
<div className="text-center">
<h1 className="text-2xl font-bold">Invalid URL: {interpolatedUrl}</h1>
<h1 className="text-2xl font-bold">Invalid URL: {finalUrl}</h1>
<p className="text-gray-500">Please check the URL and try again</p>
</div>
</div>

View File

@ -24,6 +24,7 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import NetworkError from 'components/ResponsePane/NetworkError/index';
import { uuid } from 'utils/common';
const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs);
@ -188,6 +189,16 @@ const CollectionItem = ({ item, collection, searchText }) => {
toast.error('URL is required');
}
};
const viewFolderSettings = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
folderUid: item.uid,
type: 'folder-settings'
})
);
};
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
@ -345,6 +356,17 @@ const CollectionItem = ({ item, collection, searchText }) => {
>
Delete
</div>
{isFolder && (
<div
className="dropdown-item"
onClick={(e) => {
dropdownTippyRef.current.hide();
viewFolderSettings();
}}
>
Settings
</div>
)}
</Dropdown>
</div>
</div>

View File

@ -17,32 +17,32 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
});
const handleImportBrunoCollection = () => {
importBrunoCollection()
.then((collection) => {
handleSubmit(collection);
.then(({ collection }) => {
handleSubmit({ collection });
})
.catch((err) => toastError(err, 'Import collection failed'));
};
const handleImportPostmanCollection = () => {
importPostmanCollection(options)
.then((collection) => {
handleSubmit(collection);
.then(({ collection, translationLog }) => {
handleSubmit({ collection, translationLog });
})
.catch((err) => toastError(err, 'Postman Import collection failed'));
};
const handleImportInsomniaCollection = () => {
importInsomniaCollection()
.then((collection) => {
handleSubmit(collection);
.then(({ collection }) => {
handleSubmit({ collection });
})
.catch((err) => toastError(err, 'Insomnia Import collection failed'));
};
const handleImportOpenapiCollection = () => {
importOpenapiCollection()
.then((collection) => {
handleSubmit(collection);
.then(({ collection }) => {
handleSubmit({ collection });
})
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
};

View File

@ -1,11 +1,108 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import Modal from 'components/Modal';
import { IconAlertTriangle, IconArrowRight, IconCaretDown, IconCaretRight, IconCopy } from '@tabler/icons';
import toast from 'react-hot-toast';
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
const TranslationLog = ({ translationLog }) => {
const [showDetails, setShowDetails] = useState(false);
const preventSetShowDetails = (e) => {
e.stopPropagation();
e.preventDefault();
setShowDetails(!showDetails);
};
const copyClipboard = (e, value) => {
e.stopPropagation();
e.preventDefault();
navigator.clipboard.writeText(value);
toast.success('Copied to clipboard');
};
return (
<div className="flex flex-col mt-2">
<div className="border-l-2 border-amber-500 dark:border-amber-300 bg-amber-50 dark:bg-amber-50/10 p-1.5 rounded-r">
<div className="flex items-center">
<div className="flex-shrink-0">
<IconAlertTriangle className="h-4 w-4 text-amber-500 dark:text-amber-300" aria-hidden="true" />
</div>
<div className="ml-2">
<p className="text-xs text-amber-700 dark:text-amber-300">
<span className="font-semibold">Warning:</span> Some commands were not translated.{' '}
</p>
</div>
</div>
</div>
<button
onClick={(e) => preventSetShowDetails(e)}
className="flex w-fit items-center rounded px-2.5 py-1 mt-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
>
See details
{showDetails ? <IconCaretDown size={16} className="ml-1" /> : <IconCaretRight size={16} className="ml-1" />}
</button>
{showDetails && (
<div className="flex relative flex-col text-xs max-w-[364px] max-h-[300px] overflow-scroll mt-2 p-2 bg-slate-50 dark:bg-slate-400/10 ring-1 ring-inset rounded text-slate-700 dark:text-slate-300 ring-slate-600/20 dark:ring-slate-400/20">
<span className="font-semibold flex items-center">
Impacted Collections: {Object.keys(translationLog || {}).length}
</span>
<span className="font-semibold flex items-center">
Impacted Lines:{' '}
{Object.values(translationLog || {}).reduce(
(acc, curr) => acc + (curr.script?.length || 0) + (curr.test?.length || 0),
0
)}
</span>
<span className="my-1">
The numbers after 'script' and 'test' indicate the line numbers of incomplete translations.
</span>
<ul>
{Object.entries(translationLog || {}).map(([name, value]) => (
<li key={name} className="list-none text-xs font-semibold">
<div className="font-semibold flex items-center text-xs whitespace-nowrap">
<IconCaretRight className="min-w-4 max-w-4 -ml-1" />
{name}
</div>
<div className="flex flex-col">
{value.script && (
<div className="flex items-center text-xs font-light mb-1 flex-wrap">
<span className="mr-2">script :</span>
{value.script.map((scriptValue, index) => (
<span className="flex items-center" key={`script_${name}_${index}`}>
<span className="text-xs font-light">{scriptValue}</span>
{index < value.script.length - 1 && <> - </>}
</span>
))}
</div>
)}
{value.test && (
<div className="flex items-center text-xs font-light mb-1 flex-wrap">
<span className="mr-2">test :</span>
{value.test.map((testValue, index) => (
<div className="flex items-center" key={`test_${name}_${index}`}>
<span className="text-xs font-light">{testValue}</span>
{index < value.test.length - 1 && <> - </>}
</div>
))}
</div>
)}
</div>
</li>
))}
</ul>
<button
className="absolute top-1 right-1 flex w-fit items-center rounded p-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
onClick={(e) => copyClipboard(e, JSON.stringify(translationLog))}
>
<IconCopy size={16} />
</button>
</div>
)}
</div>
);
};
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, translationLog }) => {
const inputRef = useRef();
const dispatch = useDispatch();
@ -24,7 +121,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
handleSubmit(values.collectionLocation);
}
});
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
@ -52,7 +148,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
Name
</label>
<div className="mt-2">{collectionName}</div>
{translationLog && Object.keys(translationLog).length > 0 && (
<TranslationLog translationLog={translationLog} />
)}
<>
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
Location

View File

@ -14,23 +14,34 @@ import StyledWrapper from './StyledWrapper';
const TitleBar = () => {
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
const dispatch = useDispatch();
const { ipcRenderer } = window;
const handleImportCollection = (collection) => {
const handleImportCollection = ({ collection, translationLog }) => {
setImportedCollection(collection);
if (translationLog) {
setImportedTranslationLog(translationLog);
}
setImportCollectionModalOpen(false);
setImportCollectionLocationModalOpen(true);
};
const handleImportCollectionLocation = (collectionLocation) => {
dispatch(importCollection(importedCollection, collectionLocation));
dispatch(importCollection(importedCollection, collectionLocation))
.then(() => {
setImportCollectionLocationModalOpen(false);
setImportedCollection(null);
toast.success('Collection imported successfully');
})
.catch((err) => {
setImportCollectionLocationModalOpen(false);
console.error(err);
toast.error('An error occurred while importing the collection. Check the logs for more information.');
});
};
const menuDropdownTippyRef = useRef();
@ -64,6 +75,7 @@ const TitleBar = () => {
{importCollectionLocationModalOpen ? (
<ImportCollectionLocation
collectionName={importedCollection.name}
translationLog={importedTranslationLog}
onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation}
/>

View File

@ -129,7 +129,7 @@ const Sidebar = () => {
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.16.1</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.20.0</div>
</div>
</div>
</div>

View File

@ -24,13 +24,15 @@ class SingleLineEditor extends Component {
componentDidMount() {
// Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */
const variables = getAllVariables(this.props.collection, this.props.item);
this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false,
lineNumbers: false,
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
mode: 'brunovariables',
brunoVarInfo: {
variables: getAllVariables(this.props.collection)
variables
},
scrollbarStyle: null,
tabindex: 0,
@ -82,7 +84,7 @@ class SingleLineEditor extends Component {
});
if (this.props.autocomplete) {
this.editor.on('keyup', (cm, event) => {
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.key !== 'Enter') {
/*Enter - do not open autocomplete list just after item has been selected in it*/
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
}
@ -90,7 +92,7 @@ class SingleLineEditor extends Component {
}
this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
this.addOverlay();
this.addOverlay(variables);
}
_onEdit = () => {
@ -108,10 +110,10 @@ class SingleLineEditor extends Component {
// event loop.
this.ignoreChangeEvent = true;
let variables = getAllVariables(this.props.collection);
let variables = getAllVariables(this.props.collection, this.props.item);
if (!isEqual(variables, this.variables)) {
this.editor.options.brunoVarInfo.variables = variables;
this.addOverlay();
this.addOverlay(variables);
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
@ -127,12 +129,10 @@ class SingleLineEditor extends Component {
this.editor.getWrapperElement().remove();
}
addOverlay = () => {
let variables = getAllVariables(this.props.collection);
addOverlay = (variables) => {
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
this.editor.setOption('mode', 'brunovariables');
this.editor.setOption('mode', 'combinedmode');
};
render() {

View File

@ -13,6 +13,7 @@ import StyledWrapper from './StyledWrapper';
const Welcome = () => {
const dispatch = useDispatch();
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
@ -23,17 +24,27 @@ const Welcome = () => {
);
};
const handleImportCollection = (collection) => {
const handleImportCollection = ({ collection, translationLog }) => {
setImportedCollection(collection);
if (translationLog) {
setImportedTranslationLog(translationLog);
}
setImportCollectionModalOpen(false);
setImportCollectionLocationModalOpen(true);
};
const handleImportCollectionLocation = (collectionLocation) => {
dispatch(importCollection(importedCollection, collectionLocation));
dispatch(importCollection(importedCollection, collectionLocation))
.then(() => {
setImportCollectionLocationModalOpen(false);
setImportedCollection(null);
toast.success('Collection imported successfully');
})
.catch((err) => {
setImportCollectionLocationModalOpen(false);
console.error(err);
toast.error('An error occurred while importing the collection. Check the logs for more information.');
});
};
return (
@ -44,13 +55,14 @@ const Welcome = () => {
) : null}
{importCollectionLocationModalOpen ? (
<ImportCollectionLocation
translationLog={importedTranslationLog}
collectionName={importedCollection.name}
onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation}
/>
) : null}
<div className="">
<div>
<Bruno width={50} />
</div>
<div className="text-xl font-semibold select-none">bruno</div>

View File

@ -6,7 +6,7 @@ export default function Home() {
return (
<div>
<Head>
<title>bruno</title>
<title>Bruno</title>
<link rel="icon" href="/favicon.ico" />
</Head>

View File

@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
version: '1.16.1'
version: '1.20.0'
}
});
};

View File

@ -18,7 +18,7 @@ const initialState = {
filePath: null
},
keepDefaultCaCertificates: {
enabled: false
enabled: true
},
timeout: 0
},

View File

@ -14,10 +14,10 @@ import {
findParentItemInCollection,
getItemsToResequence,
isItemAFolder,
refreshUidsInItem,
isItemARequest,
moveCollectionItem,
moveCollectionItemToRootOfCollection,
refreshUidsInItem,
transformRequestToSaveToFilesystem
} from 'utils/collections';
import { uuid, waitForNextTick } from 'utils/common';
@ -41,6 +41,7 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
import { name } from 'file-loader';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@ -143,7 +144,42 @@ export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
});
};
export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getState) => {
export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
const folder = findItemInCollection(collection, folderUid);
return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}
if (!folder) {
return reject(new Error('Folder not found'));
}
console.log(collection);
const { ipcRenderer } = window;
const folderData = {
name: folder.name,
pathname: folder.pathname,
root: folder.root
};
console.log(folderData);
ipcRenderer
.invoke('renderer:save-folder-root', folderData)
.then(() => toast.success('Folder Settings saved successfully'))
.then(resolve)
.catch((err) => {
toast.error('Failed to save folder settings!');
reject(err);
});
});
};
export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@ -156,7 +192,10 @@ export const sendCollectionOauth2Request = (collectionUid) => (dispatch, getStat
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables)
const externalSecrets = getExternalCollectionSecretsForActiveEnvironment({ collection });
const secretVariables = getFormattedCollectionSecretVariables({ externalSecrets });
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables, itemUid, secretVariables)
.then((response) => {
if (response?.data?.error) {
toast.error(response?.data?.error);
@ -184,9 +223,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const itemCopy = cloneDeep(item || {});
const collectionCopy = cloneDeep(collection);
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
sendNetworkRequest(itemCopy, collection, environment, collectionCopy.collectionVariables)
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.collectionVariables)
.then((response) => {
return dispatch(
responseReceived({

View File

@ -1,13 +1,7 @@
import { uuid } from 'utils/common';
import path from 'path';
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, debounce } from 'lodash';
import { createSlice } from '@reduxjs/toolkit';
import cloneDeep from 'lodash/cloneDeep';
import concat from 'lodash/concat';
import each from 'lodash/each';
import filter from 'lodash/filter';
import find from 'lodash/find';
import forOwn from 'lodash/forOwn';
import get from 'lodash/get';
import map from 'lodash/map';
import set from 'lodash/set';
import {
addDepth,
areItemsTheSameExceptSeqUpdate,
@ -21,9 +15,9 @@ import {
findItemInCollectionByPathname,
isItemARequest
} from 'utils/collections';
import { uuid } from 'utils/common';
import { PATH_SEPARATOR, getDirectoryName, getSubdirectoriesFromRoot } from 'utils/common/platform';
import { parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform';
import toast from 'react-hot-toast';
const initialState = {
collections: [],
@ -40,6 +34,8 @@ export const collectionsSlice = createSlice({
collection.settingsSelectedTab = 'headers';
collection.folderLevelSettingsSelectedTab = {};
// TODO: move this to use the nextAction approach
// last action is used to track the last action performed on the collection
// this is optional
@ -96,7 +92,7 @@ export const collectionsSlice = createSlice({
}
},
updateSettingsSelectedTab: (state, action) => {
const { collectionUid, tab } = action.payload;
const { collectionUid, folderUid, tab } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@ -104,6 +100,19 @@ export const collectionsSlice = createSlice({
collection.settingsSelectedTab = tab;
}
},
updatedFolderSettingsSelectedTab: (state, action) => {
const { collectionUid, folderUid, tab } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
const folder = findItemInCollection(collection, folderUid);
if (folder) {
collection.folderLevelSettingsSelectedTab[folderUid] = tab;
}
}
},
collectionUnlinkEnvFileEvent: (state, action) => {
const { data: environment, meta } = action.payload;
const collection = findCollectionByUid(state.collections, meta.collectionUid);
@ -294,10 +303,35 @@ export const collectionsSlice = createSlice({
if (collection && collection.items && collection.items.length) {
const parts = splitOnFirst(action.payload.requestUrl, '?');
const params = parseQueryParams(parts[1]);
each(params, (urlParam) => {
urlParam.enabled = true;
});
const queryParams = parseQueryParams(parts[1]);
let pathParams = [];
try {
pathParams = parsePathParams(parts[0]);
} catch (err) {
console.error(err);
toast.error(err.message);
}
const queryParamObjects = queryParams.map((param) => ({
uid: uuid(),
name: param.key,
value: param.value,
description: '',
type: 'query',
enabled: true
}));
const pathParamObjects = pathParams.map((param) => ({
uid: uuid(),
name: param.key,
value: param.value,
description: '',
type: 'path',
enabled: true
}));
const params = [...queryParamObjects, ...pathParamObjects];
const item = {
uid: action.payload.uid,
@ -349,27 +383,59 @@ export const collectionsSlice = createSlice({
}
item.draft.request.url = action.payload.url;
const parts = splitOnFirst(item.draft.request.url, '?');
const urlParams = parseQueryParams(parts[1]);
const disabledParams = filter(item.draft.request.params, (p) => !p.enabled);
let enabledParams = filter(item.draft.request.params, (p) => p.enabled);
const parts = splitOnFirst(item?.draft?.request?.url, '?');
const urlQueryParams = parseQueryParams(parts[1]);
let urlPathParams = [];
try {
urlPathParams = parsePathParams(parts[0]);
} catch (err) {
console.error(err);
toast.error(err.message);
}
const disabledQueryParams = filter(item?.draft?.request?.params, (p) => !p.enabled && p.type === 'query');
let enabledQueryParams = filter(item?.draft?.request?.params, (p) => p.enabled && p.type === 'query');
let oldPathParams = filter(item?.draft?.request?.params, (p) => p.enabled && p.type === 'path');
let newPathParams = [];
// try and connect as much as old params uid's as possible
each(urlParams, (urlParam) => {
const existingParam = find(enabledParams, (p) => p.name === urlParam.name || p.value === urlParam.value);
urlParam.uid = existingParam ? existingParam.uid : uuid();
urlParam.enabled = true;
each(urlQueryParams, (urlQueryParam) => {
const existingQueryParam = find(
enabledQueryParams,
(p) => p?.name === urlQueryParam?.name || p?.value === urlQueryParam?.value
);
urlQueryParam.uid = existingQueryParam?.uid || uuid();
urlQueryParam.enabled = true;
urlQueryParam.type = 'query';
// once found, remove it - trying our best here to accommodate duplicate query params
if (existingParam) {
enabledParams = filter(enabledParams, (p) => p.uid !== existingParam.uid);
if (existingQueryParam) {
enabledQueryParams = filter(enabledQueryParams, (p) => p?.uid !== existingQueryParam?.uid);
}
});
// filter the newest path param and compare with previous data that already inserted
newPathParams = filter(urlPathParams, (urlPath) => {
const existingPathParam = find(oldPathParams, (p) => p.name === urlPath.name);
if (existingPathParam) {
return false;
}
urlPath.uid = uuid();
urlPath.enabled = true;
urlPath.type = 'path';
return true;
});
// remove path param that not used or deleted when typing url
oldPathParams = filter(oldPathParams, (urlPath) => {
return find(urlPathParams, (p) => p.name === urlPath.name);
});
// ultimately params get replaced with params in url + the disabled ones that existed prior
// the query params are the source of truth, the url in the queryurl input gets constructed using these params
// we however are also storing the full url (with params) in the url itself
item.draft.request.params = concat(urlParams, disabledParams);
item.draft.request.params = concat(urlQueryParams, newPathParams, disabledQueryParams, oldPathParams);
}
}
},
@ -426,6 +492,7 @@ export const collectionsSlice = createSlice({
name: '',
value: '',
description: '',
type: 'query',
enabled: true
});
}
@ -441,16 +508,20 @@ export const collectionsSlice = createSlice({
if (!item.draft) {
item.draft = cloneDeep(item);
}
const param = find(item.draft.request.params, (h) => h.uid === action.payload.param.uid);
if (param) {
param.name = action.payload.param.name;
param.value = action.payload.param.value;
param.description = action.payload.param.description;
param.enabled = action.payload.param.enabled;
const queryParam = find(
item.draft.request.params,
(h) => h.uid === action.payload.queryParam.uid && h.type === 'query'
);
if (queryParam) {
queryParam.name = action.payload.queryParam.name;
queryParam.value = action.payload.queryParam.value;
queryParam.enabled = action.payload.queryParam.enabled;
// update request url
const parts = splitOnFirst(item.draft.request.url, '?');
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled));
const query = stringifyQueryParams(
filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')
);
// if no query is found, then strip the query params in url
if (!query || !query.length) {
@ -486,7 +557,7 @@ export const collectionsSlice = createSlice({
// update request url
const parts = splitOnFirst(item.draft.request.url, '?');
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled));
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
if (query && query.length) {
item.draft.request.url = parts[0] + '?' + query;
} else {
@ -495,6 +566,29 @@ export const collectionsSlice = createSlice({
}
}
},
updatePathParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
item.draft = cloneDeep(item);
}
const param = find(
item.draft.request.params,
(p) => p.uid === action.payload.pathParam.uid && p.type === 'path'
);
if (param) {
param.name = action.payload.pathParam.name;
param.value = action.payload.pathParam.value;
}
}
}
},
addRequestHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -1038,6 +1132,137 @@ export const collectionsSlice = createSlice({
set(collection, 'root.docs', action.payload.docs);
}
},
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', []);
headers.push({
uid: uuid(),
name: '',
value: '',
description: '',
enabled: true
});
set(folder, 'root.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', []);
const header = find(headers, (h) => h.uid === action.payload.header.uid);
if (header) {
header.name = action.payload.header.name;
header.value = action.payload.header.value;
header.description = action.payload.header.description;
header.enabled = action.payload.header.enabled;
}
}
},
deleteFolderHeader: (state, action) => {
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', []);
headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
set(folder, 'root.request.headers', headers);
}
},
addFolderVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
const type = action.payload.type;
if (folder) {
if (type === 'request') {
const vars = get(folder, 'root.request.vars.req', []);
vars.push({
uid: uuid(),
name: '',
value: '',
type: 'request',
enabled: true
});
set(folder, 'root.request.vars.req', vars);
} else if (type === 'response') {
const vars = get(folder, 'root.request.vars.res', []);
vars.push({
uid: uuid(),
name: '',
value: '',
type: 'response',
enabled: true
});
set(folder, 'root.request.vars.res', vars);
}
}
},
updateFolderVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
const type = action.payload.type;
if (folder) {
if (type === 'request') {
let vars = get(folder, '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(folder, 'root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(folder, '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(folder, 'root.request.vars.res', vars);
}
}
},
deleteFolderVar: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
const type = action.payload.type;
if (folder) {
if (type === 'request') {
let vars = get(folder, 'root.request.vars.req', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(folder, 'root.request.vars.req', vars);
} else if (type === 'response') {
let vars = get(folder, 'root.request.vars.res', []);
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
set(folder, 'root.request.vars.res', vars);
}
}
},
updateFolderRequestScript: (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.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);
}
},
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);
}
},
addCollectionHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@ -1079,8 +1304,8 @@ export const collectionsSlice = createSlice({
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
const isCollectionRoot = file.meta.collectionRoot ? true : false;
const isFolderRoot = file.meta.folderRoot ? true : false;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
if (isCollectionRoot) {
if (collection) {
collection.root = file.data;
@ -1088,6 +1313,15 @@ export const collectionsSlice = createSlice({
return;
}
if (isFolderRoot) {
const folderPath = path.dirname(file.meta.pathname);
const folderItem = findItemInCollectionByPathname(collection, folderPath);
if (folderItem) {
folderItem.root = file.data;
}
return;
}
if (collection) {
const dirname = getDirectoryName(file.meta.pathname);
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
@ -1111,7 +1345,7 @@ export const collectionsSlice = createSlice({
currentSubItems = childItem.items;
}
if (!currentSubItems.find((f) => f.name === file.meta.name)) {
if (file.meta.name != 'folder.bru' && !currentSubItems.find((f) => f.name === file.meta.name)) {
// this happens when you rename a file
// the add event might get triggered first, before the unlink event
// this results in duplicate uids causing react renderer to go mad
@ -1398,6 +1632,7 @@ export const {
sortCollections,
updateLastAction,
updateSettingsSelectedTab,
updatedFolderSettingsSelectedTab,
collectionUnlinkEnvFileEvent,
saveEnvironment,
selectEnvironment,
@ -1420,6 +1655,7 @@ export const {
addQueryParam,
updateQueryParam,
deleteQueryParam,
updatePathParam,
addRequestHeader,
updateRequestHeader,
deleteRequestHeader,
@ -1444,6 +1680,15 @@ export const {
addVar,
updateVar,
deleteVar,
addFolderHeader,
updateFolderHeader,
deleteFolderHeader,
addFolderVar,
updateFolderVar,
deleteFolderVar,
updateFolderRequestScript,
updateFolderResponseScript,
updateFolderTests,
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader,

View File

@ -38,7 +38,8 @@ export const tabsSlice = createSlice({
requestPaneWidth: null,
requestPaneTab: action.payload.requestPaneTab || 'params',
responsePaneTab: 'response',
type: action.payload.type || 'request'
type: action.payload.type || 'request',
...(action.payload.folderUid ? { folderUid: action.payload.folderUid } : {})
});
state.activeTabUid = action.payload.uid;
},

View File

@ -0,0 +1,30 @@
import get from 'lodash/get';
export const getAuthHeaders = (collectionRootAuth, requestAuth) => {
const auth = collectionRootAuth && ['inherit'].includes(requestAuth?.mode) ? collectionRootAuth : requestAuth;
switch (auth.mode) {
case 'basic':
const username = get(auth, 'basic.username', '');
const password = get(auth, 'basic.password', '');
const basicToken = Buffer.from(`${username}:${password}`).toString('base64');
return [
{
enabled: true,
name: 'Authorization',
value: `Basic ${basicToken}`
}
];
case 'bearer':
return [
{
enabled: true,
name: 'Authorization',
value: `Bearer ${get(auth, 'bearer.token', '')}`
}
];
default:
return [];
}
};

View File

@ -24,7 +24,7 @@ const createHeaders = (headers) => {
const createQuery = (queryParams = []) => {
return queryParams
.filter((param) => param.enabled)
.filter((param) => param.enabled && param.type === 'query')
.map((param) => ({
name: param.name,
value: param.value

View File

@ -6,6 +6,11 @@
* LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3
*/
// Todo: Fix this
// import { interpolate } from '@usebruno/common';
import brunoCommon from '@usebruno/common';
const { interpolate } = brunoCommon;
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
const { get } = require('lodash');
@ -18,10 +23,23 @@ if (!SERVER_RENDERED) {
if (!str || !str.length || typeof str !== 'string') {
return;
}
// str is of format {{variableName}}, extract variableName
// we are seeing that from the gql query editor, the token string is of format variableName
const variableName = str.replace('{{', '').replace('}}', '').trim();
const variableValue = get(options.variables, variableName);
// str is of format {{variableName}} or :variableName, extract variableName
let variableName;
let variableValue;
if (str.startsWith('{{')) {
variableName = str.replace('{{', '').replace('}}', '').trim();
variableValue = interpolate(get(options.variables, variableName), options.variables);
} else if (str.startsWith(':')) {
variableName = str.replace(':', '').trim();
variableValue =
options.variables && options.variables.pathParams ? options.variables.pathParams[variableName] : undefined;
}
if (variableValue === undefined) {
return;
}
const into = document.createElement('div');
const descriptionDiv = document.createElement('div');

View File

@ -29,9 +29,6 @@ export const deleteUidsInItems = (items) => {
export const transformItem = (items = []) => {
each(items, (item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
item.request.query = item.request.params;
delete item.request.params;
if (item.type === 'graphql-request') {
item.type = 'graphql';
}

View File

@ -228,13 +228,14 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
});
};
const copyQueryParams = (params) => {
const copyParams = (params) => {
return map(params, (param) => {
return {
uid: param.uid,
name: param.name,
value: param.value,
description: param.description,
type: param.type,
enabled: param.enabled
};
});
@ -267,6 +268,10 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
const copyItems = (sourceItems, destItems) => {
each(sourceItems, (si) => {
if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') {
return;
}
const di = {
uid: si.uid,
type: si.type,
@ -274,50 +279,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
seq: si.seq
};
// if items is draft, then take data from draft to save
// The condition "!options.ignoreDraft" may appear confusing
// When saving a collection, this option allows the caller to specify to ignore any draft changes while still saving rest of the collection.
// This is useful for performing rename request/collections while still leaving changes in draft not making its way into the indexeddb
if (si.draft && !options.ignoreDraft) {
if (si.draft.request) {
di.request = {
url: si.draft.request.url,
method: si.draft.request.method,
headers: copyHeaders(si.draft.request.headers),
params: copyQueryParams(si.draft.request.params),
body: {
mode: si.draft.request.body.mode,
json: si.draft.request.body.json,
text: si.draft.request.body.text,
xml: si.draft.request.body.xml,
graphql: si.draft.request.body.graphql,
sparql: si.draft.request.body.sparql,
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
},
auth: {
mode: get(si.draft.request, 'auth.mode', 'none'),
basic: {
username: get(si.draft.request, 'auth.basic.username', ''),
password: get(si.draft.request, 'auth.basic.password', '')
},
bearer: {
token: get(si.draft.request, 'auth.bearer.token', '')
}
},
script: si.draft.request.script,
vars: si.draft.request.vars,
assertions: si.draft.request.assertions,
tests: si.draft.request.tests
};
}
} else {
if (si.request) {
di.request = {
url: si.request.url,
method: si.request.method,
headers: copyHeaders(si.request.headers),
params: copyQueryParams(si.request.params),
params: copyParams(si.request.params),
body: {
mode: si.request.body.mode,
json: si.request.body.json,
@ -328,26 +295,93 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
},
auth: {
mode: get(si.request, 'auth.mode', 'none'),
basic: {
username: get(si.request, 'auth.basic.username', ''),
password: get(si.request, 'auth.basic.password', '')
},
bearer: {
token: get(si.request, 'auth.bearer.token', '')
}
},
script: si.request.script,
vars: si.request.vars,
assertions: si.request.assertions,
tests: si.request.tests
};
// Handle auth object dynamically
di.request.auth = {
mode: get(si.request, 'auth.mode', 'none')
};
switch (di.request.auth.mode) {
case 'awsv4':
di.request.auth.awsv4 = {
accessKeyId: get(si.request, 'auth.awsv4.accessKeyId', ''),
secretAccessKey: get(si.request, 'auth.awsv4.secretAccessKey', ''),
sessionToken: get(si.request, 'auth.awsv4.sessionToken', ''),
service: get(si.request, 'auth.awsv4.service', ''),
region: get(si.request, 'auth.awsv4.region', ''),
profileName: get(si.request, 'auth.awsv4.profileName', '')
};
break;
case 'basic':
di.request.auth.basic = {
username: get(si.request, 'auth.basic.username', ''),
password: get(si.request, 'auth.basic.password', '')
};
break;
case 'bearer':
di.request.auth.bearer = {
token: get(si.request, 'auth.bearer.token', '')
};
break;
case 'digest':
di.request.auth.digest = {
username: get(si.request, 'auth.digest.username', ''),
password: get(si.request, 'auth.digest.password', '')
};
break;
case 'oauth2':
let grantType = get(si.request, 'auth.oauth2.grantType', '');
switch (grantType) {
case 'password':
di.request.auth.oauth2 = {
grantType: grantType,
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
username: get(si.request, 'auth.oauth2.username', ''),
password: get(si.request, 'auth.oauth2.password', ''),
clientId: get(si.request, 'auth.oauth2.clientId', ''),
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
scope: get(si.request, 'auth.oauth2.scope', '')
};
break;
case 'authorization_code':
di.request.auth.oauth2 = {
grantType: grantType,
callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''),
authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''),
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
clientId: get(si.request, 'auth.oauth2.clientId', ''),
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
scope: get(si.request, 'auth.oauth2.scope', ''),
pkce: get(si.request, 'auth.oauth2.pkce', false)
};
break;
case 'client_credentials':
di.request.auth.oauth2 = {
grantType: grantType,
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
clientId: get(si.request, 'auth.oauth2.clientId', ''),
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
scope: get(si.request, 'auth.oauth2.scope', '')
};
break;
}
break;
default:
break;
}
if (di.request.body.mode === 'json') {
di.request.body.json = replaceTabsWithSpaces(di.request.body.json);
}
}
if (di.request && di.request.body.mode === 'json') {
di.request.body.json = replaceTabsWithSpaces(di.request.body.json);
if (si.type === 'js') {
di.fileContent = si.raw;
}
destItems.push(di);
@ -369,8 +403,14 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;
collectionToSave.environments = collection.environments || [];
copyItems(collection.items, collectionToSave.items);
collectionToSave.brunoConfig = cloneDeep(collection?.brunoConfig);
// delete proxy password if present
if (collectionToSave?.brunoConfig?.proxy?.auth?.password) {
delete collectionToSave.brunoConfig.proxy.auth.password;
}
copyItems(collection.items, collectionToSave.items);
return collectionToSave;
};
@ -402,6 +442,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
name: param.name,
value: param.value,
description: param.description,
type: param.type,
enabled: param.enabled
});
});
@ -616,6 +657,18 @@ export const getEnvironmentVariables = (collection) => {
return variables;
};
const getPathParams = (item) => {
let pathParams = {};
if (item && item.request && item.request.params) {
item.request.params.forEach((param) => {
if (param.type === 'path' && param.name && param.value) {
pathParams[param.name] = param.value;
}
});
}
return pathParams;
};
export const getTotalRequestCountInCollection = (collection) => {
let count = 0;
each(collection.items, (item) => {
@ -629,12 +682,16 @@ export const getTotalRequestCountInCollection = (collection) => {
return count;
};
export const getAllVariables = (collection) => {
export const getAllVariables = (collection, item) => {
const environmentVariables = getEnvironmentVariables(collection);
const pathParams = getPathParams(item);
return {
...environmentVariables,
...collection.collectionVariables,
pathParams: {
...pathParams
},
process: {
env: {
...collection.processEnvVariables

View File

@ -13,32 +13,61 @@ const pathFoundInVariables = (path, obj) => {
};
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
let variablesOverlay = {
token: function (stream, state) {
CodeMirror.defineMode('combinedmode', function (config, parserConfig) {
const variablesOverlay = {
token: function (stream) {
if (stream.match('{{', true)) {
let ch;
let word = '';
while ((ch = stream.next()) != null) {
if (ch == '}' && stream.next() == '}') {
if (ch === '}' && stream.peek() === '}') {
stream.eat('}');
let found = pathFoundInVariables(word, variables);
if (found) {
return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9);
} else {
return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9);
}
// Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror.
const found = pathFoundInVariables(word, variables);
const status = found ? 'valid' : 'invalid';
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
return `variable-${status} ${randomClass}`;
}
word += ch;
}
}
while (stream.next() != null && !stream.match('{{', false)) {}
stream.skipTo('{{') || stream.skipToEnd();
return null;
}
};
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
const urlPathParamsOverlay = {
token: function (stream) {
if (stream.match(':', true)) {
let ch;
let word = '';
while ((ch = stream.next()) != null) {
if (ch === '/' || ch === '?' || ch === '&' || ch === '=') {
stream.backUp(1);
const found = pathFoundInVariables(word, variables?.pathParams);
const status = found ? 'valid' : 'invalid';
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
return `variable-${status} ${randomClass}`;
}
word += ch;
}
// If we've consumed all characters and the word is not empty, it might be a path parameter at the end of the URL.
if (word) {
const found = pathFoundInVariables(word, variables?.pathParams);
const status = found ? 'valid' : 'invalid';
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
return `variable-${status} ${randomClass}`;
}
}
stream.skipTo(':') || stream.skipToEnd();
return null;
}
};
return CodeMirror.overlayMode(
CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay),
urlPathParamsOverlay
);
});
};

View File

@ -48,6 +48,13 @@ function getDataString(request) {
}
const parsedQueryString = querystring.parse(request.data, { sort: false });
// if missing `=`, `query-string` will set value as `null`. Reset value as empty string ('') here.
// https://github.com/sindresorhus/query-string/blob/3d8fbf2328220c06e45f166cdf58e70617c7ee68/base.js#L364-L366
Object.keys(parsedQueryString).forEach((key) => {
if (parsedQueryString[key] === null) {
parsedQueryString[key] = '';
}
});
const keyCount = Object.keys(parsedQueryString).length;
const singleKeyOnly = keyCount === 1 && !parsedQueryString[Object.keys(parsedQueryString)[0]];
const singularData = request.isDataBinary || singleKeyOnly;

View File

@ -171,11 +171,43 @@ export const exportCollection = (collection) => {
}
};
const generateHost = (url) => {
try {
const { hostname } = new URL(url);
return hostname.split('.');
} catch (error) {
console.error(`Invalid URL: ${url}`, error);
return [];
}
};
const generatePathParams = (params) => {
return params.filter((param) => param.type === 'path').map((param) => `:${param.name}`);
};
const generateQueryParams = (params) => {
return params
.filter((param) => param.type === 'query')
.map(({ name, value, description }) => ({ key: name, value, description }));
};
const generateVariables = (params) => {
return params
.filter((param) => param.type === 'path')
.map(({ name, value, description }) => ({ key: name, value, description }));
};
const generateRequestSection = (itemRequest) => {
const requestObject = {
method: itemRequest.method,
header: generateHeaders(itemRequest.headers),
url: itemRequest.url,
url: {
raw: itemRequest.url,
host: generateHost(itemRequest.url),
path: generatePathParams(itemRequest.params),
query: generateQueryParams(itemRequest.params),
variable: generateVariables(itemRequest.params)
},
auth: generateAuth(itemRequest.auth)
};

View File

@ -32,7 +32,7 @@ const importCollection = () => {
.then(updateUidsInCollection)
.then(transformItemsInCollection)
.then(validateSchema)
.then((collection) => resolve(collection))
.then((collection) => resolve({ collection }))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import collection failed'));

View File

@ -29,7 +29,6 @@ export const updateUidsInCollection = (_collection) => {
item.uid = uuid();
each(get(item, 'request.headers'), (header) => (header.uid = uuid()));
each(get(item, 'request.query'), (param) => (param.uid = uuid()));
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
each(get(item, 'request.vars.req'), (v) => (v.uid = uuid()));
each(get(item, 'request.vars.res'), (v) => (v.uid = uuid()));
@ -66,8 +65,13 @@ export const transformItemsInCollection = (collection) => {
if (['http', 'graphql'].includes(item.type)) {
item.type = `${item.type}-request`;
if (item.request.query) {
item.request.params = item.request.query;
item.request.params = item.request.query.map((queryItem) => ({
...queryItem,
type: 'query',
uid: queryItem.uid || uuid()
}));
}
delete item.request.query;

Some files were not shown because too many files have changed in this diff Show More