mirror of
https://github.com/usebruno/bruno.git
synced 2025-02-13 08:19:57 +01:00
Merge branch 'build' into feature/binary-response-in-tests
# Conflicts: # packages/bruno-js/src/bruno-response.js
This commit is contained in:
commit
0013bca0a0
@ -15,6 +15,7 @@
|
||||
| [正體中文](docs/contributing/contributing_zhtw.md)
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Nederlands](docs/contributing/contributing_nl.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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)
|
||||
|
||||
## আসুন ব্রুনোকে আরও ভালো করি, একসাথে!!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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!!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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 !
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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 को बेहतर बनाएं !!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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 をよりよいものにしていきましょう!!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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를 더 좋게 만들어요!!
|
||||
|
||||
|
82
docs/contributing/contributing_nl.md
Normal file
82
docs/contributing/contributing_nl.md
Normal file
@ -0,0 +1,82 @@
|
||||
[English](../../contributing.md)
|
||||
|
||||
## Laten we Bruno samen beter maken !!
|
||||
|
||||
We zijn blij dat je Bruno wilt verbeteren. Hieronder staan de richtlijnen om Bruno op je computer op te zetten.
|
||||
|
||||
### Technologiestack
|
||||
|
||||
Bruno is gebouwd met Next.js en React. We gebruiken ook Electron om een desktopversie te leveren (die lokale collecties ondersteunt).
|
||||
|
||||
Bibliotheken die we gebruiken:
|
||||
|
||||
- CSS - Tailwind
|
||||
- Code Editors - Codemirror
|
||||
- State Management - Redux
|
||||
- Iconen - Tabler Icons
|
||||
- Formulieren - formik
|
||||
- Schema Validatie - Yup
|
||||
- Request Client - axios
|
||||
- Bestandsysteem Watcher - chokidar
|
||||
|
||||
### Afhankelijkheden
|
||||
|
||||
Je hebt [Node v18.x of de nieuwste LTS-versie](https://nodejs.org/en/) en npm 8.x nodig. We gebruiken npm workspaces in het project.
|
||||
|
||||
## Ontwikkeling
|
||||
|
||||
Bruno wordt ontwikkeld als een desktop-app. Je moet de app laden door de Next.js app in één terminal te draaien en daarna de Electron app in een andere terminal te draaien.
|
||||
|
||||
### Lokale Ontwikkeling
|
||||
|
||||
```bash
|
||||
# gebruik voorgeschreven node versie
|
||||
nvm use
|
||||
|
||||
# installeer afhankelijkheden
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build pakketten
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
|
||||
# draai next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# draai electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Problemen oplossen
|
||||
|
||||
Je kunt een `Unsupported platform`-fout tegenkomen wanneer je `npm install` uitvoert. Om dit te verhelpen, moet je `node_modules` en `package-lock.json` verwijderen en `npm install` uitvoeren. Dit zou alle benodigde afhankelijkheden moeten installeren om de app te draaien.
|
||||
|
||||
```shell
|
||||
# Verwijder node_modules in subdirectories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Verwijder package-lock in subdirectories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testen
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Pull Requests indienen
|
||||
|
||||
- Houd de PR's klein en gefocust op één ding
|
||||
- Volg het formaat voor het aanmaken van branches
|
||||
- feature/[feature naam]: Deze branch moet wijzigingen voor een specifieke functie bevatten
|
||||
- Voorbeeld: feature/dark-mode
|
||||
- bugfix/[bug naam]: Deze branch moet alleen bugfixes voor een specifieke bug bevatten
|
||||
- Voorbeeld: bugfix/bug-1
|
@ -1,20 +1,4 @@
|
||||
[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 !!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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!!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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ă!!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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)
|
||||
|
||||
## Давайте вместе сделаем Бруно лучше!!!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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!!!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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 краще, разом !!
|
||||
|
||||
|
@ -1,20 +1,4 @@
|
||||
[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 變得更好!
|
||||
|
||||
|
7
docs/publishing/publishin_nl.md
Normal file
7
docs/publishing/publishin_nl.md
Normal file
@ -0,0 +1,7 @@
|
||||
[English](../../publishing.md)
|
||||
|
||||
### Bruno publiceren naar een nieuwe pakketbeheerder
|
||||
|
||||
Hoewel onze code open source is en beschikbaar voor iedereen, verzoeken we je vriendelijk om contact met ons op te nemen voordat je publicatie overweegt op nieuwe pakketbeheerders. Als de maker van Bruno houd ik het handelsmerk `Bruno` voor dit project en wil ik het distributieproces beheren. Als je Bruno op een nieuwe pakketbeheerder wilt zien, dien dan een GitHub-issue in.
|
||||
|
||||
Hoewel de meerderheid van onze functies gratis en open source zijn (die REST en GraphQL API's dekken), streven we ernaar een harmonieuze balans te vinden tussen open-source principes en duurzaamheid - https://github.com/usebruno/bruno/discussions/269
|
@ -1,14 +1,4 @@
|
||||
[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)
|
||||
|
||||
### ব্রুনোকে নতুন প্যাকেজ ম্যানেজারে প্রকাশ করা
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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 发布到新的包管理器
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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 を新しいパッケージマネージャに公開する場合の注意
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
[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 發佈到新的套件管理器
|
||||
|
||||
|
157
docs/readme/readme_nl.md
Normal file
157
docs/readme/readme_nl.md
Normal file
@ -0,0 +1,157 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - Open source IDE voor het verkennen en testen van API's.
|
||||
|
||||
[![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) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | ** Nederlands ** | [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) | [日本語](docs/readme/readme_ja.md)
|
||||
|
||||
Bruno is een nieuwe en innovatieve API-client, gericht op het revolutioneren van de status quo die wordt vertegenwoordigd door Postman en vergelijkbare tools.
|
||||
|
||||
Bruno slaat je collecties direct op in een map op je bestandssysteem. We gebruiken een platte tekst opmaaktaal, Bru, om informatie over API-verzoeken op te slaan.
|
||||
|
||||
Je kunt Git of elke versiebeheertool naar keuze gebruiken om samen te werken aan je API-collecties.
|
||||
|
||||
Bruno is uitsluitend offline. Er zijn geen plannen om ooit cloud-synchronisatie aan Bruno toe te voegen. We waarderen je gegevensprivacy en geloven dat deze op je apparaat moet blijven. Lees onze langetermijnvisie [hier](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
[Download Bruno](https://www.usebruno.com/downloads)
|
||||
|
||||
📢 Bekijk onze recente presentatie op de India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
![bruno](/assets/images/landing-2.png) <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
De meeste van onze functies zijn gratis en open source.
|
||||
We streven naar een harmonieuze balans tussen [open-source principes en duurzaamheid](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
Je kunt de [Golden Edition](https://www.usebruno.com/pricing) kopen voor een eenmalige betaling van **$19**! <br/>
|
||||
|
||||
### Installatie
|
||||
|
||||
Bruno is beschikbaar als binaire download [op onze website](https://www.usebruno.com/downloads) voor Mac, Windows en Linux.
|
||||
|
||||
Je kunt Bruno ook installeren via pakketbeheerders zoals Homebrew, Chocolatey, Scoop, Snap, Flatpak en Apt.
|
||||
|
||||
```sh
|
||||
# Op Mac via Homebrew
|
||||
brew install bruno
|
||||
|
||||
# Op Windows via Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# Op Windows via Scoop
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# Op Windows via winget
|
||||
winget install Bruno.Bruno
|
||||
|
||||
# Op Linux via Snap
|
||||
snap install bruno
|
||||
|
||||
# Op Linux via Flatpak
|
||||
flatpak install com.usebruno.Bruno
|
||||
|
||||
# Op 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 [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
|
||||
```
|
||||
|
||||
### Draai op meerdere platformen 🖥️
|
||||
|
||||
![bruno](/assets/images/run-anywhere.png) <br /><br />
|
||||
|
||||
### Samenwerken via Git 👩💻🧑💻
|
||||
|
||||
Of elk versiebeheersysteem naar keuze
|
||||
|
||||
![bruno](/assets/images/version-control.png) <br /><br />
|
||||
|
||||
### Sponsors
|
||||
|
||||
#### Gouden Sponsors
|
||||
|
||||
<img src="../../assets/images/sponsors/samagata.png" width="150"/>
|
||||
|
||||
#### Zilveren Sponsors
|
||||
|
||||
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
|
||||
|
||||
#### Bronzen Sponsors
|
||||
|
||||
<a href="https://zuplo.link/bruno">
|
||||
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
|
||||
</a>
|
||||
|
||||
### Belangrijke Links 📌
|
||||
|
||||
- [Onze Langetermijnvisie](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Documentatie](https://docs.usebruno.com)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
|
||||
- [Website](https://www.usebruno.com)
|
||||
- [Prijzen](https://www.usebruno.com/pricing)
|
||||
- [Download](https://www.usebruno.com/downloads)
|
||||
- [GitHub Sponsors](https://github.com/sponsors/helloanoop)
|
||||
|
||||
### Showcase 🎥
|
||||
|
||||
- [Getuigenissen](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Kenniscentrum](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Ondersteuning ❤️
|
||||
|
||||
Als je Bruno leuk vindt en ons open-source werk wilt ondersteunen, overweeg dan om ons te sponsoren via [GitHub Sponsors](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Deel Getuigenissen 📣
|
||||
|
||||
Als Bruno je heeft geholpen op je werk en in je teams, deel dan je [getuigenissen op onze GitHub-discussie](https://github.com/usebruno/bruno/discussions/343).
|
||||
|
||||
|
||||
### Blijf in contact 🌐
|
||||
|
||||
[𝕏 (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)
|
||||
|
||||
### Handelsmerk
|
||||
|
||||
**Naam**
|
||||
|
||||
`Bruno` is een handelsmerk in bezit van [Anoop M D](https://www.helloanoop.com/).
|
||||
|
||||
**Logo**
|
||||
|
||||
Het logo is afkomstig van [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licentie: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### Bijdragen 👩💻🧑💻
|
||||
|
||||
Ik ben blij dat je Bruno wilt verbeteren. Bekijk de [bijdragegids](contributing.md).
|
||||
|
||||
Zelfs als je geen bijdragen via code kunt leveren, aarzel dan niet om bugs en functieverzoeken in te dienen die moeten worden geïmplementeerd om jouw gebruiksscenario op te lossen.
|
||||
|
||||
### Auteurs
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Licentie 📄
|
||||
|
||||
[MIT](../../license.md)
|
55
package-lock.json
generated
55
package-lock.json
generated
@ -50,6 +50,7 @@
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@ -670,6 +671,7 @@
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -677,6 +679,7 @@
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
@ -740,6 +743,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.25.2",
|
||||
@ -754,6 +758,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
@ -761,6 +766,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
@ -839,6 +845,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.24.7",
|
||||
@ -905,6 +912,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-simple-access": {
|
||||
"version": "7.24.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.24.7",
|
||||
@ -942,6 +950,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.24.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -962,6 +971,7 @@
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.25.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.0",
|
||||
@ -3644,37 +3654,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@n8n/vm2": {
|
||||
"version": "3.9.25",
|
||||
"resolved": "https://registry.npmjs.org/@n8n/vm2/-/vm2-3.9.25.tgz",
|
||||
"integrity": "sha512-qoGLFzyHBW7HKpwXkl05QKsIh3GkDw6lOiTOWYlUDnOIQ1b7EgM+O5EMjrMGy7r+kz52+Q7o6GLxBIcxVI8rEg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.7.0",
|
||||
"acorn-walk": "^8.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"vm2": "bin/vm2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.10",
|
||||
"pnpm": ">=9.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@n8n/vm2/node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "12.3.3",
|
||||
"license": "MIT"
|
||||
@ -4751,6 +4730,7 @@
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
@ -4759,6 +4739,7 @@
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "12.2.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "*",
|
||||
@ -4767,6 +4748,7 @@
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
@ -6240,6 +6222,7 @@
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.3",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -7238,6 +7221,7 @@
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
@ -8477,6 +8461,7 @@
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.11",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-util": {
|
||||
@ -9438,6 +9423,7 @@
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -13186,6 +13172,7 @@
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.18",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-vault": {
|
||||
@ -16201,6 +16188,7 @@
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -17663,6 +17651,7 @@
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -18578,7 +18567,6 @@
|
||||
"inquirer": "^9.1.4",
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"vm2": "^3.9.13",
|
||||
@ -18633,7 +18621,7 @@
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "v1.26.1",
|
||||
"version": "v1.28.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.525.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
@ -18664,7 +18652,6 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "3.3.4",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
|
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { isEqual, escapeRegExp } from 'lodash';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@ -61,6 +61,8 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.hasEnvVar(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.getFolderVar(key)',
|
||||
'bru.getCollectionVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.hasVar(key)',
|
||||
'bru.getVar(key)',
|
||||
@ -406,7 +408,8 @@ export default class CodeEditor extends React.Component {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput && searchInput.value.length > 0) {
|
||||
const text = new RegExp(searchInput.value, 'gi');
|
||||
// Escape special characters in search input to prevent RegExp crashes. Fixes #3051
|
||||
const text = new RegExp(escapeRegExp(searchInput.value), 'gi');
|
||||
const matches = this.editor.getValue().match(text);
|
||||
count = matches ? matches.length : 0;
|
||||
}
|
||||
|
@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.auth-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,109 @@
|
||||
import React, { useRef, forwardRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
|
||||
|
||||
const ApiKeyAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const apikeyAuth = get(collection, 'root.request.auth.apikey', {});
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end auth-type-label select-none">
|
||||
{humanizeRequestAPIKeyPlacement(apikeyAuth?.placement)}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleAuthChange = (property, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
...apikeyAuth,
|
||||
[property]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
!apikeyAuth?.placement &&
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
placement: 'header'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [apikeyAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.key || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('key', val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Value</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('value', val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Add To</label>
|
||||
<div className="inline-flex items-center cursor-pointer auth-placement-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleAuthChange('placement', 'header');
|
||||
}}
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleAuthChange('placement', 'queryparams');
|
||||
}}
|
||||
>
|
||||
Query Params
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyAuth;
|
@ -79,6 +79,15 @@ const AuthMode = ({ collection }) => {
|
||||
>
|
||||
Oauth2
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
@ -6,6 +6,7 @@ import AwsV4Auth from './AwsV4Auth';
|
||||
import BearerAuth from './BearerAuth';
|
||||
import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import ApiKeyAuth from './ApiKeyAuth/';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
@ -33,6 +34,9 @@ const Auth = ({ collection }) => {
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -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;
|
@ -0,0 +1,162 @@
|
||||
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 { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import {
|
||||
addCollectionVar,
|
||||
deleteCollectionVar,
|
||||
updateCollectionVar
|
||||
} from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const VarsTable = ({ collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const addVar = () => {
|
||||
dispatch(
|
||||
addCollectionVar({
|
||||
collectionUid: collection.uid,
|
||||
type: varType
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.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(
|
||||
updateCollectionVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteCollectionVar({
|
||||
type: varType,
|
||||
varUid: _var.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>
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-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;
|
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
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 collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable 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;
|
@ -16,6 +16,7 @@ import Docs from './Docs';
|
||||
import Presets from './Presets';
|
||||
import Info from './Info';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -77,6 +78,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth collection={collection} />;
|
||||
}
|
||||
@ -123,6 +127,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
|
@ -44,7 +44,7 @@ const CopyEnvironment = ({ collection, environment, onClose }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Copy Environment'} confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
New Environment Name
|
||||
|
@ -50,7 +50,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Environment Name
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@ -9,12 +10,12 @@ import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const addButtonRef = useRef(null);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@ -85,6 +86,12 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.dirty) {
|
||||
addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [formik.values, formik.dirty]);
|
||||
|
||||
const handleReset = () => {
|
||||
formik.resetForm({ originalEnvironmentVariables });
|
||||
};
|
||||
@ -159,11 +166,15 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={addVariable}>
|
||||
+ Add Variable
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
ref={addButtonRef}
|
||||
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={addVariable}
|
||||
>
|
||||
+ Add Variable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -50,7 +50,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Environment Name
|
||||
|
@ -116,6 +116,7 @@ const Headers = ({ collection, folder }) => {
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -130,6 +130,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -2,6 +2,7 @@ import MarkdownIt from 'markdown-it';
|
||||
import * as MarkdownItReplaceLink from 'markdown-it-replace-link';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import React from 'react';
|
||||
import { isValidUrl } from 'utils/url/index';
|
||||
|
||||
const Markdown = ({ collectionPath, onDoubleClick, content }) => {
|
||||
const markdownItOptions = {
|
||||
@ -15,7 +16,7 @@ const Markdown = ({ collectionPath, onDoubleClick, content }) => {
|
||||
if (target.tagName === 'A') {
|
||||
event.preventDefault();
|
||||
const href = target.getAttribute('href');
|
||||
if (href) {
|
||||
if (href && isValidUrl(href)) {
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useFocusTrap from 'hooks/useFocusTrap';
|
||||
|
||||
const ESC_KEY_CODE = 27;
|
||||
const ENTER_KEY_CODE = 13;
|
||||
|
||||
const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
|
||||
<div className="bruno-modal-header">
|
||||
@ -69,25 +73,35 @@ const Modal = ({
|
||||
onClick,
|
||||
closeModalFadeTimeout = 500
|
||||
}) => {
|
||||
const modalRef = useRef(null);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const escFunction = (event) => {
|
||||
const escKeyCode = 27;
|
||||
if (event.keyCode === escKeyCode) {
|
||||
closeModal({ type: 'esc' });
|
||||
|
||||
const handleKeydown = (event) => {
|
||||
const { keyCode, shiftKey, ctrlKey, altKey, metaKey } = event;
|
||||
switch (keyCode) {
|
||||
case ESC_KEY_CODE: {
|
||||
if (disableEscapeKey) return;
|
||||
return closeModal({ type: 'esc' });
|
||||
}
|
||||
case ENTER_KEY_CODE: {
|
||||
if (!shiftKey && !ctrlKey && !altKey && !metaKey && handleConfirm) {
|
||||
return handleConfirm();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useFocusTrap(modalRef);
|
||||
|
||||
const closeModal = (args) => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (disableEscapeKey) return;
|
||||
document.addEventListener('keydown', escFunction, false);
|
||||
|
||||
document.addEventListener('keydown', handleKeydown, false);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', escFunction, false);
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, [disableEscapeKey, document]);
|
||||
|
||||
@ -100,7 +114,13 @@ const Modal = ({
|
||||
}
|
||||
return (
|
||||
<StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>
|
||||
<div className={`bruno-modal-card modal-${size}`}>
|
||||
<div
|
||||
className={`bruno-modal-card modal-${size}`}
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
<ModalHeader
|
||||
title={title}
|
||||
hideClose={hideClose}
|
||||
|
@ -93,10 +93,12 @@ const Notifications = () => {
|
||||
dispatch(fetchNotifications());
|
||||
setShowNotificationsModal(true);
|
||||
}}
|
||||
aria-label="Check all Notifications"
|
||||
>
|
||||
<ToolHint text="Notifications" toolhintId="Notifications" offset={8} >
|
||||
<ToolHint text="Notifications" toolhintId="Notifications" offset={8}>
|
||||
<IconBell
|
||||
size={18}
|
||||
aria-hidden
|
||||
strokeWidth={1.5}
|
||||
className={`mr-2 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
|
||||
/>
|
||||
@ -133,8 +135,9 @@ const Notifications = () => {
|
||||
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
|
||||
<li
|
||||
key={notification.id}
|
||||
className={`p-4 flex flex-col justify-center ${selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
|
||||
}`}
|
||||
className={`p-4 flex flex-col justify-center ${
|
||||
selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
|
||||
}`}
|
||||
onClick={handleNotificationItemClick(notification)}
|
||||
>
|
||||
<div className="notification-title w-full">{notification?.title}</div>
|
||||
@ -144,8 +147,9 @@ const Notifications = () => {
|
||||
</ul>
|
||||
<div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs">
|
||||
<button
|
||||
className={`pl-2 pr-2 py-3 select-none ${pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
|
||||
}`}
|
||||
className={`pl-2 pr-2 py-3 select-none ${
|
||||
pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
|
||||
}`}
|
||||
onClick={handlePrev}
|
||||
>
|
||||
{'Prev'}
|
||||
@ -161,8 +165,9 @@ const Notifications = () => {
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={`pl-2 pr-2 py-3 select-none ${pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
|
||||
}`}
|
||||
className={`pl-2 pr-2 py-3 select-none ${
|
||||
pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
|
||||
}`}
|
||||
onClick={handleNext}
|
||||
>
|
||||
{'Next'}
|
||||
|
@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.settings-label {
|
||||
width: 80px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
@ -20,6 +20,12 @@ const StyledWrapper = styled.div`
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.system-proxy-settings {
|
||||
label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -11,14 +11,17 @@ import { useState } from 'react';
|
||||
|
||||
const ProxySettings = ({ close }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const systemProxyEnvVariables = useSelector((state) => state.app.systemProxyEnvVariables);
|
||||
const { http_proxy, https_proxy, no_proxy } = systemProxyEnvVariables || {};
|
||||
const dispatch = useDispatch();
|
||||
console.log(preferences);
|
||||
|
||||
const proxySchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
mode: Yup.string().oneOf(['off', 'on', 'system']),
|
||||
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
is: 'on',
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
@ -31,7 +34,7 @@ const ProxySettings = ({ close }) => {
|
||||
.transform((_, val) => (val ? Number(val) : null)),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
is: 'on',
|
||||
then: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string()
|
||||
@ -54,7 +57,7 @@ const ProxySettings = ({ close }) => {
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
enabled: preferences.proxy.enabled || false,
|
||||
mode: preferences.proxy.mode,
|
||||
protocol: preferences.proxy.protocol || 'http',
|
||||
hostname: preferences.proxy.hostname || '',
|
||||
port: preferences.proxy.port || 0,
|
||||
@ -94,7 +97,7 @@ const ProxySettings = ({ close }) => {
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: preferences.proxy.enabled || false,
|
||||
mode: preferences.proxy.mode,
|
||||
protocol: preferences.proxy.protocol || 'http',
|
||||
hostname: preferences.proxy.hostname || '',
|
||||
port: preferences.proxy.port || '',
|
||||
@ -109,188 +112,256 @@ const ProxySettings = ({ close }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Global Proxy Settings</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="enabled">
|
||||
Enabled
|
||||
</label>
|
||||
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
Mode
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={formik.values.protocol === 'http'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
name="mode"
|
||||
value="false"
|
||||
checked={formik.values.mode === 'off'}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('mode', 'off');
|
||||
}}
|
||||
className="mr-1 cursor-pointer"
|
||||
/>
|
||||
HTTP
|
||||
Off
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<label className="flex items-center ml-4 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={formik.values.protocol === 'https'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
name="mode"
|
||||
value="true"
|
||||
checked={formik.values.mode === 'on'}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('mode', 'on');
|
||||
}}
|
||||
className="mr-1 cursor-pointer"
|
||||
/>
|
||||
HTTPS
|
||||
On
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<label className="flex items-center ml-4 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={formik.values.protocol === 'socks4'}
|
||||
name="mode"
|
||||
value="system"
|
||||
checked={formik.values.mode === 'system'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
className="mr-1 cursor-pointer"
|
||||
/>
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={formik.values.protocol === 'socks5'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
System Proxy
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={formik.values.auth.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.username}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
{formik?.values?.mode === 'system' ? (
|
||||
<div className="mb-3 pt-1 text-muted system-proxy-settings">
|
||||
<small>
|
||||
Below values are sourced from your system environment variables and cannot be directly updated in Bruno.<br/>
|
||||
Please refer to your OS documentation to change these values.
|
||||
</small>
|
||||
<div className="flex flex-col justify-start items-start pt-2">
|
||||
<div className="mb-1 flex items-center">
|
||||
<label className="settings-label" htmlFor="http_proxy">
|
||||
http_proxy
|
||||
</label>
|
||||
<div className="opacity-80">{http_proxy || '-'}</div>
|
||||
</div>
|
||||
<div className="mb-1 flex items-center">
|
||||
<label className="settings-label" htmlFor="https_proxy">
|
||||
https_proxy
|
||||
</label>
|
||||
<div className="opacity-80">{https_proxy || '-'}</div>
|
||||
</div>
|
||||
<div className="mb-1 flex items-center">
|
||||
<label className="settings-label" htmlFor="no_proxy">
|
||||
no_proxy
|
||||
</label>
|
||||
<div className="opacity-80">{no_proxy || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative">
|
||||
) : null}
|
||||
{formik?.values?.mode === 'on' ? (
|
||||
<>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={formik.values.protocol === 'http'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={formik.values.protocol === 'https'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={formik.values.protocol === 'socks4'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={formik.values.protocol === 'socks5'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
id="auth.password"
|
||||
type={passwordVisible ? `text` : 'password'}
|
||||
name="auth.password"
|
||||
className="outline-none w-[10.5rem] bg-transparent"
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.password}
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={formik.values.auth.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
|
||||
</button>
|
||||
</div>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.bypassProxy || ''}
|
||||
/>
|
||||
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.username}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative">
|
||||
<input
|
||||
id="auth.password"
|
||||
type={passwordVisible ? `text` : 'password'}
|
||||
name="auth.password"
|
||||
className="outline-none w-[10.5rem] bg-transparent"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.password}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
|
||||
</button>
|
||||
</div>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.bypassProxy || ''}
|
||||
/>
|
||||
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-md btn-secondary">
|
||||
Save
|
||||
|
112
packages/bruno-app/src/components/ReorderTable/index.js
Normal file
112
packages/bruno-app/src/components/ReorderTable/index.js
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||
|
||||
/**
|
||||
* ReorderTable Component
|
||||
*
|
||||
* A table component that allows rows to be reordered via drag-and-drop.
|
||||
*
|
||||
* @param {Object} props - The component props
|
||||
* @param {React.ReactNode[]} props.children - The table rows as children
|
||||
* @param {function} props.updateReorderedItem - Callback function to handle reordered rows
|
||||
*/
|
||||
|
||||
const ReorderTable = ({ children, updateReorderedItem }) => {
|
||||
const tbodyRef = useRef();
|
||||
const [rowsOrder, setRowsOrder] = useState(React.Children.toArray(children));
|
||||
const [hoveredRow, setHoveredRow] = useState(null);
|
||||
const [dragStart, setDragStart] = useState(null);
|
||||
|
||||
/**
|
||||
* useEffect hook to update the rows order and handle row hover states
|
||||
*/
|
||||
useEffect(() => {
|
||||
setRowsOrder(React.Children.toArray(children));
|
||||
handleRowHover(null, false);
|
||||
}, [children, dragStart]);
|
||||
|
||||
const handleRowHover = (index, hoverstatus = true) => {
|
||||
setHoveredRow(hoverstatus ? index : null);
|
||||
};
|
||||
|
||||
const handleDragStart = (e, index) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', index);
|
||||
setDragStart(index);
|
||||
};
|
||||
|
||||
const handleDragOver = (e, index) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
handleRowHover(index);
|
||||
};
|
||||
|
||||
const handleDrop = (e, toIndex) => {
|
||||
e.preventDefault();
|
||||
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
|
||||
if (fromIndex !== toIndex) {
|
||||
const updatedRowsOrder = [...rowsOrder];
|
||||
const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);
|
||||
updatedRowsOrder.splice(toIndex, 0, movedRow);
|
||||
setRowsOrder(updatedRowsOrder);
|
||||
|
||||
updateReorderedItem({
|
||||
updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
handleRowHover(toIndex);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tbody ref={tbodyRef}>
|
||||
{rowsOrder.map((row, index) => (
|
||||
<tr
|
||||
key={row.props['data-uid']}
|
||||
data-uid={row.props['data-uid']}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, index)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
onMouseEnter={() => handleRowHover(index)}
|
||||
onMouseLeave={() => handleRowHover(index, false)}
|
||||
>
|
||||
{React.Children.map(row.props.children, (child, childIndex) => {
|
||||
if (childIndex === 0) {
|
||||
return React.cloneElement(child, {
|
||||
children: (
|
||||
<>
|
||||
<div
|
||||
draggable
|
||||
className="group drag-handle absolute z-10 left-[-17px] p-3.5 py-3.5 px-2.5 top-[3px] cursor-grab"
|
||||
>
|
||||
{hoveredRow === index && (
|
||||
<>
|
||||
<IconGripVertical
|
||||
size={14}
|
||||
className="z-10 icon-grip rounded-md absolute hidden group-hover:block"
|
||||
/>
|
||||
<IconMinusVertical
|
||||
size={14}
|
||||
className="z-10 icon-minus rounded-md absolute block group-hover:hidden"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{child.props.children}
|
||||
</>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReorderTable;
|
@ -191,6 +191,7 @@ const AssertionRow = ({
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
) : (
|
||||
<input type="text" className="cursor-default" disabled />
|
||||
|
@ -0,0 +1,57 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.auth-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,114 @@
|
||||
import React, { useRef, forwardRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
|
||||
|
||||
const ApiKeyAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const apikeyAuth = item.draft ? get(item, 'draft.request.auth.apikey', {}) : get(item, 'request.auth.apikey', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end auth-type-label select-none">
|
||||
{humanizeRequestAPIKeyPlacement(apikeyAuth?.placement)}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleAuthChange = (property, value) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
...apikeyAuth,
|
||||
[property]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
!apikeyAuth?.placement &&
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
placement: 'header'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [apikeyAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.key || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('key', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Value</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('value', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Add To</label>
|
||||
<div className="inline-flex items-center cursor-pointer auth-placement-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleAuthChange('placement', 'header');
|
||||
}}
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleAuthChange('placement', 'queryparams');
|
||||
}}
|
||||
>
|
||||
Query Params
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyAuth;
|
@ -80,6 +80,15 @@ const AuthMode = ({ item, collection }) => {
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
onModeChange('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
@ -5,6 +5,7 @@ import AwsV4Auth from './AwsV4Auth';
|
||||
import BearerAuth from './BearerAuth';
|
||||
import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
import OAuth2 from './OAuth2/index';
|
||||
@ -32,6 +33,9 @@ const Auth = ({ item, collection }) => {
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} item={item} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} item={item} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
return (
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
|
@ -6,6 +6,9 @@ import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/colle
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import { IconWand } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -13,6 +16,25 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onPrettify = () => {
|
||||
if (!variables) return;
|
||||
try {
|
||||
const edits = format(variables, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyVariables = applyEdits(variables, edits);
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
variables: prettyVariables,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
toast.success('Variables prettified');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Error occurred while prettifying GraphQL variables');
|
||||
}
|
||||
};
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
@ -27,7 +49,14 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<StyledWrapper className="w-full relative">
|
||||
<button
|
||||
className="btn-add-param text-link px-4 py-4 select-none absolute top-0 right-0 z-10"
|
||||
onClick={onPrettify}
|
||||
title={'Prettify'}
|
||||
>
|
||||
<IconWand size={20} strokeWidth={1.5} />
|
||||
</button>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={variables || ''}
|
||||
|
@ -97,6 +97,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
const script = getPropertyFromDraftOrRequest('request.script');
|
||||
const assertions = getPropertyFromDraftOrRequest('request.assertions');
|
||||
const tests = getPropertyFromDraftOrRequest('request.tests');
|
||||
const docs = getPropertyFromDraftOrRequest('request.docs');
|
||||
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
||||
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
|
||||
|
||||
@ -139,10 +140,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
{tests && <ContentIndicator />}
|
||||
{tests && tests.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
|
@ -22,14 +22,12 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
td {
|
||||
&:nth-child(1) {
|
||||
padding: 0 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,14 +7,17 @@ import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
updatePathParam,
|
||||
updateQueryParam
|
||||
moveQueryParam,
|
||||
updatePathParam
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Table from 'components/Table/index';
|
||||
import ReorderTable from 'components/ReorderTable';
|
||||
|
||||
const QueryParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -100,76 +103,75 @@ const QueryParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleParamDrag = ({ updateReorderedItem }) => {
|
||||
dispatch(
|
||||
moveQueryParam({
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
updateReorderedItem
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<StyledWrapper className="w-full flex flex-col absolute">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-2 title text-xs">Query</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div className="mb-1 title text-xs">Query</div>
|
||||
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Name', accessor: 'name', width: '31%' },
|
||||
{ name: 'Path', accessor: 'path', width: '56%' },
|
||||
{ name: '', accessor: '', width: '13%' }
|
||||
]}
|
||||
>
|
||||
<ReorderTable updateReorderedItem={handleParamDrag}>
|
||||
{queryParams && queryParams.length
|
||||
? queryParams.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid}>
|
||||
<td>
|
||||
? queryParams.map((param, index) => (
|
||||
<tr key={param.uid} data-uid={param.uid}>
|
||||
<td className="flex relative">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleQueryParamChange({ target: { value: newValue } }, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
variablesAutocomplete={true}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'name')}
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleQueryParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
|
||||
+ <span>Add Param</span>
|
||||
</button>
|
||||
|
@ -1,14 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconDeviceFloppy, IconArrowRight } from '@tabler/icons';
|
||||
import { IconDeviceFloppy, IconArrowRight, IconCode } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import GenerateCodeItem from 'components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const { theme, storedTheme } = useTheme();
|
||||
@ -17,26 +19,43 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');
|
||||
const isMac = isMacOS();
|
||||
const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.querySelector('.method-selector-container');
|
||||
setMethodSelectorWidth(el.offsetWidth);
|
||||
}, [method]);
|
||||
|
||||
const onSave = () => {
|
||||
const onSave = (finalValue) => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
};
|
||||
|
||||
const onUrlChange = (value) => {
|
||||
if (!editorRef.current?.editor) return;
|
||||
const editor = editorRef.current.editor;
|
||||
const cursor = editor.getCursor();
|
||||
|
||||
const finalUrl = value?.trim() ?? value;
|
||||
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
url: value && typeof value === 'string' ? value.trim() : value
|
||||
url: finalUrl
|
||||
})
|
||||
);
|
||||
|
||||
// Restore cursor position only if URL was trimmed
|
||||
if (finalUrl !== value) {
|
||||
setTimeout(() => {
|
||||
if (editor) {
|
||||
editor.setCursor(cursor);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const onMethodSelect = (verb) => {
|
||||
@ -49,6 +68,15 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleGenerateCode = (e) => {
|
||||
e.stopPropagation();
|
||||
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) {
|
||||
setGenerateCodeItemModalOpen(true);
|
||||
} else {
|
||||
toast.error('URL is required');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
@ -63,8 +91,9 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
}}
|
||||
>
|
||||
<SingleLineEditor
|
||||
ref={editorRef}
|
||||
value={url}
|
||||
onSave={onSave}
|
||||
onSave={(finalValue) => onSave(finalValue)}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
@ -73,6 +102,22 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
item={item}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<div
|
||||
className="infotip mr-3"
|
||||
onClick={(e) => {
|
||||
handleGenerateCode(e);
|
||||
}}
|
||||
>
|
||||
<IconCode
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={'cursor-pointer'}
|
||||
/>
|
||||
<span className="infotiptext text-xs">
|
||||
Generate Code
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="infotip mr-3"
|
||||
onClick={(e) => {
|
||||
@ -94,6 +139,9 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
|
||||
</div>
|
||||
</div>
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -132,6 +132,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -0,0 +1,10 @@
|
||||
const CloseTabIcon = () => (
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default CloseTabIcon;
|
@ -0,0 +1,15 @@
|
||||
const DraftTabIcon = () => (
|
||||
<svg
|
||||
focusable="false"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="8"
|
||||
height="16"
|
||||
fill="#cc7b1b"
|
||||
className="has-changes-icon"
|
||||
viewBox="0 0 8 8"
|
||||
>
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default DraftTabIcon;
|
@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
|
||||
const RequestTabNotFound = ({ handleCloseClick }) => {
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
@ -28,12 +29,7 @@ const RequestTabNotFound = ({ handleCloseClick }) => {
|
||||
) : null}
|
||||
</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
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
<CloseTabIcon />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
|
||||
@ -51,12 +52,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
|
||||
<>
|
||||
<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
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
<CloseTabIcon />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -15,6 +15,8 @@ import StyledWrapper from './StyledWrapper';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
|
||||
import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import DraftTabIcon from './DraftTabIcon';
|
||||
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -49,9 +51,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
if (e.button === 1) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Close the tab
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: [tab.uid]
|
||||
@ -68,7 +71,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
|
||||
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<StyledWrapper
|
||||
className="flex items-center justify-between tab-container px-1"
|
||||
onMouseUp={handleMouseUp} // Add middle-click behavior here
|
||||
>
|
||||
{tab.type === 'folder-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} type={tab.type} tabName={folder?.name} />
|
||||
) : (
|
||||
@ -82,7 +88,17 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
|
||||
if (!item) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<StyledWrapper
|
||||
className="flex items-center justify-between tab-container px-1"
|
||||
onMouseUp={(e) => {
|
||||
if (e.button === 1) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(closeTabs({ tabUids: [tab.uid] }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
@ -166,24 +182,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
}}
|
||||
>
|
||||
{!item.draft ? (
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
<CloseTabIcon />
|
||||
) : (
|
||||
<svg
|
||||
focusable="false"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="8"
|
||||
height="16"
|
||||
fill="#cc7b1b"
|
||||
className="has-changes-icon"
|
||||
viewBox="0 0 8 8"
|
||||
>
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
<DraftTabIcon />
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
@ -29,7 +29,7 @@ const QueryResultPreview = ({
|
||||
setNumPages(numPages);
|
||||
}
|
||||
// Fail safe, so we don't render anything with an invalid tab
|
||||
if (!allowedPreviewModes.includes(previewTab)) {
|
||||
if (!allowedPreviewModes.find((previewMode) => previewMode?.uid == previewTab?.uid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ const QueryResultPreview = ({
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
switch (previewTab) {
|
||||
switch (previewTab?.mode) {
|
||||
case 'preview-web': {
|
||||
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent?.url || ''}">`);
|
||||
return (
|
||||
|
@ -12,18 +12,35 @@ import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { uuid } from 'utils/common/index';
|
||||
|
||||
const formatResponse = (data, mode, filter) => {
|
||||
if (data === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (data === null) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (mode.includes('json')) {
|
||||
let isValidJSON = false;
|
||||
|
||||
try {
|
||||
isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object';
|
||||
} catch (error) {
|
||||
console.log('Error parsing JSON: ', error.message);
|
||||
}
|
||||
|
||||
if (!isValidJSON && typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
try {
|
||||
data = JSONPath({ path: filter, json: data });
|
||||
} catch (e) {
|
||||
console.warn('Could not filter with JSONPath.', e.message);
|
||||
console.warn('Could not apply JSONPath filter:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +52,6 @@ const formatResponse = (data, mode, filter) => {
|
||||
if (typeof parsed === 'string') {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return safeStringifyJSON(parsed, true);
|
||||
}
|
||||
|
||||
@ -43,7 +59,7 @@ const formatResponse = (data, mode, filter) => {
|
||||
return data;
|
||||
}
|
||||
|
||||
return safeStringifyJSON(data);
|
||||
return safeStringifyJSON(data, true);
|
||||
};
|
||||
|
||||
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
|
||||
@ -59,18 +75,18 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
|
||||
const allowedPreviewModes = useMemo(() => {
|
||||
// Always show raw
|
||||
const allowedPreviewModes = ['raw'];
|
||||
const allowedPreviewModes = [{ mode: 'raw', name: 'Raw', uid: uuid() }];
|
||||
|
||||
if (mode.includes('html') && typeof data === 'string') {
|
||||
allowedPreviewModes.unshift('preview-web');
|
||||
allowedPreviewModes.unshift({ mode: 'preview-web', name: 'Web', uid: uuid() });
|
||||
} else if (mode.includes('image')) {
|
||||
allowedPreviewModes.unshift('preview-image');
|
||||
allowedPreviewModes.unshift({ mode: 'preview-image', name: 'Image', uid: uuid() });
|
||||
} else if (contentType.includes('pdf')) {
|
||||
allowedPreviewModes.unshift('preview-pdf');
|
||||
allowedPreviewModes.unshift({ mode: 'preview-pdf', name: 'PDF', uid: uuid() });
|
||||
} else if (contentType.includes('audio')) {
|
||||
allowedPreviewModes.unshift('preview-audio');
|
||||
allowedPreviewModes.unshift({ mode: 'preview-audio', name: 'Audio', uid: uuid() });
|
||||
} else if (contentType.includes('video')) {
|
||||
allowedPreviewModes.unshift('preview-video');
|
||||
allowedPreviewModes.unshift({ mode: 'preview-video', name: 'Video', uid: uuid() });
|
||||
}
|
||||
|
||||
return allowedPreviewModes;
|
||||
@ -79,7 +95,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
const [previewTab, setPreviewTab] = useState(allowedPreviewModes[0]);
|
||||
// Ensure the active Tab is always allowed
|
||||
useEffect(() => {
|
||||
if (!allowedPreviewModes.includes(previewTab)) {
|
||||
if (!allowedPreviewModes.find((previewMode) => previewMode?.uid == previewTab?.uid)) {
|
||||
setPreviewTab(allowedPreviewModes[0]);
|
||||
}
|
||||
}, [previewTab, allowedPreviewModes]);
|
||||
@ -91,12 +107,15 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
|
||||
return allowedPreviewModes.map((previewMode) => (
|
||||
<div
|
||||
className={classnames('select-none capitalize', previewMode === previewTab ? 'active' : 'cursor-pointer')}
|
||||
className={classnames(
|
||||
'select-none capitalize',
|
||||
previewMode?.uid === previewTab?.uid ? 'active' : 'cursor-pointer'
|
||||
)}
|
||||
role="tab"
|
||||
onClick={() => setPreviewTab(previewMode)}
|
||||
key={previewMode}
|
||||
key={previewMode?.uid}
|
||||
>
|
||||
{previewMode.replace(/-(.*)/, ' ')}
|
||||
{previewMode?.name}
|
||||
</div>
|
||||
));
|
||||
}, [allowedPreviewModes, previewTab]);
|
||||
|
@ -17,6 +17,8 @@ const Timeline = ({ request, response }) => {
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<div>
|
||||
@ -31,10 +33,10 @@ const Timeline = ({ request, response }) => {
|
||||
);
|
||||
})}
|
||||
|
||||
{request.data ? (
|
||||
{requestData ? (
|
||||
<pre className="line request">
|
||||
<span className="arrow">{'>'}</span> data{' '}
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{request.data}</pre>
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -41,10 +41,10 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('Collection created');
|
||||
toast.success('Collection created!');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while creating the collection'));
|
||||
.catch((e) => toast.error('An error occurred while creating the collection - ' + e));
|
||||
}
|
||||
});
|
||||
|
||||
@ -72,7 +72,7 @@ const CloneCollection = ({ onClose, collection }) => {
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Clone Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="collection-name" className="flex items-center font-semibold">
|
||||
Name
|
||||
|
@ -25,6 +25,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
onSubmit: (values) => {
|
||||
dispatch(cloneItem(values.name, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Request cloned!');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -49,7 +50,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
{isFolder ? 'Folder' : 'Request'} Name
|
||||
|
@ -8,8 +8,9 @@ const StyledWrapper = styled.div`
|
||||
.generate-code-sidebar {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
min-height: 400px;
|
||||
max-height: 80vh;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.generate-code-item {
|
||||
|
@ -6,56 +6,11 @@ import { isValidUrl } from 'utils/url';
|
||||
import { find, get } from 'lodash';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
|
||||
const languages = [
|
||||
{
|
||||
name: 'HTTP',
|
||||
target: 'http',
|
||||
client: 'http1.1'
|
||||
},
|
||||
{
|
||||
name: 'JavaScript-Fetch',
|
||||
target: 'javascript',
|
||||
client: 'fetch'
|
||||
},
|
||||
{
|
||||
name: 'Javascript-jQuery',
|
||||
target: 'javascript',
|
||||
client: 'jquery'
|
||||
},
|
||||
{
|
||||
name: 'Javascript-axios',
|
||||
target: 'javascript',
|
||||
client: 'axios'
|
||||
},
|
||||
{
|
||||
name: 'Python-Python3',
|
||||
target: 'python',
|
||||
client: 'python3'
|
||||
},
|
||||
{
|
||||
name: 'Python-Requests',
|
||||
target: 'python',
|
||||
client: 'requests'
|
||||
},
|
||||
{
|
||||
name: 'PHP',
|
||||
target: 'php',
|
||||
client: 'curl'
|
||||
},
|
||||
{
|
||||
name: 'Shell-curl',
|
||||
target: 'shell',
|
||||
client: 'curl'
|
||||
},
|
||||
{
|
||||
name: 'Shell-httpie',
|
||||
target: 'shell',
|
||||
client: 'httpie'
|
||||
}
|
||||
];
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
|
||||
const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
const languages = getLanguages();
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
let envVars = {};
|
||||
if (environment) {
|
||||
|
@ -5,6 +5,7 @@ import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isItemAFolder } from 'utils/tabs';
|
||||
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -27,8 +28,14 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
if (!isFolder && item.draft) {
|
||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||
}
|
||||
dispatch(renameItem(values.name, item.uid, collection.uid));
|
||||
onClose();
|
||||
dispatch(renameItem(values.name, item.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Request renamed!');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occurred while renaming the request');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -48,7 +55,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
{isFolder ? 'Folder' : 'Request'} Name
|
||||
|
@ -23,43 +23,53 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const runLength = item ? get(item, 'items.length', 0) : get(collection, 'items.length', 0);
|
||||
const items = flattenItems(item ? item.items : collection.items);
|
||||
const requestItems = items.filter((item) => item.type !== 'folder');
|
||||
const recursiveRunLength = requestItems.length;
|
||||
const getRequestsCount = (items) => {
|
||||
const requestTypes = ['http-request', 'graphql-request']
|
||||
return items.filter(req => requestTypes.includes(req.type)).length;
|
||||
}
|
||||
|
||||
const runLength = item ? getRequestsCount(item.items) : get(collection, 'items.length', 0);
|
||||
const flattenedItems = flattenItems(item ? item.items : collection.items);
|
||||
const recursiveRunLength = getRequestsCount(flattenedItems);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="Collection Runner" hideFooter={true} handleCancel={onClose}>
|
||||
<div className="mb-1">
|
||||
<span className="font-medium">Run</span>
|
||||
<span className="ml-1 text-xs">({runLength} requests)</span>
|
||||
</div>
|
||||
<div className="mb-8">This will only run the requests in this folder.</div>
|
||||
{!runLength && !recursiveRunLength ? (
|
||||
<div className="mb-8">No request found in this folder.</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="mb-1">
|
||||
<span className="font-medium">Run</span>
|
||||
<span className="ml-1 text-xs">({runLength} requests)</span>
|
||||
</div>
|
||||
<div className="mb-8">This will only run the requests in this folder.</div>
|
||||
|
||||
<div className="mb-1">
|
||||
<span className="font-medium">Recursive Run</span>
|
||||
<span className="ml-1 text-xs">({recursiveRunLength} requests)</span>
|
||||
</div>
|
||||
<div className="mb-8">This will run all the requests in this folder and all its subfolders.</div>
|
||||
<div className="mb-1">
|
||||
<span className="font-medium">Recursive Run</span>
|
||||
<span className="ml-1 text-xs">({recursiveRunLength} requests)</span>
|
||||
</div>
|
||||
<div className="mb-8">This will run all the requests in this folder and all its subfolders.</div>
|
||||
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className="mr-3">
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className="mr-3">
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" disabled={!recursiveRunLength} className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" disabled={!runLength} className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
@ -65,7 +65,7 @@ const Wrapper = styled.div`
|
||||
div.dropdown-item.delete-item {
|
||||
color: ${(props) => props.theme.colors.danger};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
background-color: ${(props) => props.theme.colors.bg.danger} !important;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
@ -21,9 +21,14 @@ const RenameCollection = ({ collection, onClose }) => {
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(renameCollection(values.name, collection.uid));
|
||||
toast.success('Collection renamed!');
|
||||
onClose();
|
||||
dispatch(renameCollection(values.name, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection renamed!');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err ? err.message : 'An error occurred while renaming the collection');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -37,7 +42,7 @@ const RenameCollection = ({ collection, onClose }) => {
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Rename Collection" confirmText="Rename" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Name
|
||||
|
@ -34,10 +34,10 @@ const CreateCollection = ({ onClose }) => {
|
||||
onSubmit: (values) => {
|
||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||
.then(() => {
|
||||
toast.success('Collection created');
|
||||
toast.success('Collection created!');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while creating the collection'));
|
||||
.catch((e) => toast.error('An error occurred while creating the collection - ' + e));
|
||||
}
|
||||
});
|
||||
|
||||
@ -65,7 +65,7 @@ const CreateCollection = ({ onClose }) => {
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="collection-name" className="flex items-center font-semibold">
|
||||
Name
|
||||
|
@ -144,7 +144,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
|
@ -32,7 +32,10 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
|
||||
.then(() => onClose())
|
||||
.then(() => {
|
||||
toast.success('New folder created!');
|
||||
onClose()
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the folder'));
|
||||
}
|
||||
});
|
||||
@ -47,7 +50,7 @@ const NewFolder = ({ collection, item, onClose }) => {
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="New Folder" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="folderName" className="block font-semibold">
|
||||
Folder Name
|
||||
|
@ -113,7 +113,10 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
auth: request.auth
|
||||
})
|
||||
)
|
||||
.then(() => onClose())
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onClose()
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
} else {
|
||||
dispatch(
|
||||
@ -126,7 +129,10 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
itemUid: item ? item.uid : null
|
||||
})
|
||||
)
|
||||
.then(() => onClose())
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onClose()
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}
|
||||
}
|
||||
@ -162,16 +168,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form
|
||||
className="bruno-form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Type
|
||||
|
@ -82,16 +82,12 @@ const TitleBar = () => {
|
||||
) : null}
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center cursor-pointer" onClick={handleTitleClick}>
|
||||
<Bruno width={30} />
|
||||
</div>
|
||||
<div
|
||||
onClick={handleTitleClick}
|
||||
className="flex items-center font-medium select-none cursor-pointer"
|
||||
style={{ fontSize: 14, paddingLeft: 6, position: 'relative', top: -1 }}
|
||||
>
|
||||
<button className="flex items-center gap-2 text-sm font-medium" onClick={handleTitleClick}>
|
||||
<span aria-hidden>
|
||||
<Bruno width={30} />
|
||||
</span>
|
||||
bruno
|
||||
</div>
|
||||
</button>
|
||||
<div className="collection-dropdown flex flex-grow items-center justify-end">
|
||||
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
|
@ -20,7 +20,7 @@ const MAX_LEFT_SIDEBAR_WIDTH = 600;
|
||||
const Sidebar = () => {
|
||||
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const preferencesOpen = useSelector((state) => state.app.showPreferences);
|
||||
const [goldenEditonOpen, setGoldenEditonOpen] = useState(false);
|
||||
const [goldenEditionOpen, setGoldenEditionOpen] = useState(false);
|
||||
|
||||
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
|
||||
const [cookiesOpen, setCookiesOpen] = useState(false);
|
||||
@ -83,10 +83,43 @@ const Sidebar = () => {
|
||||
return (
|
||||
<StyledWrapper className="flex relative h-screen">
|
||||
<aside>
|
||||
{goldenEditonOpen && <GoldenEdition onClose={() => setGoldenEditonOpen(false)} />}
|
||||
{goldenEditionOpen && (
|
||||
<GoldenEdition
|
||||
onClose={() => {
|
||||
setGoldenEditionOpen(false);
|
||||
document.querySelector('[data-trigger="golden-edition"]').focus();
|
||||
}}
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
aria-labelledby="golden-edition-title"
|
||||
aria-describedby="golden-edition-description"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row h-screen w-full">
|
||||
{preferencesOpen && <Preferences onClose={() => dispatch(showPreferences(false))} />}
|
||||
{cookiesOpen && <Cookies onClose={() => setCookiesOpen(false)} />}
|
||||
{preferencesOpen && (
|
||||
<Preferences
|
||||
onClose={() => {
|
||||
dispatch(showPreferences(false));
|
||||
document.querySelector('[data-trigger="preferences"]').focus();
|
||||
}}
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
aria-labelledby="preferences-title"
|
||||
aria-describedby="preferences-description"
|
||||
/>
|
||||
)}
|
||||
{cookiesOpen && (
|
||||
<Cookies
|
||||
onClose={() => {
|
||||
setCookiesOpen(false);
|
||||
document.querySelector('[data-trigger="cookies"]').focus();
|
||||
}}
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
aria-labelledby="cookies-title"
|
||||
aria-describedby="cookies-description"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col w-full" style={{ width: asideWidth }}>
|
||||
<div className="flex flex-col flex-grow">
|
||||
@ -96,30 +129,50 @@ const Sidebar = () => {
|
||||
|
||||
<div className="footer flex px-1 py-2 absolute bottom-0 left-0 right-0 items-center select-none">
|
||||
<div className="flex items-center ml-1 text-xs ">
|
||||
<a className="mr-2 cursor-pointer" onClick={() => dispatch(showPreferences(true))}>
|
||||
<ToolHint text="Preferences" toolhintId="Preferences" effect='float' place='top-start' offset={8}>
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</ToolHint>
|
||||
</a>
|
||||
<a
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
>
|
||||
<ToolHint text="Cookies" toolhintId="Cookies" offset={8}>
|
||||
<IconCookie size={18} strokeWidth={1.5} />
|
||||
</ToolHint>
|
||||
</a>
|
||||
<a
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setGoldenEditonOpen(true)}
|
||||
>
|
||||
<ToolHint text="Golden Edition" toolhintId="Golden Edition" offset={8} >
|
||||
<IconHeart size={18} strokeWidth={1.5} />
|
||||
</ToolHint>
|
||||
</a>
|
||||
<a>
|
||||
<Notifications />
|
||||
</a>
|
||||
<ul role="menubar" className="flex space-x-2">
|
||||
<li role="menuitem">
|
||||
<a
|
||||
className="cursor-pointer"
|
||||
data-trigger="preferences"
|
||||
onClick={() => dispatch(showPreferences(true))}
|
||||
tabIndex={0}
|
||||
aria-label="Open Preferences"
|
||||
>
|
||||
<ToolHint text="Preferences" toolhintId="Preferences" effect="float" place="top-start" offset={8}>
|
||||
<IconSettings size={18} strokeWidth={1.5} aria-hidden="true" />
|
||||
</ToolHint>
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a
|
||||
className="cursor-pointer"
|
||||
data-trigger="cookies"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
tabIndex={0}
|
||||
aria-label="Open Cookies Settings"
|
||||
>
|
||||
<ToolHint text="Cookies" toolhintId="Cookies" offset={8}>
|
||||
<IconCookie size={18} strokeWidth={1.5} aria-hidden="true" />
|
||||
</ToolHint>
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a
|
||||
className="cursor-pointer"
|
||||
data-trigger="golden-edition"
|
||||
onClick={() => setGoldenEditionOpen(true)}
|
||||
tabIndex={0}
|
||||
aria-label="Open Golden Edition"
|
||||
>
|
||||
<ToolHint text="Golden Edition" toolhintId="Golden Edition" offset={8}>
|
||||
<IconHeart size={18} strokeWidth={1.5} aria-hidden="true" />
|
||||
</ToolHint>
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<Notifications />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
|
||||
{/* This will get moved to home page */}
|
||||
@ -132,15 +185,16 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.27.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.30.1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="absolute drag-sidebar h-full" onMouseDown={handleDragbarMouseDown}>
|
||||
<div className="drag-request-border" />
|
||||
</div>
|
||||
</StyledWrapper >
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
64
packages/bruno-app/src/components/Table/StyledWrapper.js
Normal file
64
packages/bruno-app/src/components/Table/StyledWrapper.js
Normal file
@ -0,0 +1,64 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
|
||||
// for icon hover
|
||||
position: inherit;
|
||||
left: -4px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
grid-template-columns: ${({ columns }) =>
|
||||
columns?.[0]?.width
|
||||
? columns.map((col) => `${col?.width}`).join(' ')
|
||||
: columns.map((col) => `${100 / columns.length}%`).join(' ')};
|
||||
}
|
||||
|
||||
table thead,
|
||||
table tbody,
|
||||
table tr {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
table th {
|
||||
position: relative;
|
||||
border-bottom: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
}
|
||||
|
||||
tr {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
tr.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
tr.hovered {
|
||||
transform: translateY(10px); /* Adjust the value as needed for the animation effect */
|
||||
}
|
||||
|
||||
table tr th {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
|
||||
&:nth-child(1) {
|
||||
border-left: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
110
packages/bruno-app/src/components/Table/index.js
Normal file
110
packages/bruno-app/src/components/Table/index.js
Normal file
@ -0,0 +1,110 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Table = ({ minColumnWidth = 1, headers = [], children }) => {
|
||||
const [activeColumnIndex, setActiveColumnIndex] = useState(null);
|
||||
const tableRef = useRef(null);
|
||||
|
||||
const columns = headers?.map((item) => ({
|
||||
...item,
|
||||
ref: useRef()
|
||||
}));
|
||||
|
||||
const updateDivHeights = () => {
|
||||
if (tableRef.current) {
|
||||
const height = tableRef.current.offsetHeight;
|
||||
columns.forEach((col) => {
|
||||
if (col.ref.current) {
|
||||
col.ref.current.querySelector('.resizer').style.height = `${height}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateDivHeights();
|
||||
window.addEventListener('resize', updateDivHeights);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDivHeights);
|
||||
};
|
||||
}, [columns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableRef.current) {
|
||||
const observer = new MutationObserver(updateDivHeights);
|
||||
observer.observe(tableRef.current, { childList: true, subtree: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
const handleMouseDown = (index) => (e) => {
|
||||
setActiveColumnIndex(index);
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e) => {
|
||||
const gridColumns = columns.map((col, i) => {
|
||||
if (i === activeColumnIndex) {
|
||||
const width = e.clientX - col.ref?.current?.getBoundingClientRect()?.left;
|
||||
|
||||
if (width >= minColumnWidth) {
|
||||
return `${width}px`;
|
||||
}
|
||||
}
|
||||
return `${col.ref.current.offsetWidth}px`;
|
||||
});
|
||||
|
||||
tableRef.current.style.gridTemplateColumns = `${gridColumns.join(' ')}`;
|
||||
},
|
||||
[activeColumnIndex, columns, minColumnWidth]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setActiveColumnIndex(null);
|
||||
removeListeners();
|
||||
}, [removeListeners]);
|
||||
|
||||
const removeListeners = useCallback(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', removeListeners);
|
||||
}, [handleMouseMove]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeColumnIndex !== null) {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
return () => {
|
||||
removeListeners();
|
||||
};
|
||||
}, [activeColumnIndex, handleMouseMove, handleMouseUp, removeListeners]);
|
||||
|
||||
return (
|
||||
<StyledWrapper columns={columns}>
|
||||
<div className="relative">
|
||||
<table ref={tableRef} className="px-4 inherit left-[4px]">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ ref, name }, i) => (
|
||||
<th ref={ref} key={name} title={name}>
|
||||
<span>{name}</span>
|
||||
<div
|
||||
className="resizer absolute cursor-col-resize w-[4px] right-[-2px] top-0 z-10 opacity-50 hover:bg-blue-500 active:bg-blue-500"
|
||||
onMouseDown={handleMouseDown(i)}
|
||||
></div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
53
packages/bruno-app/src/hooks/useFocusTrap/index.js
Normal file
53
packages/bruno-app/src/hooks/useFocusTrap/index.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const useFocusTrap = (modalRef) => {
|
||||
|
||||
// refer to this implementation for modal focus: https://stackoverflow.com/a/38865836
|
||||
const focusableSelector = 'a[href], area[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex]:not([tabindex="-1"]), *[contenteditable]';
|
||||
|
||||
useEffect(() => {
|
||||
const modalElement = modalRef.current;
|
||||
if (!modalElement) return;
|
||||
|
||||
const focusableElements = Array.from(document.querySelectorAll(focusableSelector));
|
||||
const modalFocusableElements = Array.from(modalElement.querySelectorAll(focusableSelector));
|
||||
const elementsToHide = focusableElements.filter(el => !modalFocusableElements.includes(el));
|
||||
|
||||
// Hide elements outside the modal
|
||||
elementsToHide.forEach(el => {
|
||||
const originalTabIndex = el.getAttribute('tabindex');
|
||||
el.setAttribute('data-tabindex', originalTabIndex || 'inline');
|
||||
el.setAttribute('tabindex', -1);
|
||||
});
|
||||
|
||||
// Set focus to the first focusable element in the modal
|
||||
const firstElement = modalFocusableElements[0];
|
||||
const lastElement = modalFocusableElements[modalFocusableElements.length - 1];
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Tab') {
|
||||
if (event.shiftKey && document.activeElement === firstElement) {
|
||||
event.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === lastElement) {
|
||||
event.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
modalElement.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
modalElement.removeEventListener('keydown', handleKeyDown);
|
||||
|
||||
// Restore original tabindex values
|
||||
elementsToHide.forEach(el => {
|
||||
const originalTabIndex = el.getAttribute('data-tabindex');
|
||||
el.setAttribute('tabindex', originalTabIndex === 'inline' ? '' : originalTabIndex);
|
||||
});
|
||||
};
|
||||
}, [modalRef]);
|
||||
};
|
||||
|
||||
export default useFocusTrap;
|
@ -1,5 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { showPreferences, updateCookies, updatePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import {
|
||||
showPreferences,
|
||||
updateCookies,
|
||||
updatePreferences,
|
||||
updateSystemProxyEnvVariables
|
||||
} from 'providers/ReduxStore/slices/app';
|
||||
import {
|
||||
brunoConfigUpdateEvent,
|
||||
collectionAddDirectoryEvent,
|
||||
@ -136,6 +141,10 @@ const useIpcEvents = () => {
|
||||
dispatch(updatePreferences(val));
|
||||
});
|
||||
|
||||
const removeSystemProxyEnvUpdatesListener = ipcRenderer.on('main:load-system-proxy-env', (val) => {
|
||||
dispatch(updateSystemProxyEnvVariables(val));
|
||||
});
|
||||
|
||||
const removeCookieUpdateListener = ipcRenderer.on('main:cookies-update', (val) => {
|
||||
dispatch(updateCookies(val));
|
||||
});
|
||||
@ -155,6 +164,7 @@ const useIpcEvents = () => {
|
||||
removeShowPreferencesListener();
|
||||
removePreferencesUpdatesListener();
|
||||
removeCookieUpdateListener();
|
||||
removeSystemProxyEnvUpdatesListener();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.27.0'
|
||||
version: '1.30.1'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -7,9 +7,9 @@ import SaveRequest from 'components/RequestPane/SaveRequest';
|
||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
export const HotkeysContext = React.createContext();
|
||||
|
||||
@ -54,6 +54,8 @@ export const HotkeysProvider = (props) => {
|
||||
const item = findItemInCollection(collection, activeTab.uid);
|
||||
if (item && item.uid) {
|
||||
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
|
||||
} else if (activeTab.type === 'collection-settings') {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
} else {
|
||||
// todo: when ephermal requests go live
|
||||
// setShowSaveRequestModal(true);
|
||||
@ -154,7 +156,41 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid]);
|
||||
|
||||
// close all tabs
|
||||
// Switch to the previous tab
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => {
|
||||
dispatch(
|
||||
switchTab({
|
||||
direction: 'pageup'
|
||||
})
|
||||
);
|
||||
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+pageup', 'ctrl+pageup']);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// Switch to the next tab
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => {
|
||||
dispatch(
|
||||
switchTab({
|
||||
direction: 'pagedown'
|
||||
})
|
||||
);
|
||||
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// Close all tabs
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
|
@ -27,7 +27,8 @@ const initialState = {
|
||||
}
|
||||
},
|
||||
cookies: [],
|
||||
taskQueue: []
|
||||
taskQueue: [],
|
||||
systemProxyEnvVariables: {}
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
@ -72,6 +73,9 @@ export const appSlice = createSlice({
|
||||
},
|
||||
removeAllTasksFromQueue: (state) => {
|
||||
state.taskQueue = [];
|
||||
},
|
||||
updateSystemProxyEnvVariables: (state, action) => {
|
||||
state.systemProxyEnvVariables = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -89,7 +93,8 @@ export const {
|
||||
updateCookies,
|
||||
insertTaskIntoQueue,
|
||||
removeTaskFromQueue,
|
||||
removeAllTasksFromQueue
|
||||
removeAllTasksFromQueue,
|
||||
updateSystemProxyEnvVariables
|
||||
} = appSlice.actions;
|
||||
|
||||
export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||
|
@ -477,6 +477,10 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.auth.mode = 'oauth2';
|
||||
item.draft.request.auth.oauth2 = action.payload.content;
|
||||
break;
|
||||
case 'apikey':
|
||||
item.draft.request.auth.mode = 'apikey';
|
||||
item.draft.request.auth.apikey = action.payload.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -503,6 +507,39 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
moveQueryParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
// Ensure item.draft is a deep clone of item if not already present
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
// Extract payload data
|
||||
const { updateReorderedItem } = action.payload;
|
||||
const params = item.draft.request.params;
|
||||
|
||||
item.draft.request.params = updateReorderedItem.map((uid) => {
|
||||
return params.find((param) => param.uid === uid);
|
||||
});
|
||||
|
||||
// update request url
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
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 {
|
||||
item.draft.request.url = parts[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateQueryParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -1104,6 +1141,9 @@ export const collectionsSlice = createSlice({
|
||||
case 'oauth2':
|
||||
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
case 'apikey':
|
||||
set(collection, 'root.request.auth.apikey', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1302,6 +1342,71 @@ export const collectionsSlice = createSlice({
|
||||
set(collection, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
addCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (collection) {
|
||||
if (type === 'request') {
|
||||
const vars = get(collection, 'root.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
const vars = get(collection, 'root.request.vars.res', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'root.request.vars.req', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'root.request.vars.res', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
},
|
||||
deleteCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (collection) {
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'root.request.vars.req', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'root.request.vars.res', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const isCollectionRoot = file.meta.collectionRoot ? true : false;
|
||||
@ -1655,6 +1760,7 @@ export const {
|
||||
requestUrlChanged,
|
||||
updateAuth,
|
||||
addQueryParam,
|
||||
moveQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
updatePathParam,
|
||||
@ -1694,6 +1800,9 @@ export const {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
addCollectionVar,
|
||||
updateCollectionVar,
|
||||
deleteCollectionVar,
|
||||
updateCollectionAuthMode,
|
||||
updateCollectionAuth,
|
||||
updateCollectionRequestScript,
|
||||
|
@ -48,6 +48,26 @@ export const tabsSlice = createSlice({
|
||||
focusTab: (state, action) => {
|
||||
state.activeTabUid = action.payload.uid;
|
||||
},
|
||||
switchTab: (state, action) => {
|
||||
if (!state.tabs || !state.tabs.length) {
|
||||
state.activeTabUid = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const direction = action.payload.direction;
|
||||
|
||||
const activeTabIndex = state.tabs.findIndex((t) => t.uid === state.activeTabUid);
|
||||
|
||||
let toBeActivatedTabIndex = 0;
|
||||
|
||||
if (direction == 'pageup') {
|
||||
toBeActivatedTabIndex = (activeTabIndex - 1 + state.tabs.length) % state.tabs.length;
|
||||
} else if (direction == 'pagedown') {
|
||||
toBeActivatedTabIndex = (activeTabIndex + 1) % state.tabs.length;
|
||||
}
|
||||
|
||||
state.activeTabUid = state.tabs[toBeActivatedTabIndex].uid;
|
||||
},
|
||||
updateRequestPaneTabWidth: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
@ -111,6 +131,7 @@ export const tabsSlice = createSlice({
|
||||
export const {
|
||||
addTab,
|
||||
focusTab,
|
||||
switchTab,
|
||||
updateRequestPaneTabWidth,
|
||||
updateRequestPaneTab,
|
||||
updateResponsePaneTab,
|
||||
|
31
packages/bruno-app/src/utils/codegenerator/targets.js
Normal file
31
packages/bruno-app/src/utils/codegenerator/targets.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { targets } from 'httpsnippet';
|
||||
|
||||
export const getLanguages = () => {
|
||||
const allLanguages = [];
|
||||
for (const target of Object.values(targets)) {
|
||||
const { key, title } = target.info;
|
||||
const clients = Object.keys(target.clientsById);
|
||||
const languages =
|
||||
(clients.length === 1)
|
||||
? [{
|
||||
name: title,
|
||||
target: key,
|
||||
client: clients[0]
|
||||
}]
|
||||
: clients.map(client => ({
|
||||
name: `${title}-${client}`,
|
||||
target: key,
|
||||
client
|
||||
}));
|
||||
allLanguages.push(...languages);
|
||||
|
||||
// Move "Shell-curl" to the top of the array
|
||||
const shellCurlIndex = allLanguages.findIndex(lang => lang.name === "Shell-curl");
|
||||
if (shellCurlIndex !== -1) {
|
||||
const [shellCurl] = allLanguages.splice(shellCurlIndex, 1);
|
||||
allLanguages.unshift(shellCurl);
|
||||
}
|
||||
}
|
||||
|
||||
return allLanguages;
|
||||
};
|
@ -372,6 +372,14 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'apikey':
|
||||
di.request.auth.apikey = {
|
||||
key: get(si.request, 'auth.apikey.key', ''),
|
||||
value: get(si.request, 'auth.apikey.value', ''),
|
||||
placement: get(si.request, 'auth.apikey.placement', 'header')
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -661,6 +669,26 @@ export const humanizeRequestAuthMode = (mode) => {
|
||||
label = 'OAuth 2.0';
|
||||
break;
|
||||
}
|
||||
case 'apikey': {
|
||||
label = 'API Key';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
export const humanizeRequestAPIKeyPlacement = (placement) => {
|
||||
let label = 'Header';
|
||||
switch (placement) {
|
||||
case 'header': {
|
||||
label = 'Header';
|
||||
break;
|
||||
}
|
||||
case 'queryparams': {
|
||||
label = 'Query Params';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
@ -787,24 +815,25 @@ export const getTotalRequestCountInCollection = (collection) => {
|
||||
};
|
||||
|
||||
export const getAllVariables = (collection, item) => {
|
||||
const environmentVariables = getEnvironmentVariables(collection);
|
||||
let requestVariables = {};
|
||||
if (item?.request) {
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
requestVariables = mergeFolderLevelVars(item?.request, requestTreePath);
|
||||
}
|
||||
const envVariables = getEnvironmentVariables(collection);
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);
|
||||
const pathParams = getPathParams(item);
|
||||
|
||||
const { processEnvVariables = {}, runtimeVariables = {} } = collection;
|
||||
|
||||
return {
|
||||
...environmentVariables,
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...collection.runtimeVariables,
|
||||
...runtimeVariables,
|
||||
pathParams: {
|
||||
...pathParams
|
||||
},
|
||||
process: {
|
||||
env: {
|
||||
...collection.processEnvVariables
|
||||
...processEnvVariables
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -831,14 +860,22 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const mergeFolderLevelVars = (request, requestTreePath = []) => {
|
||||
const mergeVars = (collection, requestTreePath = []) => {
|
||||
let collectionVariables = {};
|
||||
let folderVariables = {};
|
||||
let requestVariables = {};
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
collectionVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
requestVariables[_var.name] = _var.value;
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -850,6 +887,9 @@ const mergeFolderLevelVars = (request, requestTreePath = []) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return requestVariables;
|
||||
return {
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,105 @@
|
||||
import map from 'lodash/map';
|
||||
import * as FileSaver from 'file-saver';
|
||||
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from 'utils/collections/export';
|
||||
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../collections/export';
|
||||
|
||||
/**
|
||||
* Transforms a given URL string into an object representing the protocol, host, path, query, and variables.
|
||||
*
|
||||
* @param {string} url - The raw URL to be transformed.
|
||||
* @param {Object} params - The params object.
|
||||
* @returns {Object|null} An object containing the URL's protocol, host, path, query, and variables, or {} if an error occurs.
|
||||
*/
|
||||
export const transformUrl = (url, params) => {
|
||||
if (typeof url !== 'string' || !url.trim()) {
|
||||
throw new Error("Invalid URL input");
|
||||
}
|
||||
|
||||
const urlRegexPatterns = {
|
||||
protocolAndRestSeparator: /:\/\//,
|
||||
hostAndPathSeparator: /\/(.+)/,
|
||||
domainSegmentSeparator: /\./,
|
||||
pathSegmentSeparator: /\//,
|
||||
queryStringSeparator: /\?/
|
||||
};
|
||||
|
||||
const postmanUrl = { raw: url };
|
||||
|
||||
/**
|
||||
* Splits a URL into its protocol, host and path.
|
||||
*
|
||||
* @param {string} url - The URL to be split.
|
||||
* @returns {Object} An object containing the protocol and the raw host/path string.
|
||||
*/
|
||||
const splitUrl = (url) => {
|
||||
const urlParts = url.split(urlRegexPatterns.protocolAndRestSeparator);
|
||||
if (urlParts.length === 1) {
|
||||
return { protocol: '', rawHostAndPath: urlParts[0] };
|
||||
} else if (urlParts.length === 2) {
|
||||
const [hostAndPath, _] = urlParts[1].split(urlRegexPatterns.queryStringSeparator);
|
||||
return { protocol: urlParts[0], rawHostAndPath: hostAndPath };
|
||||
} else {
|
||||
throw new Error(`Invalid URL format: ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits the host and path from a raw host/path string.
|
||||
*
|
||||
* @param {string} rawHostAndPath - The raw host and path string to be split.
|
||||
* @returns {Object} An object containing the host and path.
|
||||
*/
|
||||
const splitHostAndPath = (rawHostAndPath) => {
|
||||
const [host, path = ''] = rawHostAndPath.split(urlRegexPatterns.hostAndPathSeparator);
|
||||
return { host, path };
|
||||
};
|
||||
|
||||
try {
|
||||
const { protocol, rawHostAndPath } = splitUrl(url);
|
||||
postmanUrl.protocol = protocol;
|
||||
|
||||
const { host, path } = splitHostAndPath(rawHostAndPath);
|
||||
postmanUrl.host = host ? host.split(urlRegexPatterns.domainSegmentSeparator) : [];
|
||||
postmanUrl.path = path ? path.split(urlRegexPatterns.pathSegmentSeparator) : [];
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Construct query params.
|
||||
postmanUrl.query = params
|
||||
.filter((param) => param.type === 'query')
|
||||
.map(({ name, value, description }) => ({ key: name, value, description }));
|
||||
|
||||
// Construct path params.
|
||||
postmanUrl.variable = params
|
||||
.filter((param) => param.type === 'path')
|
||||
.map(({ name, value, description }) => ({ key: name, value, description }));
|
||||
|
||||
return postmanUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapses multiple consecutive slashes (`//`) into a single slash, while skipping the protocol (e.g., `http://` or `https://`).
|
||||
*
|
||||
* @param {String} url - A URL string
|
||||
* @returns {String} The sanitized URL
|
||||
*
|
||||
*/
|
||||
const collapseDuplicateSlashes = (url) => {
|
||||
return url.replace(/(?<!:)\/{2,}/g, '/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces all `\\` (backslashes) with `//` (forward slashes) and collapses multiple slashes into one.
|
||||
*
|
||||
* @param {string} url - The URL to sanitize.
|
||||
* @returns {string} The sanitized URL.
|
||||
*
|
||||
*/
|
||||
export const sanitizeUrl = (url) => {
|
||||
let sanitizedUrl = collapseDuplicateSlashes(url.replace(/\\/g, '//'));
|
||||
return sanitizedUrl;
|
||||
};
|
||||
|
||||
export const exportCollection = (collection) => {
|
||||
delete collection.uid;
|
||||
@ -147,7 +246,7 @@ export const exportCollection = (collection) => {
|
||||
};
|
||||
|
||||
const generateAuth = (itemAuth) => {
|
||||
switch (itemAuth) {
|
||||
switch (itemAuth?.mode) {
|
||||
case 'bearer':
|
||||
return {
|
||||
type: 'bearer',
|
||||
@ -174,52 +273,41 @@ export const exportCollection = (collection) => {
|
||||
]
|
||||
};
|
||||
}
|
||||
case 'apikey': {
|
||||
return {
|
||||
type: 'apikey',
|
||||
apikey: [
|
||||
{
|
||||
key: 'key',
|
||||
value: itemAuth.apikey.key,
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
value: itemAuth.apikey.value,
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
default: {
|
||||
console.error('Unsupported auth mode:', itemAuth.mode);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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),
|
||||
auth: generateAuth(itemRequest.auth),
|
||||
description: itemRequest.docs,
|
||||
url: {
|
||||
raw: itemRequest.url,
|
||||
host: generateHost(itemRequest.url),
|
||||
path: generatePathParams(itemRequest.params),
|
||||
query: generateQueryParams(itemRequest.params),
|
||||
variable: generateVariables(itemRequest.params)
|
||||
},
|
||||
auth: generateAuth(itemRequest.auth)
|
||||
// We sanitize the URL to make sure it's in the right format before passing it to the transformUrl func. This means changing backslashes to forward slashes and reducing multiple slashes to a single one, except in the protocol part.
|
||||
url: transformUrl(sanitizeUrl(itemRequest.url), itemRequest.params)
|
||||
};
|
||||
|
||||
if (itemRequest.body.mode != 'none') {
|
||||
if (itemRequest.body.mode !== 'none') {
|
||||
requestObject.body = generateBody(itemRequest.body);
|
||||
}
|
||||
return requestObject;
|
||||
|
@ -0,0 +1,81 @@
|
||||
const { sanitizeUrl, transformUrl } = require('./postman-collection');
|
||||
|
||||
describe('transformUrl', () => {
|
||||
it('should handle basic URL with path variables', () => {
|
||||
const url = 'https://example.com/{{username}}/api/resource/:id';
|
||||
const params = [
|
||||
{ name: 'id', value: '123', type: 'path' },
|
||||
];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'https://example.com/{{username}}/api/resource/:id',
|
||||
protocol: 'https',
|
||||
host: ['example', 'com'],
|
||||
path: ['{{username}}', 'api', 'resource', ':id'],
|
||||
query: [],
|
||||
variable: [
|
||||
{ key: 'id', value: '123' },
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL with query parameters', () => {
|
||||
const url = 'https://example.com/api/resource?limit=10&offset=20';
|
||||
const params = [
|
||||
{ name: 'limit', value: '10', type: 'query' },
|
||||
{ name: 'offset', value: '20', type: 'query' }
|
||||
];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'https://example.com/api/resource?limit=10&offset=20',
|
||||
protocol: 'https',
|
||||
host: ['example', 'com'],
|
||||
path: ['api', 'resource'],
|
||||
query: [
|
||||
{ key: 'limit', value: '10' },
|
||||
{ key: 'offset', value: '20' }
|
||||
],
|
||||
variable: []
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL without protocol', () => {
|
||||
const url = 'example.com/api/resource';
|
||||
const params = [];
|
||||
|
||||
const result = transformUrl(url, params);
|
||||
|
||||
expect(result).toEqual({
|
||||
raw: 'example.com/api/resource',
|
||||
protocol: '',
|
||||
host: ['example', 'com'],
|
||||
path: ['api', 'resource'],
|
||||
query: [],
|
||||
variable: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeUrl', () => {
|
||||
it('should replace backslashes with slashes', () => {
|
||||
const input = 'http:\\\\example.com\\path\\to\\file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should collapse multiple slashes into a single slash', () => {
|
||||
const input = 'http://example.com//path///to////file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle URLs with mixed slashes', () => {
|
||||
const input = 'http:\\example.com//path\\to//file';
|
||||
const expected = 'http://example.com/path/to/file';
|
||||
expect(sanitizeUrl(input)).toBe(expected);
|
||||
});
|
||||
})
|
@ -174,7 +174,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||
} else if (mimeType === 'text/plain') {
|
||||
brunoRequestItem.request.body.mode = 'text';
|
||||
brunoRequestItem.request.body.text = request.body.text;
|
||||
} else if (mimeType === 'text/xml') {
|
||||
} else if (mimeType === 'text/xml' || mimeType === 'application/xml') {
|
||||
brunoRequestItem.request.body.mode = 'xml';
|
||||
brunoRequestItem.request.body.xml = request.body.text;
|
||||
} else if (mimeType === 'application/graphql') {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user