mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
90 Commits
v2.5.0
...
feature/ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3a99a4d85 | ||
|
|
bbfa2b39a0 | ||
|
|
1a93eabf01 | ||
|
|
df1c5f9363 | ||
|
|
803d2d96c9 | ||
|
|
99873af281 | ||
|
|
1b63798ff3 | ||
|
|
c90d607046 | ||
|
|
c6c3931446 | ||
|
|
10e872c6ab | ||
|
|
6792cc26bd | ||
|
|
c76d99d1b0 | ||
|
|
b813c916b8 | ||
|
|
fab9d00566 | ||
|
|
afcd7395d9 | ||
|
|
ed9c61908d | ||
|
|
999e3e5b71 | ||
|
|
81ae8db1a9 | ||
|
|
f2b5b6f783 | ||
|
|
e8eab46f48 | ||
|
|
bb913d32bc | ||
|
|
2ea59dcdae | ||
|
|
bbdf514098 | ||
|
|
a0950dc4f3 | ||
|
|
d65ae78119 | ||
|
|
e6afbc75ff | ||
|
|
47e420dec1 | ||
|
|
1d6566679b | ||
|
|
535865fdeb | ||
|
|
5065b2ac37 | ||
|
|
6349e9b816 | ||
|
|
eb70883127 | ||
|
|
1e83b3b35c | ||
|
|
ef18805008 | ||
|
|
5d51a528d7 | ||
|
|
ff0ceb2879 | ||
|
|
4d7c044eba | ||
|
|
3a92cb4eda | ||
|
|
6244679d5b | ||
|
|
59c1b6b675 | ||
|
|
92a0f093db | ||
|
|
39dccd4b5f | ||
|
|
674820f7c9 | ||
|
|
f138b126f3 | ||
|
|
efaac453ce | ||
|
|
879c124aec | ||
|
|
9fe13f1868 | ||
|
|
2bbfb28090 | ||
|
|
3c65642e92 | ||
|
|
cf5f52b7b9 | ||
|
|
04d0439c9d | ||
|
|
f1116c3008 | ||
|
|
bbf4ad6b98 | ||
|
|
3fe3eec465 | ||
|
|
a93b05fd6e | ||
|
|
da25d46df4 | ||
|
|
0d13d40cd7 | ||
|
|
4664fd60b5 | ||
|
|
65ba984c2f | ||
|
|
8355b67bae | ||
|
|
9b3fe2fd97 | ||
|
|
34614f039f | ||
|
|
acd42eaa1b | ||
|
|
aebc8241cc | ||
|
|
0eda1b761d | ||
|
|
a05f7cb686 | ||
|
|
745a71700c | ||
|
|
ac9c190b41 | ||
|
|
1a1a230a1e | ||
|
|
b2e02b7762 | ||
|
|
9cbfeccbed | ||
|
|
4725300c41 | ||
|
|
f2aedf780d | ||
|
|
f03047a2f9 | ||
|
|
a7ba23d97e | ||
|
|
2521e980ea | ||
|
|
1c118fa04a | ||
|
|
b6fb5e02d4 | ||
|
|
5313704d84 | ||
|
|
b147f14fef | ||
|
|
66fe1528df | ||
|
|
a598cda624 | ||
|
|
69f218cc16 | ||
|
|
e1c12ea699 | ||
|
|
9801e91720 | ||
|
|
9e628fa6be | ||
|
|
1b52bb27f7 | ||
|
|
1d12bebce4 | ||
|
|
b5861dae39 | ||
|
|
5f9c21d00f |
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -113,6 +113,10 @@ jobs:
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- name: Install dependencies for test collection environment
|
||||
run: |
|
||||
npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- name: Build libraries
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[English](../../contributing.md)
|
||||
[Inglés](../../contributing.md)
|
||||
|
||||
## ¡Juntos, hagamos a Bruno mejor!
|
||||
|
||||
@@ -6,58 +6,111 @@ Estamos encantados de que quieras ayudar a mejorar Bruno. A continuación encont
|
||||
|
||||
### Tecnologías utilizadas
|
||||
|
||||
Bruno está construido con NextJs y React. También usamos electron para distribuir una versión de escritorio (que soporta colecciones locales).
|
||||
Bruno está construido con React y Electron
|
||||
|
||||
Librerías que utilizamos:
|
||||
|
||||
- CSS - Tailwind
|
||||
- Editores de código - Codemirror
|
||||
- CSS - Tailwind CSS
|
||||
- Editores de código - CodeMirror
|
||||
- Manejo del estado - Redux
|
||||
- Íconos - Tabler Icons
|
||||
- Formularios - formik
|
||||
- Validación de esquemas - Yup
|
||||
- Cliente de peticiones - axios
|
||||
- Monitor del sistema de archivos - chokidar
|
||||
- i18n (internacionalización) - i18next
|
||||
|
||||
### Dependencias
|
||||
|
||||
Necesitarás [Node v20.x o la última versión LTS](https://nodejs.org/es) y npm 8.x. Ten en cuenta que utilizamos espacios de trabajo de npm en el proyecto.
|
||||
> [!IMPORTANT]
|
||||
> Necesitarás [Node v22.x o la última versión LTS](https://nodejs.org/es/). Ten en cuenta que Bruno usa los espacios de trabajo de npm
|
||||
|
||||
## Desarrollo
|
||||
|
||||
Bruno está siendo desarrollado como una aplicación de escritorio. Para ejecutarlo, primero debes ejecutar la aplicación de nextjs en una terminal y luego ejecutar la aplicación de electron en otra terminal.
|
||||
Bruno es una aplicación de escritorio. A continuación se detallan las instrucciones paso a paso para ejecutar Bruno.
|
||||
|
||||
### Dependencias
|
||||
> Nota: Utilizamos React para el frontend y rsbuild para el servidor de desarrollo.
|
||||
|
||||
- NodeJS v18
|
||||
### Instalar dependencias
|
||||
|
||||
```bash
|
||||
# Use la versión 22.x o LTS (Soporte a Largo Plazo) de Node.js
|
||||
nvm use 22.11.0
|
||||
|
||||
# instalar las dependencias
|
||||
npm i --legacy-peer-deps
|
||||
```
|
||||
|
||||
> ¿Por qué `--legacy-peer-deps`?: Fuerza la instalación ignorando conflictos en dependencias “peer”, evitando errores de árbol de dependencias.
|
||||
|
||||
### Desarrollo local
|
||||
|
||||
#### Construir paquetes
|
||||
|
||||
##### Opción 1
|
||||
|
||||
```bash
|
||||
# Utiliza la versión 18 de nodejs
|
||||
nvm use
|
||||
|
||||
# Instala las dependencias
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# Construye la documentación de graphql
|
||||
# construir paquetes
|
||||
npm run build:graphql-docs
|
||||
|
||||
# Construye bruno-query
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# Ejecuta la aplicación de nextjs (terminal 1)
|
||||
# empaquetar bibliotecas JavaScript del entorno de pruebas aislado
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
|
||||
##### Opción 2
|
||||
|
||||
```bash
|
||||
# instalar dependencias y configurar el entorno
|
||||
npm run setup
|
||||
```
|
||||
|
||||
#### Ejecutar la aplicación
|
||||
|
||||
```bash
|
||||
# ejecutar aplicación react (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# Ejecuta la aplicación de electron (terminal 2)
|
||||
# ejecutar aplicación electron (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
##### Opción 1
|
||||
|
||||
```bash
|
||||
# ejecutar aplicación react (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# ejecutar aplicación electron (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
##### Opción 2
|
||||
|
||||
```bash
|
||||
# ejecutar aplicación electron y react de forma concurrente
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Personalizar la ruta `userData` de Electron
|
||||
|
||||
Si la variable de entorno `ELECTRON_USER_DATA_PATH` está presente y se encuentra en modo de desarrollo, entonces la ruta `userData` se modifica en consecuencia.
|
||||
ejemplo:
|
||||
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
|
||||
Esto creará una carpeta llamada `bruno-test` en tu escritorio y la usará como la ruta userData.
|
||||
|
||||
### Solución de problemas
|
||||
|
||||
Es posible que encuentres un error de `Unsupported platform` cuando ejecutes `npm install`. Para solucionarlo, debes eliminar la carpeta `node_modules` y el archivo `package-lock.json`, luego, ejecuta `npm install`. Lo anterior debería instalar todos los paquetes necesarios para ejecutar la aplicación.
|
||||
Es posible que te encuentres con un error `Unsupported platform` cuando ejecutes `npm install`. Para solucionarlo, tendrás que eliminar las carpetas `node_modules` y el archivo `package-lock.json`, y luego volver a ejecutar `npm install`. Esto debería instalar todos los paquetes necesarios para que la aplicación funcione.
|
||||
|
||||
```shell
|
||||
```sh
|
||||
# Elimina la carpeta node_modules en los subdirectorios
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
@@ -69,10 +122,42 @@ find . -type f -name "package-lock.json" -delete
|
||||
|
||||
### Pruebas
|
||||
|
||||
```bash
|
||||
# ejecutar pruebas de esquema bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
#### Pruebas individuales
|
||||
|
||||
```bash
|
||||
# ejecutar pruebas de bruno-app
|
||||
npm run test --workspace=packages/bruno-app
|
||||
|
||||
# ejecutar pruebas de bruno-electron
|
||||
npm run test --workspace=packages/bruno-electron
|
||||
|
||||
# ejecutar pruebas de bruno-cli
|
||||
npm run test --workspace=packages/bruno-cli
|
||||
|
||||
# ejecutar pruebas de bruno-common
|
||||
npm run test --workspace=packages/bruno-common
|
||||
|
||||
# ejecutar pruebas de bruno-converters
|
||||
npm run test --workspace=packages/bruno-converters
|
||||
|
||||
# ejecutar pruebas de bruno-schema
|
||||
npm run test --workspace=packages/bruno-schema
|
||||
|
||||
# ejecutar pruebas de bruno-query
|
||||
npm run test --workspace=packages/bruno-query
|
||||
|
||||
# ejecutar pruebas de bruno-js
|
||||
npm run test --workspace=packages/bruno-js
|
||||
|
||||
# ejecutar pruebas de bruno-lang
|
||||
npm run test --workspace=packages/bruno-lang
|
||||
|
||||
# ejecutar pruebas de bruno-toml
|
||||
npm run test --workspace=packages/bruno-toml
|
||||
```
|
||||
#### Pruebas en conjunto
|
||||
|
||||
```bash
|
||||
# ejecutar pruebas en todos los espacios de trabajo
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -74,12 +74,11 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# على نظام Linux عبر Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt
|
||||
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
|
||||
```
|
||||
|
||||
### التشغيل عبر منصات متعددة 🖥️
|
||||
|
||||
@@ -59,12 +59,11 @@ snap install bruno
|
||||
|
||||
# Apt এর মাধ্যমে লিনাক্সে
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### একাধিক প্ল্যাটফর্মে চালান 🖥️
|
||||
|
||||
@@ -63,12 +63,11 @@ snap install bruno
|
||||
|
||||
# 在 Linux 上用 Apt 安装
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### 在 Mac 上通过 Homebrew 安装 🖥️
|
||||
|
||||
@@ -78,12 +78,11 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# Auf Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### Einsatz auf verschiedensten Plattformen 🖥️
|
||||
|
||||
@@ -75,12 +75,11 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# En Linux con Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### Ejecútalo en múltiples plataformas 🖥️
|
||||
|
||||
@@ -63,12 +63,11 @@ snap install bruno
|
||||
|
||||
# Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### Fonctionne sur de multiples plateformes 🖥️
|
||||
|
||||
151
docs/readme/readme_hi.md
Normal file
151
docs/readme/readme_hi.md
Normal file
@@ -0,0 +1,151 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### ब्रूनो - API इंटरफेस (API) का अन्वेषण और परीक्षण करने के लिए एक ओपन-सोर्स विकास वातावरण।
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md)
|
||||
| [Українська](./readme_ua.md)
|
||||
| [Русский](./readme_ru.md)
|
||||
| [Türkçe](./readme_tr.md)
|
||||
| [Deutsch](./readme_de.md)
|
||||
| [Français](./readme_fr.md)
|
||||
| [Português (BR)](./readme_pt_br.md)
|
||||
| [한국어](./readme_kr.md)
|
||||
| [বাংলা](./readme_bn.md)
|
||||
| [Español](./readme_es.md)
|
||||
| [Italiano](./readme_it.md)
|
||||
| [Română](./readme_ro.md)
|
||||
| [Polski](./readme_pl.md)
|
||||
| [简体中文](./readme_cn.md)
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
| **हिन्दी**
|
||||
|
||||
ब्रूनो एक नया और अभिनव API क्लाइंट है, जिसका उद्देश्य Postman और अन्य समान उपकरणों द्वारा प्रस्तुत स्थिति को बदलना है।
|
||||
|
||||
ब्रूनो आपकी कलेक्शनों को सीधे आपकी फाइल सिस्टम के एक फ़ोल्डर में संग्रहीत करता है। हम API अनुरोधों के बारे में जानकारी सहेजने के लिए एक सामान्य टेक्स्ट मार्कअप भाषा, Bru, का उपयोग करते हैं।
|
||||
|
||||
आप अपनी API कलेक्शनों पर सहयोग करने के लिए Git या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग कर सकते हैं।
|
||||
|
||||
ब्रूनो केवल ऑफ़लाइन उपयोग के लिए है। ब्रूनो में कभी भी क्लाउड-सिंक जोड़ने की कोई योजना नहीं है। हम आपके डेटा की गोपनीयता को महत्व देते हैं और मानते हैं कि इसे आपके डिवाइस पर ही रहना चाहिए। हमारी दीर्घकालिक दृष्टि [यहाँ](https://github.com/usebruno/bruno/discussions/269) पढ़ें।
|
||||
|
||||
📢 हमारे हालिया India FOSS 3.0 सम्मेलन में हमारे वार्तालाप को [यहाँ](https://www.youtube.com/watch?v=7bSMFpbcPiY) देखें।
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### गोल्डन संस्करण ✨
|
||||
|
||||
हमारी अधिकांश सुविधाएँ मुफ्त और ओपन-सोर्स हैं।
|
||||
हम [पारदर्शिता और स्थिरता के सिद्धांतों](https://github.com/usebruno/bruno/discussions/269) के बीच एक सामंजस्यपूर्ण संतुलन प्राप्त करने का प्रयास करते हैं।
|
||||
|
||||
[गोल्डन संस्करण](https://www.usebruno.com/pricing) के लिए खरीदारी जल्द ही $9 की कीमत पर उपलब्ध होगी! <br/>
|
||||
[यहाँ सदस्यता लें](https://usebruno.ck.page/4c65576bd4) ताकि आपको लॉन्च पर सूचनाएं मिलें।
|
||||
|
||||
### स्थापना
|
||||
|
||||
ब्रूनो Mac, Windows और Linux के लिए हमारे [वेबसाइट](https://www.usebruno.com/downloads) पर एक बाइनरी डाउनलोड के रूप में उपलब्ध है।
|
||||
|
||||
आप ब्रूनो को Homebrew, Chocolatey, Scoop, Snap, Flatpak और Apt जैसे पैकेज प्रबंधकों के माध्यम से भी स्थापित कर सकते हैं।
|
||||
|
||||
```sh
|
||||
# Mac पर Homebrew के माध्यम से
|
||||
brew install bruno
|
||||
|
||||
# Windows पर Chocolatey के माध्यम से
|
||||
choco install bruno
|
||||
|
||||
# Windows पर Scoop के माध्यम से
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# Linux पर Snap के माध्यम से
|
||||
snap install bruno
|
||||
|
||||
# Linux पर Flatpak के माध्यम से
|
||||
flatpak install com.usebruno.Bruno
|
||||
|
||||
# Linux पर Apt के माध्यम से
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
|
||||
कई प्लेटफार्मों पर चलाएं 🖥️
|
||||
<br /><br />
|
||||
|
||||
Git के माध्यम से सहयोग करें 👩💻🧑💻
|
||||
या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग करें
|
||||
|
||||
<br /><br />
|
||||
|
||||
महत्वपूर्ण लिंक 📌
|
||||
हमारी दीर्घकालिक दृष्टि
|
||||
|
||||
रोडमैप
|
||||
|
||||
प्रलेखन
|
||||
|
||||
Stack Overflow
|
||||
|
||||
वेबसाइट
|
||||
|
||||
मूल्य निर्धारण
|
||||
|
||||
डाउनलोड
|
||||
|
||||
GitHub प्रायोजक
|
||||
|
||||
प्रस्तुतियाँ 🎥
|
||||
प्रशंसापत्र
|
||||
|
||||
ज्ञान केंद्र
|
||||
|
||||
Scriptmania
|
||||
|
||||
समर्थन ❤️
|
||||
यदि आप ब्रूनो को पसंद करते हैं और हमारे ओपन-सोर्स कार्य का समर्थन करना चाहते हैं, तो कृपया GitHub प्रायोजक के माध्यम से हमें प्रायोजित करने पर विचार करें।
|
||||
|
||||
प्रशंसापत्र साझा करें 📣
|
||||
यदि ब्रूनो ने आपके और आपकी टीमों के लिए काम में मदद की है, तो कृपया हमारे GitHub चर्चा में अपने प्रशंसापत्र साझा करना न भूलें
|
||||
|
||||
नए पैकेज प्रबंधकों में प्रकाशित करना
|
||||
अधिक जानकारी के लिए कृपया यहाँ देखें।
|
||||
|
||||
हमसे संपर्क करें 🌐
|
||||
𝕏 (ट्विटर) <br />
|
||||
वेबसाइट <br />
|
||||
डिस्कॉर्ड <br />
|
||||
लिंक्डइन
|
||||
|
||||
ट्रेडमार्क
|
||||
नाम
|
||||
|
||||
ब्रूनो एक ट्रेडमार्क है जो अनूप एम डी के स्वामित्व में है।
|
||||
|
||||
लोगो
|
||||
|
||||
लोगो OpenMoji से लिया गया है। लाइसेंस: CC BY-SA 4.0
|
||||
|
||||
योगदान 👩💻🧑💻
|
||||
हमें खुशी है कि आप ब्रूनो को बेहतर बनाने में रुचि रखते हैं। कृपया योगदान गाइड देखें।
|
||||
|
||||
यदि आप सीधे कोड के माध्यम से योगदान नहीं कर सकते, तो भी कृपया बग्स की रिपोर्ट करने और उन सुविधाओं का अनुरोध करने में संकोच न करें जिन्हें आपकी स्थिति को हल करने के लिए लागू किया जाना चाहिए।
|
||||
|
||||
लेखक
|
||||
<div align="center"> <a href="https://github.com/usebruno/bruno/graphs/contributors"> <img src="https://contrib.rocks/image?repo=usebruno/bruno" /> </a> </div>
|
||||
|
||||
लाइसेंस 📄
|
||||
MIT
|
||||
|
||||
@@ -59,12 +59,11 @@ snap install bruno
|
||||
|
||||
# Su Linux tramite Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### Funziona su diverse piattaforme 🖥️
|
||||
|
||||
@@ -78,12 +78,11 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# LinuxでAptを使ってインストール
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### マルチプラットフォームでの実行に対応 🖥️
|
||||
|
||||
@@ -59,12 +59,11 @@ snap install bruno
|
||||
|
||||
# On Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### 여러 플랫폼에서 실행하세요. 🖥️
|
||||
|
||||
@@ -69,12 +69,11 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# On Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
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
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### Uruchom na wielu platformach 🖥️
|
||||
|
||||
@@ -76,12 +76,11 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# No Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### Execute em várias plataformas 🖥️
|
||||
|
||||
@@ -59,12 +59,11 @@ snap install bruno
|
||||
|
||||
# Pe Linux cu Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### Utilizați pe mai multe platforme 🖥️
|
||||
|
||||
@@ -63,12 +63,11 @@ snap install bruno
|
||||
|
||||
# Apt aracılığıyla Linux'ta
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### Birden fazla platformda çalıştırın 🖥️
|
||||
|
||||
@@ -63,12 +63,11 @@ snap install bruno
|
||||
|
||||
# 在 Linux 上使用 Apt 安裝
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
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
|
||||
```
|
||||
|
||||
### 跨多個平台運行 🖥️
|
||||
|
||||
@@ -8,7 +8,7 @@ test('Create new collection and add a simple HTTP request', async ({ page, creat
|
||||
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await page.getByText('test-collection').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('r1');
|
||||
|
||||
@@ -21,14 +21,14 @@ test.describe.parallel('Run Testbench Requests', () => {
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
|
||||
});
|
||||
|
||||
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
@@ -44,6 +44,6 @@ test.describe.parallel('Run Testbench Requests', () => {
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,19 @@ module.exports = defineConfig([
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
|
||||
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.{js}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
|
||||
263
package-lock.json
generated
263
package-lock.json
generated
@@ -8209,78 +8209,6 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "9.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
|
||||
"integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.1.3",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
|
||||
@@ -8309,25 +8237,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/react": {
|
||||
"version": "14.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz",
|
||||
"integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@types/react-dom": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tippyjs/react": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
|
||||
@@ -8700,16 +8609,6 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-redux": {
|
||||
"version": "7.1.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
||||
@@ -28130,12 +28029,12 @@
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"semver": "^7.7.1",
|
||||
"shell-quote": "^1.8.3",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"system": "^2.0.1",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"yargs-parser": "^21.1.1",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -28148,9 +28047,9 @@
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
@@ -29478,6 +29377,64 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/@testing-library/dom": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.3.0",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/@testing-library/react": {
|
||||
"version": "16.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
|
||||
"integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@types/react": "^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/aria-query": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/babel-plugin-polyfill-corejs3": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
|
||||
@@ -29546,6 +29503,23 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"packages/bruno-app/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
@@ -29647,6 +29621,41 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/pretty-format/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-app/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
@@ -29658,6 +29667,18 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
@@ -31461,7 +31482,6 @@
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
@@ -31971,23 +31991,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/@faker-js/faker": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.1.tgz",
|
||||
"integrity": "sha512-0fzMEDxkExR2cn731kpDaCCnBGBUOIXEi2S1N5l8Hltp6aPf4soTMJ+g4k8r2sI5oB+rpwIW8Uy/6jkwGpnWPg==",
|
||||
"deprecated": "Please update to a newer version",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/@smithy/abort-controller": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz",
|
||||
@@ -32748,7 +32751,8 @@
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.18"
|
||||
"@types/qs": "^6.9.18",
|
||||
"axios": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
@@ -32761,6 +32765,17 @@
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-requests/node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-schema": {
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.7.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": [["styled-components", { "ssr": true }]]
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
module.exports = {
|
||||
rootDir: '.',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/",
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^assets/(.*)$': '<rootDir>/src/assets/$1',
|
||||
'^components/(.*)$': '<rootDir>/src/components/$1',
|
||||
@@ -8,18 +14,17 @@ module.exports = {
|
||||
'^api/(.*)$': '<rootDir>/src/api/$1',
|
||||
'^pageComponents/(.*)$': '<rootDir>/src/pageComponents/$1',
|
||||
'^providers/(.*)$': '<rootDir>/src/providers/$1',
|
||||
'^utils/(.*)$': '<rootDir>/src/utils/$1'
|
||||
'^utils/(.*)$': '<rootDir>/src/utils/$1',
|
||||
'^test-utils/(.*)$': '<rootDir>/src/test-utils/$1'
|
||||
},
|
||||
clearMocks: true,
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest'
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(nanoid|xml-formatter)/)'
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom'],
|
||||
setupFiles: [
|
||||
'<rootDir>/jest.setup.js',
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/*.spec.[jt]s?(x)'
|
||||
]
|
||||
};
|
||||
};
|
||||
11
packages/bruno-app/jest.setup.js
Normal file
11
packages/bruno-app/jest.setup.js
Normal file
@@ -0,0 +1,11 @@
|
||||
jest.mock('nanoid', () => {
|
||||
return {
|
||||
nanoid: () => {}
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('strip-json-comments', () => {
|
||||
return {
|
||||
stripJsonComments: (str) => str
|
||||
};
|
||||
});
|
||||
@@ -6,6 +6,7 @@
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"assets/*": ["src/assets/*"],
|
||||
"ui/*": ["src/ui/*"],
|
||||
"components/*": ["src/components/*"],
|
||||
"hooks/*": ["src/hooks/*"],
|
||||
"themes/*": ["src/themes/*"],
|
||||
|
||||
@@ -73,12 +73,12 @@
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"semver": "^7.7.1",
|
||||
"shell-quote": "^1.8.3",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"system": "^2.0.1",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"yargs-parser": "^21.1.1",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -91,9 +91,9 @@
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
@@ -111,4 +111,4 @@
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ export default defineConfig({
|
||||
],
|
||||
source: {
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,
|
||||
exclude: [
|
||||
'**/test-utils/**',
|
||||
'**/*.test.*',
|
||||
'**/*.spec.*'
|
||||
]
|
||||
},
|
||||
html: {
|
||||
title: 'Bruno'
|
||||
|
||||
40
packages/bruno-app/src/components/BulkEditor/index.js
Normal file
40
packages/bruno-app/src/components/BulkEditor/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { parseBulkKeyValue, serializeBulkKeyValue } from 'utils/common/bulkKeyValueUtils';
|
||||
|
||||
const BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
const parsedParams = useMemo(() => serializeBulkKeyValue(params), [params]);
|
||||
|
||||
const handleEdit = (value) => {
|
||||
const parsed = parseBulkKeyValue(value);
|
||||
onChange(parsed);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-[200px]">
|
||||
<CodeEditor
|
||||
mode="text/plain"
|
||||
theme={displayedTheme}
|
||||
font={preferences.codeFont || 'default'}
|
||||
value={parsedParams}
|
||||
onEdit={handleEdit}
|
||||
onSave={onSave}
|
||||
onRun={onRun}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex btn-action justify-between items-center mt-3">
|
||||
<button className="text-link select-none ml-auto" onClick={onToggle}>
|
||||
Key/Value Edit
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkEditor;
|
||||
@@ -8,120 +8,19 @@
|
||||
import React from 'react';
|
||||
import { isEqual, escapeRegExp } from 'lodash';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
window.JSHINT = JSHINT;
|
||||
|
||||
const TAB_SIZE = 2;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
window.JSHINT = JSHINT;
|
||||
//This should be done dynamically if possible
|
||||
const hintWords = [
|
||||
'res',
|
||||
'res.status',
|
||||
'res.statusText',
|
||||
'res.headers',
|
||||
'res.body',
|
||||
'res.responseTime',
|
||||
'res.getStatus()',
|
||||
'res.getStatusText()',
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.setBody(data)',
|
||||
'res.getResponseTime()',
|
||||
'req',
|
||||
'req.url',
|
||||
'req.method',
|
||||
'req.headers',
|
||||
'req.body',
|
||||
'req.timeout',
|
||||
'req.getUrl()',
|
||||
'req.setUrl(url)',
|
||||
'req.getMethod()',
|
||||
'req.getAuthMode()',
|
||||
'req.setMethod(method)',
|
||||
'req.getHeader(name)',
|
||||
'req.getHeaders()',
|
||||
'req.setHeader(name, value)',
|
||||
'req.setHeaders(data)',
|
||||
'req.getBody()',
|
||||
'req.setBody(data)',
|
||||
'req.setMaxRedirects(maxRedirects)',
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'req.getExecutionMode()',
|
||||
'req.getName()',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName()',
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.hasEnvVar(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.getFolderVar(key)',
|
||||
'bru.getCollectionVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.deleteEnvVar(key)',
|
||||
'bru.hasVar(key)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
'bru.deleteVar(key)',
|
||||
'bru.deleteAllVars()',
|
||||
'bru.setNextRequest(requestName)',
|
||||
'req.disableParsingResponseJson()',
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.runRequest(requestPathName)',
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
let startBru = cursor.ch;
|
||||
let endBru = startBru;
|
||||
while (endBru < currentLine.length && /[\w.]/.test(currentLine.charAt(endBru))) ++endBru;
|
||||
while (startBru && /[\w.]/.test(currentLine.charAt(startBru - 1))) --startBru;
|
||||
let curWordBru = startBru != endBru && currentLine.slice(startBru, endBru);
|
||||
|
||||
let start = cursor.ch;
|
||||
let end = start;
|
||||
while (end < currentLine.length && /[\w]/.test(currentLine.charAt(end))) ++end;
|
||||
while (start && /[\w]/.test(currentLine.charAt(start - 1))) --start;
|
||||
const jsHinter = CodeMirror.hint.javascript;
|
||||
let result = jsHinter(editor) || { list: [] };
|
||||
result.to = CodeMirror.Pos(cursor.line, end);
|
||||
result.from = CodeMirror.Pos(cursor.line, start);
|
||||
if (curWordBru) {
|
||||
hintWords.forEach((h) => {
|
||||
if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) {
|
||||
result.list.push(curWordBru.includes('.') ? h.split('.')?.at(-1) : h);
|
||||
}
|
||||
});
|
||||
result.list?.sort();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
||||
cm.showHint({ hint, ...options });
|
||||
};
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -141,12 +40,17 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const variables = getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
const editor = (this.editor = CodeMirror(this._node, {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: TAB_SIZE,
|
||||
mode: this.props.mode || 'application/ld+json',
|
||||
brunoVarInfo: {
|
||||
variables
|
||||
},
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
@@ -278,30 +182,24 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return found;
|
||||
});
|
||||
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
if (this.props.mode == 'javascript') {
|
||||
editor.on('keyup', function (cm, event) {
|
||||
const cursor = editor.getCursor();
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
let start = cursor.ch;
|
||||
let end = start;
|
||||
while (end < currentLine.length && /[^{}();\s\[\]\,]/.test(currentLine.charAt(end))) ++end;
|
||||
while (start && /[^{}();\s\[\]\,]/.test(currentLine.charAt(start - 1))) --start;
|
||||
let curWord = start != end && currentLine.slice(start, end);
|
||||
// Qualify if autocomplete will be shown
|
||||
if (
|
||||
/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/.test(event.key) &&
|
||||
curWord.length > 0 &&
|
||||
!/\/\/|\/\*|.*{{|`[^$]*{|`[^{]*$/.test(currentLine.slice(0, end)) &&
|
||||
/(?<!\d)[a-zA-Z\._]$/.test(curWord)
|
||||
) {
|
||||
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.brunoJS, { completeSingle: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Setup AutoComplete Helper for all modes
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: this.props.showHintsFor
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +240,9 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
this._unbindSearchHandler();
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
51
packages/bruno-app/src/components/CodeEditor/index.spec.js
Normal file
51
packages/bruno-app/src/components/CodeEditor/index.spec.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { render, act } from '@testing-library/react';
|
||||
import CodeEditor from './index';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
jest.mock('codemirror', () => {
|
||||
const codemirror = require('test-utils/mocks/codemirror');
|
||||
return codemirror;
|
||||
});
|
||||
|
||||
const MOCK_THEME = {
|
||||
codemirror: {
|
||||
bg: "#1e1e1e",
|
||||
border: "#333",
|
||||
},
|
||||
textLink: "#007acc",
|
||||
};
|
||||
|
||||
const setupEditorState = (editor, { value, cursorPosition }) => {
|
||||
editor._currentValue = value;
|
||||
editor.getCursor.mockReturnValue({ line: 0, ch: cursorPosition });
|
||||
editor.getRange.mockImplementation((from, to) => {
|
||||
if (from.line === 0 && from.ch === 0 && to.line === 0 && to.ch === cursorPosition) {
|
||||
return value;
|
||||
}
|
||||
return editor._currentValue.slice(from.ch, to.ch);
|
||||
});
|
||||
|
||||
editor.state = {
|
||||
completionActive: null,
|
||||
}
|
||||
};
|
||||
|
||||
const setupEditorWithRef = () => {
|
||||
const ref = React.createRef();
|
||||
const { rerender } = render(
|
||||
<ThemeProvider theme={MOCK_THEME}>
|
||||
<CodeEditor ref={ref} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
return { ref, rerender };
|
||||
};
|
||||
|
||||
describe('CodeEditor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it("add CodeEditor related tests here", () => {});
|
||||
});
|
||||
@@ -53,6 +53,7 @@ const Script = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
@@ -66,6 +67,7 @@ const Script = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const Tests = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions'
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection, folder }) => {
|
||||
@@ -117,6 +118,7 @@ const Headers = ({ collection, folder }) => {
|
||||
}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
autocomplete={MimeTypes}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -55,6 +55,7 @@ const Script = ({ collection, folder }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
@@ -68,6 +69,7 @@ const Script = ({ collection, folder }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ const Tests = ({ collection, folder }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -2,14 +2,10 @@ import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
const CodeMirror = require('codemirror');
|
||||
|
||||
class MultiLineEditor extends Component {
|
||||
constructor(props) {
|
||||
@@ -78,14 +74,21 @@ class MultiLineEditor extends Component {
|
||||
'Shift-Tab': false
|
||||
}
|
||||
});
|
||||
if (this.props.autocomplete) {
|
||||
this.editor.on('keyup', (cm, event) => {
|
||||
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
|
||||
/*Enter - do not open autocomplete list just after item has been selected in it*/
|
||||
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup AutoComplete Helper
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: ['variables'],
|
||||
anywordAutocompleteHints: this.props.autocomplete
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
this.editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
@@ -125,6 +128,9 @@ class MultiLineEditor extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
this.editor.getWrapperElement().remove();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import get from 'lodash/get';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const Font = ({ close }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -31,7 +32,10 @@ const Font = ({ close }) => {
|
||||
}
|
||||
})
|
||||
).then(() => {
|
||||
toast.success('Preferences saved successfully')
|
||||
close();
|
||||
}).catch(() => {
|
||||
toast.error('Failed to save preferences')
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -80,9 +80,9 @@ const General = ({ close }) => {
|
||||
storeCookies: newPreferences.storeCookies,
|
||||
sendCookies: newPreferences.sendCookies
|
||||
}
|
||||
})
|
||||
)
|
||||
}))
|
||||
.then(() => {
|
||||
toast.success('Preferences saved successfully')
|
||||
close();
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update preferences'));
|
||||
|
||||
@@ -84,7 +84,10 @@ const ProxySettings = ({ close }) => {
|
||||
proxy: validatedProxy
|
||||
})
|
||||
).then(() => {
|
||||
toast.success('Preferences saved successfully')
|
||||
close();
|
||||
}).catch(() => {
|
||||
toast.error('Failed to save preferences')
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -28,11 +28,11 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -45,12 +45,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -63,12 +63,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -81,12 +81,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -99,12 +99,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -117,12 +117,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -26,8 +26,8 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: basicAuth.password
|
||||
username: username || '',
|
||||
password: basicAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -40,8 +40,8 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: basicAuth.username,
|
||||
password: password
|
||||
username: basicAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -25,8 +25,8 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: digestAuth.password
|
||||
username: username || '',
|
||||
password: digestAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -39,8 +39,8 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: digestAuth.username,
|
||||
password: password
|
||||
username: digestAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -26,9 +26,9 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: ntlmAuth.password,
|
||||
domain: ntlmAuth.domain
|
||||
username: username || '',
|
||||
password: ntlmAuth.password || '',
|
||||
domain: ntlmAuth.domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -41,9 +41,9 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: password,
|
||||
domain: ntlmAuth.domain
|
||||
username: ntlmAuth.username || '',
|
||||
password: password || '',
|
||||
domain: ntlmAuth.domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -56,9 +56,9 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: ntlmAuth.password,
|
||||
domain: domain
|
||||
username: ntlmAuth.username || '',
|
||||
password: ntlmAuth.password || '',
|
||||
domain: domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -26,8 +26,8 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username,
|
||||
password: wsseAuth.password
|
||||
username: username || '',
|
||||
password: wsseAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -40,8 +40,8 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: wsseAuth.username,
|
||||
password
|
||||
username: wsseAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -18,8 +18,9 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
|
||||
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
@@ -66,7 +67,6 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
schema={schema}
|
||||
width={leftPaneWidth}
|
||||
onSave={onSave}
|
||||
value={query}
|
||||
onRun={onRun}
|
||||
@@ -154,7 +154,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5 flex-1 relative">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className="flex w-full mt-5 flex-1 relative">
|
||||
<HeightBoundContainer>{getTabPanel(focusedTab.requestPaneTab)}</HeightBoundContainer>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -68,6 +68,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
enableVariableHighlighting={true}
|
||||
showHintsFor={['variables']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { find, get } from 'lodash';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
@@ -33,7 +34,7 @@ const ErrorIndicator = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
const HttpRequestPane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
@@ -180,7 +181,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
'mt-5': !isMultipleContentTab
|
||||
})}
|
||||
>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
<HeightBoundContainer>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
</HeightBoundContainer>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -18,12 +18,7 @@ import { IconWand } from '@tabler/icons';
|
||||
|
||||
import onHasCompletion from './onHasCompletion';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
const CodeMirror = require('codemirror');
|
||||
|
||||
const md = new MD();
|
||||
const AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/;
|
||||
|
||||
@@ -31,7 +31,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-param {
|
||||
.btn-action {
|
||||
font-size: 0.8125rem;
|
||||
&:hover span {
|
||||
text-decoration: underline;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
moveQueryParam,
|
||||
updatePathParam
|
||||
updatePathParam,
|
||||
setQueryParams
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -18,6 +19,7 @@ import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collection
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Table from 'components/Table/index';
|
||||
import ReorderTable from 'components/ReorderTable';
|
||||
import BulkEditor from '../../BulkEditor';
|
||||
|
||||
const QueryParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -25,6 +27,8 @@ const QueryParams = ({ item, collection }) => {
|
||||
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
|
||||
const queryParams = params.filter((param) => param.type === 'query');
|
||||
const pathParams = params.filter((param) => param.type === 'path');
|
||||
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const handleAddQueryParam = () => {
|
||||
dispatch(
|
||||
@@ -113,8 +117,31 @@ const QueryParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkParamsChange = (newParams) => {
|
||||
const paramsWithType = newParams.map((item) => ({ ...item, type: 'query' }));
|
||||
dispatch(setQueryParams({ collectionUid: collection.uid, itemUid: item.uid, params: paramsWithType }));
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-3">
|
||||
<BulkEditor
|
||||
params={queryParams}
|
||||
onChange={handleBulkParamsChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={onSave}
|
||||
onRun={handleRun}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col absolute">
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Query</div>
|
||||
<Table
|
||||
@@ -171,9 +198,14 @@ const QueryParams = ({ item, collection }) => {
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
|
||||
+ <span>Add Param</span>
|
||||
</button>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-action text-link pr-2 py-3 select-none" onClick={handleAddQueryParam}>
|
||||
+ <span>Add Param</span>
|
||||
</button>
|
||||
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-2 title text-xs flex items-stretch">
|
||||
<span>Path</span>
|
||||
<InfoTip infotipId="path-param-InfoTip">
|
||||
|
||||
@@ -21,7 +21,7 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
||||
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -149,7 +149,7 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
</Dropdown>
|
||||
</div>
|
||||
{(bodyMode === 'json' || bodyMode === 'xml') && (
|
||||
<button className="ml-1" onClick={onPrettify}>
|
||||
<button className="ml-2" onClick={onPrettify}>
|
||||
Prettify
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -59,6 +59,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
onSave={onSave}
|
||||
mode={codeMirrorMode[bodyMode]}
|
||||
enableVariableHighlighting={true}
|
||||
showHintsFor={['variables']}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -22,8 +22,11 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-header {
|
||||
.btn-action {
|
||||
font-size: 0.8125rem;
|
||||
&:hover span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addRequestHeader, updateRequestHeader, deleteRequestHeader, moveRequestHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { addRequestHeader, updateRequestHeader, deleteRequestHeader, moveRequestHeader, setRequestHeaders } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -12,12 +12,16 @@ import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import Table from 'components/Table/index';
|
||||
import ReorderTable from 'components/ReorderTable/index';
|
||||
import BulkEditor from '../../BulkEditor';
|
||||
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
@@ -75,6 +79,28 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkHeadersChange = (newHeaders) => {
|
||||
dispatch(setRequestHeaders({ collectionUid: collection.uid, itemUid: item.uid, headers: newHeaders }));
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-3">
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={onSave}
|
||||
onRun={handleRun}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<Table
|
||||
@@ -153,9 +179,14 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
: null}
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-action text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@ const Script = ({ item, collection }) => {
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
@@ -66,6 +67,7 @@ const Script = ({ item, collection }) => {
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -37,6 +37,7 @@ const Tests = ({ item, collection }) => {
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,13 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
&.dragging {
|
||||
cursor: col-resize;
|
||||
|
||||
&.vertical-layout {
|
||||
cursor: row-resize;
|
||||
}
|
||||
}
|
||||
|
||||
div.drag-request {
|
||||
div.dragbar-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -15,18 +19,47 @@ const StyledWrapper = styled.div`
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
|
||||
div.drag-request-border {
|
||||
div.dragbar-handle {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
|
||||
}
|
||||
|
||||
&:hover div.drag-request-border {
|
||||
&:hover div.dragbar-handle {
|
||||
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical-layout {
|
||||
.request-pane {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.response-pane {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
div.dragbar-wrapper {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
cursor: row-resize;
|
||||
padding: 0 1rem;
|
||||
|
||||
div.dragbar-handle {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-left: none;
|
||||
border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
|
||||
}
|
||||
|
||||
&:hover div.dragbar-handle {
|
||||
border-left: none;
|
||||
border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.graphql-docs-explorer-container {
|
||||
background: white;
|
||||
outline: none;
|
||||
|
||||
@@ -29,7 +29,8 @@ import FolderNotFound from './FolderNotFound';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 350;
|
||||
const DEFAULT_PADDING = 5;
|
||||
const MIN_TOP_PANE_HEIGHT = 150;
|
||||
const MIN_BOTTOM_PANE_HEIGHT = 150;
|
||||
|
||||
const RequestTabPanel = () => {
|
||||
if (typeof window == 'undefined') {
|
||||
@@ -41,6 +42,8 @@ const RequestTabPanel = () => {
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
const _collections = useSelector((state) => state.collections.collections);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
|
||||
|
||||
// merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object
|
||||
let collections = produce(_collections, (draft) => {
|
||||
@@ -64,13 +67,15 @@ const RequestTabPanel = () => {
|
||||
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const [leftPaneWidth, setLeftPaneWidth] = useState(
|
||||
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2
|
||||
); // 2.2 so that request pane is relatively smaller
|
||||
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
|
||||
); // 2.2 is intentional to make both panes appear to be of equal width
|
||||
const [topPaneHeight, setTopPaneHeight] = useState(focusedTab?.requestPaneHeight || MIN_TOP_PANE_HEIGHT);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
|
||||
// Not a recommended pattern here to have the child component
|
||||
// make a callback to set state, but treating this as an exception
|
||||
const docExplorerRef = useRef(null);
|
||||
const mainSectionRef = useRef(null);
|
||||
const [schema, setSchema] = useState(null);
|
||||
const [showGqlDocs, setShowGqlDocs] = useState(false);
|
||||
const onSchemaLoad = (schema) => setSchema(schema);
|
||||
@@ -85,43 +90,72 @@ const RequestTabPanel = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const leftPaneWidth = (screenWidth - asideWidth) / 2.2;
|
||||
setLeftPaneWidth(leftPaneWidth);
|
||||
}, [screenWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
|
||||
}, [screenWidth, asideWidth, leftPaneWidth]);
|
||||
// Initialize vertical heights when switching to vertical layout
|
||||
if (mainSectionRef.current) {
|
||||
const mainRect = mainSectionRef.current.getBoundingClientRect();
|
||||
if (isVerticalLayout) {
|
||||
const initialHeight = mainRect.height / 2;
|
||||
setTopPaneHeight(initialHeight);
|
||||
// In vertical mode, set leftPaneWidth to full container width
|
||||
setLeftPaneWidth(mainRect.width);
|
||||
} else {
|
||||
// In horizontal mode, set to roughly half width
|
||||
setLeftPaneWidth((screenWidth - asideWidth) / 2.2);
|
||||
}
|
||||
}
|
||||
}, [isVerticalLayout, screenWidth, asideWidth]);
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragging) {
|
||||
if (dragging && mainSectionRef.current) {
|
||||
e.preventDefault();
|
||||
let leftPaneXPosition = e.clientX + 2;
|
||||
if (
|
||||
leftPaneXPosition < asideWidth + DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH ||
|
||||
leftPaneXPosition > screenWidth - MIN_RIGHT_PANE_WIDTH
|
||||
) {
|
||||
return;
|
||||
const mainRect = mainSectionRef.current.getBoundingClientRect();
|
||||
|
||||
if (isVerticalLayout) {
|
||||
const newHeight = e.clientY - mainRect.top - dragOffset.current.y;
|
||||
if (newHeight < MIN_TOP_PANE_HEIGHT || newHeight > mainRect.height - MIN_BOTTOM_PANE_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTopPaneHeight(newHeight);
|
||||
} else {
|
||||
const newWidth = e.clientX - mainRect.left - dragOffset.current.x;
|
||||
if (newWidth < MIN_LEFT_PANE_WIDTH || newWidth > mainRect.width - MIN_RIGHT_PANE_WIDTH) {
|
||||
return;
|
||||
}
|
||||
setLeftPaneWidth(newWidth);
|
||||
}
|
||||
setLeftPaneWidth(leftPaneXPosition - asideWidth);
|
||||
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
if (dragging) {
|
||||
if (dragging && mainSectionRef.current) {
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
dispatch(
|
||||
updateRequestPaneTabWidth({
|
||||
uid: activeTabUid,
|
||||
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
|
||||
})
|
||||
);
|
||||
if (!isVerticalLayout) {
|
||||
const mainRect = mainSectionRef.current.getBoundingClientRect();
|
||||
dispatch(
|
||||
updateRequestPaneTabWidth({
|
||||
uid: activeTabUid,
|
||||
requestPaneWidth: e.clientX - mainRect.left
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragbarMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
setDragging(true);
|
||||
|
||||
if (isVerticalLayout) {
|
||||
const dragBar = e.currentTarget;
|
||||
const dragBarRect = dragBar.getBoundingClientRect();
|
||||
dragOffset.current.y = e.clientY - dragBarRect.top;
|
||||
} else {
|
||||
const dragBar = e.currentTarget;
|
||||
const dragBarRect = dragBar.getBoundingClientRect();
|
||||
dragOffset.current.x = e.clientX - dragBarRect.left;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -132,7 +166,7 @@ const RequestTabPanel = () => {
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, [dragging, asideWidth]);
|
||||
}, [dragging]);
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <Welcome />;
|
||||
@@ -197,15 +231,19 @@ const RequestTabPanel = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''}`}>
|
||||
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${isVerticalLayout ? 'vertical-layout' : ''}`}>
|
||||
<div className="pt-4 pb-3 px-4">
|
||||
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
</div>
|
||||
<section className="main flex flex-grow pb-4 relative">
|
||||
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative`}>
|
||||
<section className="request-pane">
|
||||
<div
|
||||
className="px-4 h-full"
|
||||
style={{
|
||||
style={isVerticalLayout ? {
|
||||
height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,
|
||||
minHeight: `${MIN_TOP_PANE_HEIGHT}px`,
|
||||
width: '100%'
|
||||
} : {
|
||||
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
|
||||
}}
|
||||
>
|
||||
@@ -213,7 +251,6 @@ const RequestTabPanel = () => {
|
||||
<GraphQLRequestPane
|
||||
item={item}
|
||||
collection={collection}
|
||||
leftPaneWidth={leftPaneWidth}
|
||||
onSchemaLoad={onSchemaLoad}
|
||||
toggleDocs={toggleDocs}
|
||||
handleGqlClickReference={handleGqlClickReference}
|
||||
@@ -221,17 +258,17 @@ const RequestTabPanel = () => {
|
||||
) : null}
|
||||
|
||||
{item.type === 'http-request' ? (
|
||||
<HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} />
|
||||
<HttpRequestPane item={item} collection={collection} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="drag-request" onMouseDown={handleDragbarMouseDown}>
|
||||
<div className="drag-request-border" />
|
||||
<div className="dragbar-wrapper" onMouseDown={handleDragbarMouseDown}>
|
||||
<div className="dragbar-handle" />
|
||||
</div>
|
||||
|
||||
<section className="response-pane flex-grow">
|
||||
<ResponsePane item={item} collection={collection} rightPaneWidth={rightPaneWidth} response={item.response} />
|
||||
<section className="response-pane flex-grow overflow-x-auto">
|
||||
<ResponsePane item={item} collection={collection} response={item.response} />
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.warning-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-top: 10%;
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
margin-bottom: 1rem;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.warning-description {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
.size-highlight {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.current-size {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background: ${(props) => props.theme.colors.text.danger}15;
|
||||
}
|
||||
|
||||
.supported-size {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
background: ${(props) => props.theme.colors.text.yellow}15;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
background: ${(props) => props.theme.button.secondary.bg};
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { IconDownload, IconCopy, IconEye, IconAlertTriangle } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { formatSize } from 'utils/common/index';
|
||||
|
||||
const LargeResponseWarning = ({ item, responseSize, onRevealResponse }) => {
|
||||
const { ipcRenderer } = window;
|
||||
const response = item.response || {};
|
||||
|
||||
const saveResponseToFile = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item.requestSent.url)
|
||||
.then(() => {
|
||||
toast.success('Response saved to file');
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(get(err, 'error.message') || 'Something went wrong!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const copyResponse = () => {
|
||||
try {
|
||||
const textToCopy = typeof response.data === 'string'
|
||||
? response.data
|
||||
: JSON.stringify(response.data, null, 2);
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
toast.success('Response copied to clipboard');
|
||||
}).catch(() => {
|
||||
toast.error('Failed to copy response');
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy response');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="warning-container">
|
||||
<div className="warning-icon">
|
||||
<IconAlertTriangle size={45} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="warning-content">
|
||||
<div className="warning-title">
|
||||
Large Response Warning
|
||||
</div>
|
||||
<div className="warning-description">
|
||||
Handling responses over <span className="size-highlight supported-size">{formatSize(10 * 1024 * 1024)}</span> could degrade performance.
|
||||
<br />
|
||||
Size of current response: <span className="size-highlight current-size">{formatSize(responseSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="warning-actions">
|
||||
<button
|
||||
className="btn-reveal"
|
||||
onClick={onRevealResponse}
|
||||
title="Show response content"
|
||||
>
|
||||
<IconEye size={18} strokeWidth={1.5} />
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
className="btn-save"
|
||||
onClick={saveResponseToFile}
|
||||
disabled={!response.dataBuffer}
|
||||
title="Save response to file"
|
||||
>
|
||||
<IconDownload size={18} strokeWidth={1.5} />
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="btn-copy"
|
||||
onClick={copyResponse}
|
||||
disabled={!response.data}
|
||||
title="Copy response to clipboard"
|
||||
>
|
||||
<IconCopy size={18} strokeWidth={1.5} />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LargeResponseWarning;
|
||||
@@ -22,6 +22,15 @@ const StyledWrapper = styled.div`
|
||||
animation: rotateCounterClockwise 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
// spinner and request time content looks better centered vertically in vertical layout
|
||||
// while in horizontal layout, it looks better when the content is aligned to the top
|
||||
&.vertical-layout {
|
||||
div.overlay {
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconRefresh } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { cancelRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StopWatch from '../../StopWatch';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponseLoadingOverlay = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
|
||||
|
||||
const handleCancelRequest = () => {
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<StyledWrapper className={`w-full ${isVerticalLayout ? 'vertical-layout' : ''}`}>
|
||||
<div className="overlay">
|
||||
<div style={{ marginBottom: 15, fontSize: 26 }}>
|
||||
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 20%;
|
||||
width: 100%;
|
||||
|
||||
.send-icon {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
|
||||
}
|
||||
|
||||
&.vertical-layout {
|
||||
padding: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconSend } from '@tabler/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
|
||||
@@ -8,9 +9,11 @@ const Placeholder = () => {
|
||||
const sendRequestShortcut = isMac ? 'Cmd + Enter' : 'Ctrl + Enter';
|
||||
const newRequestShortcut = isMac ? 'Cmd + B' : 'Ctrl + B';
|
||||
const editEnvironmentShortcut = isMac ? 'Cmd + E' : 'Ctrl + E';
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledWrapper className={`${isVerticalLayout ? 'vertical-layout' : ''}`}>
|
||||
<div className="send-icon flex justify-center" style={{ fontSize: 200 }}>
|
||||
<IconSend size={150} strokeWidth={1} />
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { getEncoding, uuid } from 'utils/common/index';
|
||||
import LargeResponseWarning from '../LargeResponseWarning';
|
||||
|
||||
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
|
||||
if (data === undefined || !dataBuffer || !mode) {
|
||||
@@ -73,10 +74,11 @@ const formatErrorMessage = (error) => {
|
||||
return error;
|
||||
};
|
||||
|
||||
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
|
||||
const QueryResult = ({ item, collection, data, dataBuffer, disableRunEventListener, headers, error }) => {
|
||||
const contentType = getContentType(headers);
|
||||
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
|
||||
const [filter, setFilter] = useState(null);
|
||||
const [showLargeResponse, setShowLargeResponse] = useState(false);
|
||||
const responseEncoding = getEncoding(headers);
|
||||
const formattedData = useMemo(
|
||||
() => formatResponse(data, dataBuffer, responseEncoding, mode, filter),
|
||||
@@ -84,6 +86,25 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
);
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
const responseSize = useMemo(() => {
|
||||
const response = item.response || {};
|
||||
if (typeof response.size === 'number') {
|
||||
return response.size;
|
||||
}
|
||||
|
||||
if (!dataBuffer) return 0;
|
||||
|
||||
try {
|
||||
// dataBuffer is base64 encoded, so we need to calculate the actual size
|
||||
const buffer = Buffer.from(dataBuffer, 'base64');
|
||||
return buffer.length;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}, [dataBuffer, item.response]);
|
||||
|
||||
const isLargeResponse = responseSize > 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
const debouncedResultFilterOnChange = debounce((e) => {
|
||||
setFilter(e.target.value);
|
||||
}, 250);
|
||||
@@ -143,7 +164,6 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="w-full h-full relative flex"
|
||||
style={{ maxWidth: width }}
|
||||
queryFilterEnabled={queryFilterEnabled}
|
||||
>
|
||||
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
||||
@@ -151,7 +171,9 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
</div>
|
||||
{error ? (
|
||||
<div>
|
||||
{hasScriptError ? null : <div className="text-red-500">{formatErrorMessage(error)}</div>}
|
||||
{hasScriptError ? null : (
|
||||
<div className="text-red-500" style={{ whiteSpace: 'pre-line' }}>{formatErrorMessage(error)}</div>
|
||||
)}
|
||||
|
||||
{error && typeof error === 'string' && error.toLowerCase().includes('self signed certificate') ? (
|
||||
<div className="mt-6 muted text-xs">
|
||||
@@ -160,6 +182,12 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : isLargeResponse && !showLargeResponse ? (
|
||||
<LargeResponseWarning
|
||||
item={item}
|
||||
responseSize={responseSize}
|
||||
onRevealResponse={() => setShowLargeResponse(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 relative">
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const IconDockToBottom = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
>
|
||||
<path stroke="none" fill="none" d="M0 0h24v24H0z" />
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M4 15l16 0" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M 5.5135136,19.111502 C 5.2542477,18.995986 5.0221761,18.756859 4.8928709,18.47199 4.7922381,18.250288 4.7788524,18.078909 4.7777079,16.997543 l -0.0013,-1.223586 H 12 19.223587 v 1.22675 c 0,1.194609 -0.0039,1.234605 -0.149369,1.526503 -0.09333,0.187285 -0.240773,0.363095 -0.392978,0.46858 l -0.243606,0.168829 -6.373606,0.0129 c -5.2129418,0.0105 -6.4058225,-0.0015 -6.5505114,-0.06597 z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const IconDockToRight = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
>
|
||||
<path fill="none" stroke="none" d="M 0,24 V 0 h 24 v 24 z" />
|
||||
<path d="m 4,20 m 2,0 A 2,2 0 0 1 4,18 V 6 A 2,2 0 0 1 6,4 h 12 a 2,2 0 0 1 2,2 v 12 a 2,2 0 0 1 -2,2 z" />
|
||||
<path d="M 15,20 V 4" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
d="m 19.111502,18.486486 c -0.115516,0.259266 -0.354643,0.491338 -0.639512,0.620643 -0.221702,0.100633 -0.393081,0.114019 -1.474447,0.115163 l -1.223586,0.0013 V 12 4.7764125 h 1.22675 c 1.194609,0 1.234605,0.0039 1.526503,0.14937 0.187285,0.09333 0.363095,0.2407725 0.46858,0.3929775 l 0.168829,0.243606 0.0129,6.373606 c 0.0105,5.212942 -0.0015,6.405822 -0.06597,6.550511 z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const ResponseLayoutToggle = () => {
|
||||
const dispatch = useDispatch();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal';
|
||||
|
||||
const toggleOrientation = () => {
|
||||
const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
layout: {
|
||||
...preferences.layout,
|
||||
responsePaneOrientation: newOrientation
|
||||
}
|
||||
};
|
||||
dispatch(savePreferences(updatedPreferences));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button
|
||||
onClick={toggleOrientation}
|
||||
title={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
|
||||
>
|
||||
{orientation === 'horizontal' ? (
|
||||
<IconDockToBottom />
|
||||
) : (
|
||||
<IconDockToRight />
|
||||
)}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponseLayoutToggle;
|
||||
@@ -0,0 +1,173 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent} from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { ThemeProvider } from 'providers/Theme';
|
||||
import { configureStore, createSlice } from '@reduxjs/toolkit';
|
||||
import ResponseLayoutToggle from './index';
|
||||
|
||||
const mockSavePreferences = jest.fn((payload) => ({ type: 'app/savePreferences', payload }));
|
||||
|
||||
// Mock the savePreferences action
|
||||
jest.mock('providers/ReduxStore/slices/app', () => ({
|
||||
savePreferences: (payload) => mockSavePreferences(payload)
|
||||
}));
|
||||
|
||||
// Mock localStorage
|
||||
const mockLocalStorage = {
|
||||
getItem: jest.fn(() => 'dark'),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn()
|
||||
};
|
||||
|
||||
// Mock matchMedia
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn()
|
||||
})),
|
||||
});
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockLocalStorage
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockSavePreferences.mockClear();
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
app: {
|
||||
preferences: {
|
||||
layout: {
|
||||
responsePaneOrientation: 'horizontal'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createTestStore = (initialState) => {
|
||||
const appSlice = createSlice({
|
||||
name: 'app',
|
||||
initialState: initialState.app,
|
||||
reducers: {
|
||||
savePreferences: (state, action) => {
|
||||
state.preferences = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return configureStore({
|
||||
reducer: { app: appSlice.reducer }
|
||||
});
|
||||
};
|
||||
|
||||
const renderWithProviders = (component, customState = initialState) => {
|
||||
const store = createTestStore(customState);
|
||||
return {
|
||||
store,
|
||||
...render(
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
{component}
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
describe('ResponseLayoutToggle', () => {
|
||||
describe('Initial Render', () => {
|
||||
it('should render with horizontal orientation by default', () => {
|
||||
renderWithProviders(<ResponseLayoutToggle />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAttribute('title', 'Switch to vertical layout');
|
||||
});
|
||||
|
||||
it('should render with vertical orientation when specified', () => {
|
||||
const customState = {
|
||||
app: {
|
||||
preferences: {
|
||||
layout: {
|
||||
responsePaneOrientation: 'vertical'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
renderWithProviders(<ResponseLayoutToggle />, customState);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAttribute('title', 'Switch to horizontal layout');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interaction', () => {
|
||||
it('should switch to vertical layout when clicked in horizontal mode', () => {
|
||||
const { store } = renderWithProviders(<ResponseLayoutToggle />);
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
// Initial state check
|
||||
expect(button).toHaveAttribute('title', 'Switch to vertical layout');
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
// Check if action was called
|
||||
expect(mockSavePreferences).toHaveBeenCalledWith({
|
||||
layout: {
|
||||
responsePaneOrientation: 'vertical'
|
||||
}
|
||||
});
|
||||
|
||||
// Manually update store to simulate state change
|
||||
store.dispatch(mockSavePreferences({
|
||||
layout: {
|
||||
responsePaneOrientation: 'vertical'
|
||||
}
|
||||
}));
|
||||
|
||||
// Check if button title was updated
|
||||
expect(button).toHaveAttribute('title', 'Switch to horizontal layout');
|
||||
});
|
||||
|
||||
it('should switch to horizontal layout when clicked in vertical mode', () => {
|
||||
const customState = {
|
||||
app: {
|
||||
preferences: {
|
||||
layout: {
|
||||
responsePaneOrientation: 'vertical'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const { store } = renderWithProviders(<ResponseLayoutToggle />, customState);
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
// Initial state check
|
||||
expect(button).toHaveAttribute('title', 'Switch to horizontal layout');
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
// Check if action was called
|
||||
expect(mockSavePreferences).toHaveBeenCalledWith({
|
||||
layout: {
|
||||
responsePaneOrientation: 'horizontal'
|
||||
}
|
||||
});
|
||||
|
||||
// Manually update store to simulate state change
|
||||
store.dispatch(mockSavePreferences({
|
||||
layout: {
|
||||
responsePaneOrientation: 'horizontal'
|
||||
}
|
||||
}));
|
||||
|
||||
// Check if button title was updated
|
||||
expect(button).toHaveAttribute('title', 'Switch to vertical layout');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,22 +6,48 @@ import StyledWrapper from './StyledWrapper';
|
||||
const ScriptError = ({ item, onClose }) => {
|
||||
const preRequestError = item?.preRequestScriptErrorMessage;
|
||||
const postResponseError = item?.postResponseScriptErrorMessage;
|
||||
const testScriptError = item?.testScriptErrorMessage;
|
||||
|
||||
if (!preRequestError && !postResponseError) return null;
|
||||
if (!preRequestError && !postResponseError && !testScriptError) return null;
|
||||
|
||||
const errorMessage = preRequestError || postResponseError;
|
||||
const errorTitle = preRequestError ? 'Pre-Request Script Error' : 'Post-Response Script Error';
|
||||
const errors = [];
|
||||
|
||||
if (preRequestError) {
|
||||
errors.push({
|
||||
title: 'Pre-Request Script Error',
|
||||
message: preRequestError
|
||||
});
|
||||
}
|
||||
|
||||
if (postResponseError) {
|
||||
errors.push({
|
||||
title: 'Post-Response Script Error',
|
||||
message: postResponseError
|
||||
});
|
||||
}
|
||||
|
||||
if (testScriptError) {
|
||||
errors.push({
|
||||
title: 'Test Script Error',
|
||||
message: testScriptError
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4 mb-2">
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="error-title">
|
||||
{errorTitle}
|
||||
</div>
|
||||
<div className="error-message">
|
||||
{errorMessage}
|
||||
</div>
|
||||
{errors.map((error, index) => (
|
||||
<div key={index}>
|
||||
{index > 0 && <div className="border-t border-gray-300 my-3 dark:border-gray-600"></div>}
|
||||
<div className="error-title">
|
||||
{error.title}
|
||||
</div>
|
||||
<div className="error-message">
|
||||
{error.message}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="close-button flex-shrink-0 cursor-pointer"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import QueryResult from "components/ResponsePane/QueryResult/index";
|
||||
import { useState } from "react";
|
||||
|
||||
const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, width }) => {
|
||||
const BodyBlock = ({ collection, data, dataBuffer, headers, error, item }) => {
|
||||
const [isBodyCollapsed, toggleBody] = useState(true);
|
||||
return (
|
||||
<div className="collapsible-section">
|
||||
@@ -17,7 +17,6 @@ const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, width }
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
headers={headers}
|
||||
|
||||
@@ -16,7 +16,7 @@ const safeStringifyJSONIfNotString = (obj) => {
|
||||
};
|
||||
|
||||
|
||||
const Request = ({ collection, request, item, width }) => {
|
||||
const Request = ({ collection, request, item }) => {
|
||||
let { url, headers, data, dataBuffer, error } = request || {};
|
||||
if (!dataBuffer) {
|
||||
dataBuffer = Buffer.from(safeStringifyJSONIfNotString(data))?.toString('base64');
|
||||
@@ -33,7 +33,7 @@ const Request = ({ collection, request, item, width }) => {
|
||||
<Headers headers={headers} type={'request'} />
|
||||
|
||||
{/* Body */}
|
||||
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
|
||||
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const safeStringifyJSONIfNotString = (obj) => {
|
||||
}
|
||||
};
|
||||
|
||||
const Response = ({ collection, response, item, width }) => {
|
||||
const Response = ({ collection, response, item }) => {
|
||||
let { status, statusCode, statusText, dataBuffer, headers, data, error } = response || {};
|
||||
if (!dataBuffer) {
|
||||
dataBuffer = Buffer.from(safeStringifyJSONIfNotString(data))?.toString('base64');
|
||||
@@ -35,7 +35,7 @@ const Response = ({ collection, response, item, width }) => {
|
||||
<Headers headers={headers} type={'response'} />
|
||||
|
||||
{/* Body */}
|
||||
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
|
||||
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Method from "./Common/Method/index";
|
||||
import Status from "./Common/Status/index";
|
||||
import { RelativeTime } from "./Common/Time/index";
|
||||
|
||||
const TimelineItem = ({ timestamp, request, response, item, collection, width, isOauth2 }) => {
|
||||
const TimelineItem = ({ timestamp, request, response, item, collection, isOauth2 }) => {
|
||||
const [isCollapsed, _toggleCollapse] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('request');
|
||||
const toggleCollapse = () => _toggleCollapse(prev => !prev);
|
||||
@@ -57,15 +57,15 @@ const TimelineItem = ({ timestamp, request, response, item, collection, width, i
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="tab-content">
|
||||
<div className="tab-content break-all">
|
||||
{/* Request Tab */}
|
||||
{activeTab === 'request' && (
|
||||
<Request request={request} item={item} collection={collection} width={width} />
|
||||
<Request request={request} item={item} collection={collection} />
|
||||
)}
|
||||
|
||||
{/* Response Tab */}
|
||||
{activeTab === 'response' && (
|
||||
<Response response={response} item={item} collection={collection} width={width} />
|
||||
<Response response={response} item={item} collection={collection} />
|
||||
)}
|
||||
|
||||
{/* Network Logs Tab */}
|
||||
|
||||
@@ -41,7 +41,7 @@ const getEffectiveAuthSource = (collection, item) => {
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const Timeline = ({ collection, item, width }) => {
|
||||
const Timeline = ({ collection, item }) => {
|
||||
// Get the effective auth source if auth mode is inherit
|
||||
const authSource = getEffectiveAuthSource(collection, item);
|
||||
|
||||
@@ -62,7 +62,6 @@ const Timeline = ({ collection, item, width }) => {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="pb-4 w-full flex flex-grow flex-col"
|
||||
style={{ maxWidth: width - 60, overflowWrap: 'break-word' }}
|
||||
>
|
||||
{combinedTimeline.map((event, index) => {
|
||||
if (event.type === 'request') {
|
||||
@@ -76,7 +75,6 @@ const Timeline = ({ collection, item, width }) => {
|
||||
response={response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -101,7 +99,6 @@ const Timeline = ({ collection, item, width }) => {
|
||||
response={data?.response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width - 50}
|
||||
isOauth2={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -20,8 +20,10 @@ import ResponseSave from 'src/components/ResponsePane/ResponseSave';
|
||||
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
import ResponseLayoutToggle from './ResponseLayoutToggle';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const ResponsePane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
@@ -33,10 +35,10 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage) {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage) {
|
||||
setShowScriptErrorCard(true);
|
||||
}
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage]);
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage]);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -57,7 +59,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
data={response.data}
|
||||
dataBuffer={response.dataBuffer}
|
||||
headers={response.headers}
|
||||
@@ -70,7 +71,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline collection={collection} item={item} width={rightPaneWidth} />;
|
||||
return <Timeline collection={collection} item={item} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults
|
||||
@@ -105,9 +106,9 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
|
||||
if (!item.response && !requestTimeline?.length) {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<HeightBoundContainer>
|
||||
<Placeholder />
|
||||
</StyledWrapper>
|
||||
</HeightBoundContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,11 +129,11 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
|
||||
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
|
||||
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage;
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
|
||||
<div className="flex flex-wrap items-center px-4 tabs" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
</div>
|
||||
@@ -159,6 +160,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
/>
|
||||
)}
|
||||
<ResponseLayoutToggle />
|
||||
{focusedTab?.responsePaneTab === "timeline" ? (
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
) : (item?.response && !item?.response?.error) ? (
|
||||
@@ -174,7 +176,11 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-col flex-grow relative pl-3 pr-4 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
|
||||
className={`flex flex-col min-h-0 relative px-4 auto`}
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
|
||||
}}
|
||||
>
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{hasScriptError && showScriptErrorCard && (
|
||||
@@ -183,17 +189,18 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
/>
|
||||
)}
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
|
||||
<Timeline
|
||||
collection={collection}
|
||||
item={item}
|
||||
width={rightPaneWidth}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<>{getTabPanel(focusedTab.responsePaneTab)}</>
|
||||
)}
|
||||
<div className='flex-1 min-h-[200px] overflow-y-auto'>
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
|
||||
<Timeline
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<>{getTabPanel(focusedTab.responsePaneTab)}</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -3,16 +3,6 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
span.beta-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
|
||||
span.developer-mode-warning {
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
|
||||
@@ -61,7 +61,6 @@ const JsSandboxModeModal = ({ collection }) => {
|
||||
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
|
||||
Safe Mode
|
||||
</span>
|
||||
<span className='beta-tag'>BETA</span>
|
||||
</label>
|
||||
<p className='text-sm text-muted mt-1'>
|
||||
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
|
||||
@@ -85,9 +84,6 @@ const JsSandboxModeModal = ({ collection }) => {
|
||||
<p className='text-sm text-muted mt-1'>
|
||||
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
|
||||
</p>
|
||||
<small className='text-muted mt-6'>
|
||||
* SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
|
||||
</small>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
|
||||
@@ -3,16 +3,6 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
span.beta-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
|
||||
span.developer-mode-warning {
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
|
||||
@@ -47,7 +47,6 @@ const SecuritySettings = ({ collection }) => {
|
||||
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
|
||||
Safe Mode
|
||||
</span>
|
||||
<span className='beta-tag'>BETA</span>
|
||||
</label>
|
||||
<p className='text-sm text-muted mt-1'>
|
||||
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
|
||||
@@ -75,9 +74,6 @@ const SecuritySettings = ({ collection }) => {
|
||||
<button onClick={handleSave} className="submit btn btn-sm btn-secondary w-fit mt-6">
|
||||
Save
|
||||
</button>
|
||||
<small className='text-muted mt-6'>
|
||||
* SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
|
||||
</small>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,59 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.editor-content {
|
||||
height: 100%;
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 0;
|
||||
|
||||
.CodeMirror-gutters {
|
||||
background: ${props => props.theme.codemirror.gutter.bg};
|
||||
border-right: 1px solid ${props => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
font-size: 11px;
|
||||
padding: 0 3px 0 5px;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copy-to-clipboard {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
opacity: 0.5;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
opacity: 0.7;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: ${props => props.theme.text};
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,64 +1,52 @@
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import get from 'lodash/get';
|
||||
import { HTTPSnippet } from 'httpsnippet';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconCopy } from '@tabler/icons';
|
||||
import { findCollectionByItemUid, getGlobalEnvironmentVariables } from '../../../../../../../utils/collections/index';
|
||||
import { getAuthHeaders } from '../../../../../../../utils/codegenerator/auth';
|
||||
import { findCollectionByItemUid, getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { generateSnippet } from '../utils/snippet-generator';
|
||||
|
||||
const CodeView = ({ language, item }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
const { target, client, language: lang } = language;
|
||||
const requestHeaders = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
let _collection = findCollectionByItemUid(
|
||||
const generateCodePrefs = useSelector((state) => state.app.generateCode);
|
||||
|
||||
let collectionOriginal = findCollectionByItemUid(
|
||||
useSelector((state) => state.collections.collections),
|
||||
item.uid
|
||||
);
|
||||
|
||||
let collection = cloneDeep(_collection);
|
||||
const collection = useMemo(() => {
|
||||
const c = cloneDeep(collectionOriginal);
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
|
||||
globalEnvironments,
|
||||
activeGlobalEnvironmentUid
|
||||
});
|
||||
c.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
return c;
|
||||
}, [collectionOriginal, globalEnvironments, activeGlobalEnvironmentUid]);
|
||||
|
||||
// add selected global env variables to the collection object
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const collectionRootAuth = collection?.root?.request?.auth;
|
||||
const requestAuth = item.draft ? get(item, 'draft.request.auth') : get(item, 'request.auth');
|
||||
|
||||
const headers = [
|
||||
...getAuthHeaders(collectionRootAuth, requestAuth),
|
||||
...(collection?.root?.request?.headers || []),
|
||||
...(requestHeaders || [])
|
||||
];
|
||||
|
||||
let snippet = '';
|
||||
try {
|
||||
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers, type: item.type })).convert(
|
||||
target,
|
||||
client
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
snippet = 'Error generating code snippet';
|
||||
}
|
||||
const snippet = useMemo(() => {
|
||||
return generateSnippet({ language, item, collection, shouldInterpolate: generateCodePrefs.shouldInterpolate });
|
||||
}, [language, item, collection, generateCodePrefs.shouldInterpolate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledWrapper>
|
||||
<CopyToClipboard
|
||||
className="copy-to-clipboard"
|
||||
text={snippet}
|
||||
onCopy={() => toast.success('Copied to clipboard!')}
|
||||
>
|
||||
<StyledWrapper>
|
||||
<CopyToClipboard
|
||||
text={snippet}
|
||||
onCopy={() => toast.success('Copied to clipboard!')}
|
||||
>
|
||||
<button className="copy-to-clipboard">
|
||||
<IconCopy size={25} strokeWidth={1.5} />
|
||||
</CopyToClipboard>
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
<div className="editor-content">
|
||||
<CodeEditor
|
||||
readOnly
|
||||
collection={collection}
|
||||
@@ -67,10 +55,12 @@ const CodeView = ({ language, item }) => {
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
mode={lang}
|
||||
mode={language.language}
|
||||
enableVariableHighlighting={true}
|
||||
showHintsFor={['variables']}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
</>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: ${props => props.theme.requestTabPanel.card.bg};
|
||||
border-bottom: 1px solid ${props => props.theme.requestTabPanel.card.border};
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.native-select {
|
||||
background: ${props => props.theme.requestTabPanel.url.bg};
|
||||
border: 1px solid ${props => props.theme.input.border};
|
||||
border-radius: 3px;
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 12px;
|
||||
padding: 6px 28px 6px 10px;
|
||||
min-width: 140px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
appearance: none;
|
||||
|
||||
&:hover {
|
||||
border-color: ${props => props.theme.input.focusBorder};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${props => props.theme.input.focusBorder};
|
||||
box-shadow: 0 0 0 2px ${props => props.theme.input.focusBoxShadow};
|
||||
}
|
||||
|
||||
option {
|
||||
background: ${props => props.theme.bg};
|
||||
color: ${props => props.theme.text};
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.library-options {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lib-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
background: ${props => props.theme.requestTabPanel.url.bg};
|
||||
border: 1px solid ${props => props.theme.input.border};
|
||||
border-radius: 3px;
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: ${props => props.theme.dropdown.hoverBg};
|
||||
border-color: ${props => props.theme.input.focusBorder};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${props => props.theme.button.secondary.bg};
|
||||
border-color: ${props => props.theme.button.secondary.border};
|
||||
color: ${props => props.theme.button.secondary.color};
|
||||
}
|
||||
}
|
||||
|
||||
.right-controls {
|
||||
.interpolate-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: ${props => props.theme.text};
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { IconChevronDown } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useMemo } from 'react';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { updateGenerateCode } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CodeViewToolbar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const languages = getLanguages();
|
||||
const generateCodePrefs = useSelector((state) => state.app.generateCode);
|
||||
|
||||
// Group languages by their main language type
|
||||
const languageGroups = useMemo(() => {
|
||||
return languages.reduce((acc, lang) => {
|
||||
const mainLang = lang.name.split('-')[0];
|
||||
if (!acc[mainLang]) {
|
||||
acc[mainLang] = [];
|
||||
}
|
||||
acc[mainLang].push({
|
||||
...lang,
|
||||
libraryName: lang.name.split('-')[1] || 'default'
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
}, [languages]);
|
||||
|
||||
const mainLanguages = useMemo(() => Object.keys(languageGroups), [languageGroups]);
|
||||
|
||||
const availableLibraries = useMemo(() => {
|
||||
return languageGroups[generateCodePrefs.mainLanguage] || [];
|
||||
}, [generateCodePrefs.mainLanguage, languageGroups]);
|
||||
|
||||
// Event handlers
|
||||
const handleMainLanguageChange = (e) => {
|
||||
const newMainLang = e.target.value;
|
||||
const defaultLibrary = languageGroups[newMainLang][0].libraryName;
|
||||
|
||||
dispatch(updateGenerateCode({
|
||||
mainLanguage: newMainLang,
|
||||
library: defaultLibrary
|
||||
}));
|
||||
};
|
||||
|
||||
const handleLibraryChange = (libraryName) => {
|
||||
dispatch(updateGenerateCode({
|
||||
library: libraryName
|
||||
}));
|
||||
};
|
||||
|
||||
const handleInterpolateChange = (e) => {
|
||||
dispatch(updateGenerateCode({
|
||||
shouldInterpolate: e.target.checked
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="toolbar">
|
||||
<div className="left-controls">
|
||||
<div className="select-wrapper">
|
||||
<select
|
||||
className="native-select"
|
||||
value={generateCodePrefs.mainLanguage}
|
||||
onChange={handleMainLanguageChange}
|
||||
>
|
||||
{mainLanguages.map((lang) => (
|
||||
<option key={lang} value={lang}>
|
||||
{lang}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<IconChevronDown size={16} className="select-arrow" />
|
||||
</div>
|
||||
|
||||
{availableLibraries.length > 1 && (
|
||||
<div className="library-options">
|
||||
{availableLibraries.map((lib) => (
|
||||
<button
|
||||
key={lib.libraryName}
|
||||
className={`lib-btn ${generateCodePrefs.library === lib.libraryName ? 'active' : ''}`}
|
||||
onClick={() => handleLibraryChange(lib.libraryName)}
|
||||
>
|
||||
{lib.libraryName}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="right-controls">
|
||||
<label className="interpolate-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={generateCodePrefs.shouldInterpolate}
|
||||
onChange={handleInterpolateChange}
|
||||
/>
|
||||
<span>Interpolate Variables</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeViewToolbar;
|
||||
@@ -1,60 +1,44 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
margin-inline: -1rem;
|
||||
margin-block: -1.5rem;
|
||||
margin: -1.5rem -1rem;
|
||||
height: 50vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
|
||||
.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};
|
||||
max-height: 80vh;
|
||||
.code-generator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.generate-code-item {
|
||||
min-width: 150px;
|
||||
display: block;
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
background: ${props => props.theme.bg};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||
.error-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: ${props => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.flexible-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.flexible-container {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 601px) and (max-width: 1200px) {
|
||||
.flexible-container {
|
||||
width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1201px) {
|
||||
.flexible-container {
|
||||
width: 900px;
|
||||
p {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,72 +1,30 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import CodeView from './CodeView';
|
||||
import CodeViewToolbar from './CodeViewToolbar';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isValidUrl } from 'utils/url';
|
||||
import { get } from 'lodash';
|
||||
import { findEnvironmentInCollection, findItemInCollection, findParentItemInCollection } from 'utils/collections';
|
||||
import {
|
||||
findEnvironmentInCollection
|
||||
} from 'utils/collections';
|
||||
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _itemUid) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _itemUid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
// Function to resolve inherited auth
|
||||
const resolveInheritedAuth = (item, collection) => {
|
||||
const request = item.draft?.request || item.request;
|
||||
const authMode = request?.auth?.mode;
|
||||
|
||||
// If auth is not inherit or no auth defined, return the request as is
|
||||
if (!authMode || authMode !== 'inherit') {
|
||||
return {
|
||||
...request
|
||||
};
|
||||
}
|
||||
|
||||
// Get the tree path from collection to item
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
let source = 'collection';
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
source = 'folder';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
import { resolveInheritedAuth } from './utils/auth-utils';
|
||||
|
||||
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const languages = getLanguages();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
|
||||
const generateCodePrefs = useSelector((state) => state.app.generateCode);
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
|
||||
globalEnvironments,
|
||||
activeGlobalEnvironmentUid
|
||||
});
|
||||
const environment = findEnvironmentInCollection(collection, collection?.activeEnvironmentUid);
|
||||
|
||||
let envVars = {};
|
||||
if (environment) {
|
||||
const vars = get(environment, 'variables', []);
|
||||
@@ -79,7 +37,6 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const requestUrl =
|
||||
get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
// interpolate the url
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url: requestUrl,
|
||||
globalEnvironmentVariables,
|
||||
@@ -94,54 +51,27 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params')
|
||||
);
|
||||
|
||||
// Get the full language object based on current preferences
|
||||
const selectedLanguage = useMemo(() => {
|
||||
const fullName = generateCodePrefs.library === 'default'
|
||||
? generateCodePrefs.mainLanguage
|
||||
: `${generateCodePrefs.mainLanguage}-${generateCodePrefs.library}`;
|
||||
|
||||
return languages.find(lang => lang.name === fullName) || languages[0];
|
||||
}, [generateCodePrefs.mainLanguage, generateCodePrefs.library, languages]);
|
||||
|
||||
// Resolve auth inheritance
|
||||
const resolvedRequest = resolveInheritedAuth(item, collection);
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||
return (
|
||||
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
|
||||
<StyledWrapper>
|
||||
<div className="flex w-full flexible-container">
|
||||
<div>
|
||||
<div className="generate-code-sidebar">
|
||||
{languages &&
|
||||
languages.length &&
|
||||
languages.map((language) => (
|
||||
<div
|
||||
key={language.name}
|
||||
className={
|
||||
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setSelectedLanguage(language)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Tab' || (e.shiftKey && e.key === 'Tab')) {
|
||||
e.preventDefault();
|
||||
const currentIndex = languages.findIndex((lang) => lang.name === selectedLanguage.name);
|
||||
const nextIndex = e.shiftKey
|
||||
? (currentIndex - 1 + languages.length) % languages.length
|
||||
: (currentIndex + 1) % languages.length;
|
||||
setSelectedLanguage(languages[nextIndex]);
|
||||
<div className="code-generator">
|
||||
<CodeViewToolbar />
|
||||
|
||||
// Explicitly focus on the new active element
|
||||
const nextElement = document.querySelector(`[data-language="${languages[nextIndex].name}"]`);
|
||||
nextElement?.focus();
|
||||
}
|
||||
|
||||
}}
|
||||
data-language={language.name}
|
||||
aria-pressed={language.name === selectedLanguage.name}
|
||||
>
|
||||
<span className="capitalize">{language.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow p-4">
|
||||
<div className="editor-container">
|
||||
{isValidUrl(finalUrl) ? (
|
||||
<CodeView
|
||||
tabIndex={-1}
|
||||
language={selectedLanguage}
|
||||
item={{
|
||||
...item,
|
||||
@@ -152,11 +82,9 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col justify-center items-center w-full">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold">Invalid URL: {finalUrl}</h1>
|
||||
<p className="text-gray-500">Please check the URL and try again</p>
|
||||
</div>
|
||||
<div className="error-message">
|
||||
<h1>Invalid URL: {finalUrl}</h1>
|
||||
<p>Please check the URL and try again</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
findItemInCollection,
|
||||
findParentItemInCollection
|
||||
} from 'utils/collections';
|
||||
|
||||
export const getTreePathFromCollectionToItem = (collection, _itemUid) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _itemUid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
// Resolve inherited auth by traversing up the folder hierarchy
|
||||
export const resolveInheritedAuth = (item, collection) => {
|
||||
const mergedRequest = {
|
||||
...(item.request || {}),
|
||||
...(item.draft?.request || {})
|
||||
};
|
||||
|
||||
const authMode = mergedRequest.auth.mode;
|
||||
|
||||
// If auth is not inherit or no auth defined, return the merged request as is
|
||||
if (!authMode || authMode !== 'inherit') {
|
||||
return mergedRequest;
|
||||
}
|
||||
|
||||
// Get the tree path from collection to item
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...mergedRequest,
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { resolveInheritedAuth } from './auth-utils';
|
||||
|
||||
// Helper to build mock collection structure
|
||||
const buildCollection = () => {
|
||||
return {
|
||||
uid: 'c1',
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'bearer', bearer: { token: 'COLLECTION' } }
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'f1',
|
||||
type: 'folder',
|
||||
name: 'Folder',
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
uid: 'r1',
|
||||
type: 'request',
|
||||
name: 'Request',
|
||||
request: {
|
||||
auth: { mode: 'inherit' },
|
||||
url: 'http://example.com',
|
||||
method: 'GET'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
describe('auth-utils.resolveInheritedAuth', () => {
|
||||
it('should resolve to nearest folder auth when request mode is inherit', () => {
|
||||
const collection = buildCollection();
|
||||
const item = collection.items[0].items[0]; // r1
|
||||
|
||||
const resolved = resolveInheritedAuth(item, collection);
|
||||
expect(resolved.auth.mode).toBe('basic');
|
||||
expect(resolved.auth.basic.username).toBe('user');
|
||||
});
|
||||
|
||||
it('should resolve to collection auth if no folder auth', () => {
|
||||
const collection = buildCollection();
|
||||
collection.items[0].root.request.auth = { mode: 'inherit' };
|
||||
const item = collection.items[0].items[0];
|
||||
|
||||
const resolved = resolveInheritedAuth(item, collection);
|
||||
expect(resolved.auth.mode).toBe('bearer');
|
||||
expect(resolved.auth.bearer.token).toBe('COLLECTION');
|
||||
});
|
||||
|
||||
it('should return original request when mode is not inherit', () => {
|
||||
const collection = buildCollection();
|
||||
const item = collection.items[0].items[0];
|
||||
item.request.auth = { mode: 'basic', basic: { username: 'override', password: 'pwd' } };
|
||||
|
||||
const resolved = resolveInheritedAuth(item, collection);
|
||||
expect(resolved.auth.mode).toBe('basic');
|
||||
expect(resolved.auth.basic.username).toBe('override');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export const interpolateHeaders = (headers = [], variables = {}) => {
|
||||
return headers.map((header) => ({
|
||||
...header,
|
||||
name: interpolate(header.name, variables),
|
||||
value: interpolate(header.value, variables)
|
||||
}));
|
||||
};
|
||||
|
||||
export const interpolateBody = (body, variables = {}) => {
|
||||
if (!body) return null;
|
||||
|
||||
const interpolatedBody = cloneDeep(body);
|
||||
|
||||
switch (body.mode) {
|
||||
case 'json':
|
||||
let parsed = body.json;
|
||||
// If it's already a string, use it directly; if it's an object, stringify it first
|
||||
if (typeof parsed === 'object') {
|
||||
parsed = JSON.stringify(parsed);
|
||||
}
|
||||
parsed = interpolate(parsed, variables, { escapeJSONStrings: true });
|
||||
try {
|
||||
const jsonObj = JSON.parse(parsed);
|
||||
interpolatedBody.json = JSON.stringify(jsonObj, null, 2);
|
||||
} catch {
|
||||
interpolatedBody.json = parsed;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
interpolatedBody.text = interpolate(body.text, variables);
|
||||
break;
|
||||
|
||||
case 'xml':
|
||||
interpolatedBody.xml = interpolate(body.xml, variables);
|
||||
break;
|
||||
|
||||
case 'sparql':
|
||||
interpolatedBody.sparql = interpolate(body.sparql, variables);
|
||||
break;
|
||||
|
||||
case 'formUrlEncoded':
|
||||
interpolatedBody.formUrlEncoded = body.formUrlEncoded.map((param) => ({
|
||||
...param,
|
||||
value: param.enabled ? interpolate(param.value, variables) : param.value
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'multipartForm':
|
||||
interpolatedBody.multipartForm = body.multipartForm.map((param) => ({
|
||||
...param,
|
||||
value:
|
||||
param.type === 'text' && param.enabled
|
||||
? interpolate(param.value, variables)
|
||||
: param.value
|
||||
}));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return interpolatedBody;
|
||||
};
|
||||
|
||||
export const createVariablesObject = ({
|
||||
globalEnvironmentVariables = {},
|
||||
collectionVars = {},
|
||||
allVariables = {},
|
||||
collection = {},
|
||||
runtimeVariables = {},
|
||||
processEnvVars = {}
|
||||
}) => {
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
...allVariables,
|
||||
...collectionVars,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { interpolateHeaders, interpolateBody } from './interpolation';
|
||||
|
||||
describe('interpolation utils', () => {
|
||||
describe('interpolateHeaders', () => {
|
||||
it('should interpolate variables in header name and value while preserving other props', () => {
|
||||
const headers = [
|
||||
{ uid: '1', name: 'X-{{var}}', value: 'value-{{var}}', enabled: true }
|
||||
];
|
||||
const variables = { var: 'test' };
|
||||
|
||||
const result = interpolateHeaders(headers, variables);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
uid: '1',
|
||||
name: 'X-test',
|
||||
value: 'value-test',
|
||||
enabled: true
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolateBody', () => {
|
||||
it('should interpolate JSON body strings and keep formatting', () => {
|
||||
const body = {
|
||||
mode: 'json',
|
||||
json: '{"name": "{{username}}"}'
|
||||
};
|
||||
const variables = { username: 'bruno' };
|
||||
|
||||
const result = interpolateBody(body, variables);
|
||||
expect(result.json).toBe('{\n "name": "bruno"\n}');
|
||||
});
|
||||
|
||||
it('should interpolate text body', () => {
|
||||
const body = {
|
||||
mode: 'text',
|
||||
text: 'Hello {{name}}'
|
||||
};
|
||||
const result = interpolateBody(body, { name: 'World' });
|
||||
expect(result.text).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should return null when body is null', () => {
|
||||
expect(interpolateBody(null, { a: 1 })).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { interpolateHeaders, interpolateBody, createVariablesObject } from './interpolation';
|
||||
|
||||
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
|
||||
try {
|
||||
// Get HTTPSnippet dynamically so mocks can be applied in tests
|
||||
const { HTTPSnippet } = require('httpsnippet');
|
||||
|
||||
const allVariables = getAllVariables(collection, item);
|
||||
|
||||
// Create variables object for interpolation
|
||||
const variables = createVariablesObject({
|
||||
globalEnvironmentVariables: collection.globalEnvironmentVariables || {},
|
||||
collectionVars: collection.collectionVars || {},
|
||||
allVariables,
|
||||
collection,
|
||||
runtimeVariables: collection.runtimeVariables || {},
|
||||
processEnvVars: collection.processEnvVariables || {}
|
||||
});
|
||||
|
||||
const request = item.request;
|
||||
|
||||
// Prepare headers
|
||||
let headers = [...(request.headers || [])];
|
||||
|
||||
// Add auth headers if needed
|
||||
if (request.auth && request.auth.mode !== 'none') {
|
||||
const collectionAuth = collection?.root?.request?.auth || null;
|
||||
const authHeaders = getAuthHeaders(collectionAuth, request.auth);
|
||||
headers = [...headers, ...authHeaders];
|
||||
}
|
||||
|
||||
// Interpolate headers and body if needed
|
||||
if (shouldInterpolate) {
|
||||
headers = interpolateHeaders(headers, variables);
|
||||
if (request.body) {
|
||||
request.body = interpolateBody(request.body, variables);
|
||||
}
|
||||
}
|
||||
|
||||
// Build HAR request
|
||||
const harRequest = buildHarRequest({
|
||||
request,
|
||||
headers
|
||||
});
|
||||
|
||||
// Generate snippet using HTTPSnippet
|
||||
const snippet = new HTTPSnippet(harRequest);
|
||||
const result = snippet.convert(language.target, language.client);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error generating code snippet:', error);
|
||||
return 'Error generating code snippet';
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
generateSnippet
|
||||
};
|
||||
@@ -0,0 +1,421 @@
|
||||
jest.mock('httpsnippet', () => {
|
||||
return {
|
||||
HTTPSnippet: jest.fn().mockImplementation((harRequest) => ({
|
||||
convert: jest.fn(() => {
|
||||
const method = harRequest?.method || 'GET';
|
||||
const url = harRequest?.url || 'http://example.com';
|
||||
const hasBody = harRequest?.postData?.text;
|
||||
|
||||
if (method === 'POST' && hasBody) {
|
||||
return `curl -X POST ${url} -H "Content-Type: application/json" -d '${hasBody}'`;
|
||||
}
|
||||
return `curl -X ${method} ${url}`;
|
||||
})
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('utils/codegenerator/har', () => ({
|
||||
buildHarRequest: jest.fn((data) => {
|
||||
const request = data.request || {};
|
||||
const method = request.method || 'GET';
|
||||
const url = request.url || 'http://example.com';
|
||||
const body = request.body || {};
|
||||
|
||||
const harRequest = {
|
||||
method: method,
|
||||
url: url,
|
||||
headers: data.headers || [],
|
||||
httpVersion: 'HTTP/1.1'
|
||||
};
|
||||
|
||||
// Add body data for POST requests
|
||||
if (method === 'POST' && body.mode === 'json' && body.json) {
|
||||
harRequest.postData = {
|
||||
mimeType: 'application/json',
|
||||
text: body.json
|
||||
};
|
||||
}
|
||||
|
||||
return harRequest;
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock('utils/codegenerator/auth', () => ({
|
||||
getAuthHeaders: jest.fn(() => [])
|
||||
}));
|
||||
|
||||
jest.mock('utils/collections/index', () => ({
|
||||
getAllVariables: jest.fn(() => ({
|
||||
baseUrl: 'https://api.example.com',
|
||||
apiKey: 'secret-key-123',
|
||||
userId: '12345'
|
||||
}))
|
||||
}));
|
||||
|
||||
import { generateSnippet } from './snippet-generator';
|
||||
|
||||
describe('Snippet Generator - Simple Tests', () => {
|
||||
|
||||
// Simple test request - easy to understand
|
||||
const testRequest = {
|
||||
uid: 'test-request-123',
|
||||
name: 'test api call',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/{{endpoint}}',
|
||||
headers: [
|
||||
{ uid: 'h1', name: 'Authorization', value: 'Bearer {{apiToken}}', enabled: true },
|
||||
{ uid: 'h2', name: 'Content-Type', value: 'application/json', enabled: true },
|
||||
{ uid: 'h3', name: 'X-Custom', value: '{{customValue}}', enabled: true }
|
||||
],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{"message": "{{greeting}}", "count": {{number}}}'
|
||||
},
|
||||
auth: { mode: 'none' },
|
||||
assertions: [],
|
||||
tests: '',
|
||||
docs: '',
|
||||
params: [],
|
||||
vars: { req: [] }
|
||||
}
|
||||
};
|
||||
|
||||
const testCollection = {
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'none' },
|
||||
headers: []
|
||||
}
|
||||
},
|
||||
globalEnvironmentVariables: {
|
||||
endpoint: 'data',
|
||||
apiToken: 'token123',
|
||||
customValue: 'test-value',
|
||||
greeting: 'Hello World',
|
||||
number: 42
|
||||
},
|
||||
runtimeVariables: {},
|
||||
processEnvVariables: {}
|
||||
};
|
||||
|
||||
const curlLanguage = { target: 'shell', client: 'curl' };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation((harRequest) => ({
|
||||
convert: jest.fn(() => {
|
||||
const method = harRequest?.method || 'GET';
|
||||
const url = harRequest?.url || 'http://example.com';
|
||||
const hasBody = harRequest?.postData?.text;
|
||||
|
||||
if (method === 'POST' && hasBody) {
|
||||
return `curl -X POST ${url} -H "Content-Type: application/json" -d '${hasBody}'`;
|
||||
}
|
||||
return `curl -X ${method} ${url}`;
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
it('should generate curl for POST request with JSON body', () => {
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: testRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"message": "{{greeting}}", "count": {{number}}}\'');
|
||||
});
|
||||
|
||||
it('should interpolate variables when enabled', () => {
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: testRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const expectedBody = `{
|
||||
"message": "Hello World",
|
||||
"count": 42
|
||||
}`;
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`);
|
||||
});
|
||||
|
||||
it('should handle GET requests', () => {
|
||||
const getRequest = {
|
||||
...testRequest,
|
||||
request: {
|
||||
...testRequest.request,
|
||||
method: 'GET',
|
||||
body: { mode: 'none' }
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: getRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('curl -X GET https://api.example.com/{{endpoint}}');
|
||||
});
|
||||
|
||||
it('should handle requests with different headers', () => {
|
||||
const requestWithDifferentHeaders = {
|
||||
...testRequest,
|
||||
request: {
|
||||
...testRequest.request,
|
||||
headers: [
|
||||
{ uid: 'h1', name: 'X-API-Key', value: '{{apiKey}}', enabled: true },
|
||||
{ uid: 'h2', name: 'Accept', value: 'application/json', enabled: true },
|
||||
{ uid: 'h3', name: 'User-Agent', value: 'TestApp/{{version}}', enabled: true }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const collectionWithDifferentVars = {
|
||||
...testCollection,
|
||||
globalEnvironmentVariables: {
|
||||
...testCollection.globalEnvironmentVariables,
|
||||
apiKey: 'secret-key-456',
|
||||
version: '1.0.0'
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: requestWithDifferentHeaders,
|
||||
collection: collectionWithDifferentVars,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
// Body should have interpolated variables with proper formatting
|
||||
const expectedBody = `{
|
||||
"message": "Hello World",
|
||||
"count": 42
|
||||
}`;
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`);
|
||||
});
|
||||
|
||||
it('should handle complex nested JSON body', () => {
|
||||
const complexBody = {
|
||||
user: {
|
||||
name: '{{userName}}',
|
||||
settings: {
|
||||
theme: '{{userTheme}}',
|
||||
active: true
|
||||
}
|
||||
},
|
||||
data: {
|
||||
items: ['{{item1}}', '{{item2}}'],
|
||||
total: '{{totalCount}}'
|
||||
}
|
||||
};
|
||||
|
||||
const requestWithComplexBody = {
|
||||
...testRequest,
|
||||
request: {
|
||||
...testRequest.request,
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: JSON.stringify(complexBody, null, 2)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collectionWithComplexVars = {
|
||||
...testCollection,
|
||||
globalEnvironmentVariables: {
|
||||
...testCollection.globalEnvironmentVariables,
|
||||
userName: 'Alice',
|
||||
userTheme: 'dark',
|
||||
item1: 'first',
|
||||
item2: 'second',
|
||||
totalCount: 100
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: requestWithComplexBody,
|
||||
collection: collectionWithComplexVars,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const expectedComplexBody = JSON.stringify({
|
||||
user: {
|
||||
name: 'Alice',
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
active: true
|
||||
}
|
||||
},
|
||||
data: {
|
||||
items: ['first', 'second'],
|
||||
total: '100'
|
||||
}
|
||||
}, null, 2);
|
||||
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedComplexBody}'`);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', () => {
|
||||
// Set up the error mock after beforeEach has run
|
||||
const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;
|
||||
require('httpsnippet').HTTPSnippet = jest.fn(() => {
|
||||
throw new Error('Mock error!');
|
||||
});
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: testRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('Error generating code snippet');
|
||||
|
||||
require('httpsnippet').HTTPSnippet = originalHTTPSnippet;
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
it('should work with JavaScript language', () => {
|
||||
const javascriptLanguage = { target: 'javascript', client: 'fetch' };
|
||||
|
||||
const expectedJavaScriptCode = `fetch("https://api.example.com/data", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ "message": "Hello World", "count": 42 })
|
||||
})`;
|
||||
|
||||
const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;
|
||||
require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation(() => ({
|
||||
convert: jest.fn(() => expectedJavaScriptCode)
|
||||
}));
|
||||
|
||||
const result = generateSnippet({
|
||||
language: javascriptLanguage,
|
||||
item: testRequest,
|
||||
collection: testCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe(expectedJavaScriptCode);
|
||||
|
||||
// Restore the original mock
|
||||
require('httpsnippet').HTTPSnippet = originalHTTPSnippet;
|
||||
});
|
||||
|
||||
it('should interpolate simple headers and body variables', () => {
|
||||
const simpleTestRequest = {
|
||||
uid: 'test-123',
|
||||
name: 'simple test',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://api.test.com/{{endpoint}}',
|
||||
headers: [
|
||||
{ uid: 'h1', name: 'Authorization', value: 'Bearer {{token}}', enabled: true },
|
||||
{ uid: 'h2', name: 'X-User-ID', value: '{{userId}}', enabled: true },
|
||||
{ uid: 'h3', name: 'Content-Type', value: 'application/json', enabled: true }
|
||||
],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Simple collection with clear variable values
|
||||
const simpleTestCollection = {
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'none' },
|
||||
headers: []
|
||||
}
|
||||
},
|
||||
globalEnvironmentVariables: {
|
||||
endpoint: 'users',
|
||||
token: 'abc123token',
|
||||
userId: 'user456',
|
||||
userName: 'John Smith',
|
||||
userEmail: 'john@test.com',
|
||||
userAge: 30
|
||||
},
|
||||
runtimeVariables: {},
|
||||
processEnvVariables: {}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: simpleTestRequest,
|
||||
collection: simpleTestCollection,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const expectedInterpolatedBody = `{
|
||||
"name": "John Smith",
|
||||
"email": "john@test.com",
|
||||
"age": 30
|
||||
}`;
|
||||
|
||||
expect(result).toBe(`curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedInterpolatedBody}'`);
|
||||
});
|
||||
|
||||
it('should NOT interpolate when shouldInterpolate is false', () => {
|
||||
const simpleTestRequest = {
|
||||
uid: 'test-123',
|
||||
name: 'simple test',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://api.test.com/{{endpoint}}',
|
||||
headers: [
|
||||
{ uid: 'h1', name: 'Authorization', value: 'Bearer {{token}}', enabled: true },
|
||||
{ uid: 'h2', name: 'X-User-ID', value: '{{userId}}', enabled: true },
|
||||
{ uid: 'h3', name: 'Content-Type', value: 'application/json', enabled: true }
|
||||
],
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const simpleTestCollection = {
|
||||
root: {
|
||||
request: {
|
||||
auth: { mode: 'none' },
|
||||
headers: []
|
||||
}
|
||||
},
|
||||
globalEnvironmentVariables: {
|
||||
endpoint: 'users',
|
||||
token: 'abc123token',
|
||||
userId: 'user456',
|
||||
userName: 'John Smith',
|
||||
userEmail: 'john@test.com',
|
||||
userAge: 30
|
||||
},
|
||||
runtimeVariables: {},
|
||||
processEnvVariables: {}
|
||||
};
|
||||
|
||||
const result = generateSnippet({
|
||||
language: curlLanguage,
|
||||
item: simpleTestRequest,
|
||||
collection: simpleTestCollection,
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}\'');
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Modal from 'components/Modal';
|
||||
import Help from 'components/Help';
|
||||
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
||||
const inputRef = useRef();
|
||||
@@ -54,8 +56,16 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
</label>
|
||||
<div className="mt-2">{collectionName}</div>
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3 flex items-center">
|
||||
Location
|
||||
<Help>
|
||||
<p>
|
||||
Bruno stores your collections on your computer's filesystem.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Choose the location where you want to store this collection.
|
||||
</p>
|
||||
</Help>
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
|
||||
@@ -26,6 +26,11 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 0;
|
||||
|
||||
.CodeMirror-placeholder {
|
||||
color: ${(props) => props.theme.codemirror.placeholder.color} !important;
|
||||
opacity: ${(props) => props.theme.codemirror.placeholder.opacity} !important
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
|
||||
@@ -2,15 +2,11 @@ import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
|
||||
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
const CodeMirror = require('codemirror');
|
||||
|
||||
class SingleLineEditor extends Component {
|
||||
constructor(props) {
|
||||
@@ -26,6 +22,7 @@ class SingleLineEditor extends Component {
|
||||
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
/** @type {import("codemirror").Editor} */
|
||||
@@ -44,6 +41,7 @@ class SingleLineEditor extends Component {
|
||||
const noopHandler = () => {};
|
||||
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
placeholder: this.props.placeholder ?? '',
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
@@ -75,14 +73,21 @@ class SingleLineEditor extends Component {
|
||||
'Shift-Tab': false
|
||||
}
|
||||
});
|
||||
if (this.props.autocomplete) {
|
||||
this.editor.on('keyup', (cm, event) => {
|
||||
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.key !== 'Enter') {
|
||||
/*Enter - do not open autocomplete list just after item has been selected in it*/
|
||||
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup AutoComplete Helper
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: ['variables'],
|
||||
anywordAutocompleteHints: this.props.autocomplete
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
this.editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
@@ -94,7 +99,6 @@ class SingleLineEditor extends Component {
|
||||
_enableMaskedEditor = (enabled) => {
|
||||
if (typeof enabled !== 'boolean') return;
|
||||
|
||||
console.log('Enabling masked editor: ' + enabled);
|
||||
if (enabled == true) {
|
||||
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
|
||||
this.maskedEditor.enable();
|
||||
@@ -141,7 +145,14 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.editor.getWrapperElement().remove();
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.getWrapperElement().remove();
|
||||
this.editor = null;
|
||||
}
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
addOverlay = (variables) => {
|
||||
|
||||
@@ -6,12 +6,10 @@ const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding: 0 1px;
|
||||
|
||||
// for icon hover
|
||||
position: inherit;
|
||||
left: -4px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
grid-template-columns: ${({ columns }) =>
|
||||
columns?.[0]?.width
|
||||
|
||||
@@ -86,7 +86,7 @@ const Table = ({ minColumnWidth = 1, headers = [], children }) => {
|
||||
return (
|
||||
<StyledWrapper columns={columns}>
|
||||
<div className="relative">
|
||||
<table ref={tableRef} className="px-4 inherit left-[4px]">
|
||||
<table ref={tableRef} className="inherit">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ ref, name }, i) => (
|
||||
|
||||
@@ -96,7 +96,6 @@ const VariablesEditor = ({ collection }) => {
|
||||
<div className="mt-8 muted text-xs">
|
||||
Note: As of today, runtime variables can only be set via the API - <span className="font-medium">getVar()</span>{' '}
|
||||
and <span className="font-medium">setVar()</span>. <br />
|
||||
In the next release, we will add a UI to set and modify runtime variables.
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -10,40 +10,37 @@ import 'codemirror/theme/material.css';
|
||||
import 'codemirror/theme/monokai.css';
|
||||
import 'codemirror/addon/scroll/simplescrollbars.css';
|
||||
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
if (!SERVER_RENDERED) {
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/xml/xml');
|
||||
require('codemirror/mode/sparql/sparql');
|
||||
require('codemirror/addon/comment/comment');
|
||||
require('codemirror/addon/dialog/dialog');
|
||||
require('codemirror/addon/edit/closebrackets');
|
||||
require('codemirror/addon/edit/matchbrackets');
|
||||
require('codemirror/addon/fold/brace-fold');
|
||||
require('codemirror/addon/fold/foldgutter');
|
||||
require('codemirror/addon/fold/xml-fold');
|
||||
require('codemirror/addon/hint/javascript-hint');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
require('codemirror/addon/lint/lint');
|
||||
require('codemirror/addon/lint/json-lint');
|
||||
require('codemirror/addon/mode/overlay');
|
||||
require('codemirror/addon/scroll/simplescrollbars');
|
||||
require('codemirror/addon/search/jump-to-line');
|
||||
require('codemirror/addon/search/search');
|
||||
require('codemirror/addon/search/searchcursor');
|
||||
require('codemirror/addon/display/placeholder');
|
||||
require('codemirror/keymap/sublime');
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/xml/xml');
|
||||
require('codemirror/mode/sparql/sparql');
|
||||
require('codemirror/addon/comment/comment');
|
||||
require('codemirror/addon/dialog/dialog');
|
||||
require('codemirror/addon/edit/closebrackets');
|
||||
require('codemirror/addon/edit/matchbrackets');
|
||||
require('codemirror/addon/fold/brace-fold');
|
||||
require('codemirror/addon/fold/foldgutter');
|
||||
require('codemirror/addon/fold/xml-fold');
|
||||
require('codemirror/addon/hint/javascript-hint');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
require('codemirror/addon/lint/lint');
|
||||
require('codemirror/addon/lint/json-lint');
|
||||
require('codemirror/addon/mode/overlay');
|
||||
require('codemirror/addon/scroll/simplescrollbars');
|
||||
require('codemirror/addon/search/jump-to-line');
|
||||
require('codemirror/addon/search/search');
|
||||
require('codemirror/addon/search/searchcursor');
|
||||
require('codemirror/addon/display/placeholder');
|
||||
require('codemirror/keymap/sublime');
|
||||
|
||||
require('codemirror-graphql/hint');
|
||||
require('codemirror-graphql/info');
|
||||
require('codemirror-graphql/jump');
|
||||
require('codemirror-graphql/lint');
|
||||
require('codemirror-graphql/mode');
|
||||
require('codemirror-graphql/hint');
|
||||
require('codemirror-graphql/info');
|
||||
require('codemirror-graphql/jump');
|
||||
require('codemirror-graphql/lint');
|
||||
require('codemirror-graphql/mode');
|
||||
|
||||
require('utils/codemirror/brunoVarInfo');
|
||||
require('utils/codemirror/javascript-lint');
|
||||
require('utils/codemirror/autocomplete');
|
||||
}
|
||||
require('utils/codemirror/brunoVarInfo');
|
||||
require('utils/codemirror/javascript-lint');
|
||||
require('utils/codemirror/autocomplete');
|
||||
|
||||
export default function Main() {
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import filter from 'lodash/filter';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const initialState = {
|
||||
isDragging: false,
|
||||
@@ -26,6 +25,11 @@ const initialState = {
|
||||
codeFont: 'default'
|
||||
}
|
||||
},
|
||||
generateCode: {
|
||||
mainLanguage: 'Shell',
|
||||
library: 'curl',
|
||||
shouldInterpolate: true
|
||||
},
|
||||
cookies: [],
|
||||
taskQueue: [],
|
||||
systemProxyEnvVariables: {}
|
||||
@@ -76,6 +80,12 @@ export const appSlice = createSlice({
|
||||
},
|
||||
updateSystemProxyEnvVariables: (state, action) => {
|
||||
state.systemProxyEnvVariables = action.payload;
|
||||
},
|
||||
updateGenerateCode: (state, action) => {
|
||||
state.generateCode = {
|
||||
...state.generateCode,
|
||||
...action.payload
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -94,7 +104,8 @@ export const {
|
||||
insertTaskIntoQueue,
|
||||
removeTaskFromQueue,
|
||||
removeAllTasksFromQueue,
|
||||
updateSystemProxyEnvVariables
|
||||
updateSystemProxyEnvVariables,
|
||||
updateGenerateCode
|
||||
} = appSlice.actions;
|
||||
|
||||
export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||
@@ -103,14 +114,9 @@ export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-preferences', preferences)
|
||||
.then(() => toast.success('Preferences saved successfully'))
|
||||
.then(() => dispatch(updatePreferences(preferences)))
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('An error occurred while saving preferences');
|
||||
console.error(err);
|
||||
reject(err);
|
||||
});
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -579,7 +579,48 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
setQueryParams: (state, action) => {
|
||||
const { collectionUid, itemUid, params } = action.payload;
|
||||
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item || !isItemARequest(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
const existingOtherParams = item.draft.request.params?.filter(p => p.type !== 'query') || [];
|
||||
const newQueryParams = map(params, ({ name = '', value = '', enabled = true }) => ({
|
||||
uid: uuid(),
|
||||
name,
|
||||
value,
|
||||
description: '',
|
||||
type: 'query',
|
||||
enabled
|
||||
}));
|
||||
|
||||
item.draft.request.params = [...newQueryParams, ...existingOtherParams];
|
||||
|
||||
// Update the request URL to reflect the new query params
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const query = stringifyQueryParams(
|
||||
filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')
|
||||
);
|
||||
|
||||
// If there are enabled query params, append them to the URL
|
||||
if (query && query.length) {
|
||||
item.draft.request.url = parts[0] + '?' + query;
|
||||
} else {
|
||||
// If no enabled query params, remove the query part from URL
|
||||
item.draft.request.url = parts[0];
|
||||
}
|
||||
},
|
||||
moveQueryParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -785,6 +826,30 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
setRequestHeaders: (state, action) => {
|
||||
const { collectionUid, itemUid, headers } = action.payload;
|
||||
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (!item || !isItemARequest(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.headers = map(action.payload.headers, ({name = '', value = '', enabled = true}) => ({
|
||||
uid: uuid(),
|
||||
name: name,
|
||||
value: value,
|
||||
description: '',
|
||||
enabled: enabled
|
||||
}));
|
||||
},
|
||||
addFormUrlEncodedParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -1987,6 +2052,10 @@ export const collectionsSlice = createSlice({
|
||||
item.postResponseScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if(type === 'test-script-execution') {
|
||||
item.testScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if (type === 'request-queued') {
|
||||
const { cancelTokenUid } = action.payload;
|
||||
// ignore if request is already in progress or completed
|
||||
@@ -2269,6 +2338,7 @@ export const {
|
||||
requestUrlChanged,
|
||||
updateAuth,
|
||||
addQueryParam,
|
||||
setQueryParams,
|
||||
moveQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
@@ -2277,6 +2347,7 @@ export const {
|
||||
updateRequestHeader,
|
||||
deleteRequestHeader,
|
||||
moveRequestHeader,
|
||||
setRequestHeaders,
|
||||
addFormUrlEncodedParam,
|
||||
updateFormUrlEncodedParam,
|
||||
deleteFormUrlEncodedParam,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user